From a1fdb60e9453ead01eb5bb9b99fba67d6f8127b2 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 19 Nov 2024 12:24:40 -0800 Subject: [PATCH 01/20] Create folder for Engine --- .../{Engine.test.ts => Engine/index.test.ts} | 30 ++++--- app/core/{Engine.ts => Engine/index.ts} | 88 ++++++++++--------- 2 files changed, 65 insertions(+), 53 deletions(-) rename app/core/{Engine.test.ts => Engine/index.test.ts} (94%) rename app/core/{Engine.ts => Engine/index.ts} (97%) diff --git a/app/core/Engine.test.ts b/app/core/Engine/index.test.ts similarity index 94% rename from app/core/Engine.test.ts rename to app/core/Engine/index.test.ts index 530723e73f6..93f2770eb21 100644 --- a/app/core/Engine.test.ts +++ b/app/core/Engine/index.test.ts @@ -2,20 +2,20 @@ import Engine, { Engine as EngineClass, EngineState, TransactionEventPayload, -} from './Engine'; -import { backgroundState } from '../util/test/initial-root-state'; +} from '.'; +import { backgroundState } from '../../util/test/initial-root-state'; import { zeroAddress } from 'ethereumjs-util'; -import { createMockAccountsControllerState } from '../util/test/accountsControllerTestUtils'; -import { mockNetworkState } from '../util/test/network'; -import MetaMetrics from './Analytics/MetaMetrics'; -import { store } from '../store'; -import { MetaMetricsEvents } from './Analytics'; +import { createMockAccountsControllerState } from '../../util/test/accountsControllerTestUtils'; +import { mockNetworkState } from '../../util/test/network'; +import MetaMetrics from '../Analytics/MetaMetrics'; +import { store } from '../../store'; +import { MetaMetricsEvents } from '../Analytics'; import { NetworkState } from '@metamask/network-controller'; import { Hex } from '@metamask/utils'; -import { MarketDataDetails } from '../components/UI/Tokens'; +import { MarketDataDetails } from '../../components/UI/Tokens'; import { TransactionMeta } from '@metamask/transaction-controller'; -import { RootState } from '../reducers'; -import { MetricsEventBuilder } from './Analytics/MetricsEventBuilder'; +import { RootState } from '../../reducers'; +import { MetricsEventBuilder } from '../Analytics/MetricsEventBuilder'; jest.unmock('./Engine'); jest.mock('../store', () => ({ @@ -276,11 +276,17 @@ describe('Engine', () => { AccountTrackerController: { accountsByChainId: { [chainId]: { - [selectedAddress]: { balance: (ethBalance * 1e18).toString(), stakedBalance: (stakedEthBalance * 1e18).toString() }, + [selectedAddress]: { + balance: (ethBalance * 1e18).toString(), + stakedBalance: (stakedEthBalance * 1e18).toString(), + }, }, }, accounts: { - [selectedAddress]: { balance: (ethBalance * 1e18).toString(), stakedBalance: (stakedEthBalance * 1e18).toString() }, + [selectedAddress]: { + balance: (ethBalance * 1e18).toString(), + stakedBalance: (stakedEthBalance * 1e18).toString(), + }, }, }, TokensController: { diff --git a/app/core/Engine.ts b/app/core/Engine/index.ts similarity index 97% rename from app/core/Engine.ts rename to app/core/Engine/index.ts index 67f7dd6bc37..497a13b9b69 100644 --- a/app/core/Engine.ts +++ b/app/core/Engine/index.ts @@ -38,7 +38,7 @@ import { } from '@metamask/assets-controllers'; ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) import { AppState } from 'react-native'; -import PREINSTALLED_SNAPS from '../lib/snaps/preinstalled-snaps'; +import PREINSTALLED_SNAPS from '../../lib/snaps/preinstalled-snaps'; ///: END:ONLY_INCLUDE_IF import { AddressBookController, @@ -137,7 +137,7 @@ import { import { WebViewExecutionService } from '@metamask/snaps-controllers/react-native'; import { NotificationParameters } from '@metamask/snaps-rpc-methods/dist/restricted/notify.cjs'; -import { getSnapsWebViewPromise } from '../lib/snaps'; +import { getSnapsWebViewPromise } from '../../lib/snaps'; import { buildSnapEndowmentSpecifications, buildSnapRestrictedMethodSpecifications, @@ -158,16 +158,16 @@ import { LedgerMobileBridge, LedgerTransportMiddleware, } from '@metamask/eth-ledger-bridge-keyring'; -import { Encryptor, LEGACY_DERIVATION_OPTIONS } from './Encryptor'; +import { Encryptor, LEGACY_DERIVATION_OPTIONS } from '../Encryptor'; import { isMainnetByChainId, fetchEstimatedMultiLayerL1Fee, isTestNet, deprecatedGetNetworkId, getDecimalChainId, -} from '../util/networks'; -import AppConstants from './AppConstants'; -import { store } from '../store'; +} from '../../util/networks'; +import AppConstants from '../AppConstants'; +import { store } from '../../store'; import { renderFromTokenMinimalUnit, balanceToFiatNumber, @@ -175,11 +175,11 @@ import { toHexadecimal, addHexPrefix, hexToBN, -} from '../util/number'; -import NotificationManager from './NotificationManager'; -import Logger from '../util/Logger'; -import { isZero } from '../util/lodash'; -import { MetaMetricsEvents, MetaMetrics } from './Analytics'; +} from '../../util/number'; +import NotificationManager from '../NotificationManager'; +import Logger from '../../util/Logger'; +import { isZero } from '../../util/lodash'; +import { MetaMetricsEvents, MetaMetrics } from '../Analytics'; ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) import { @@ -190,8 +190,8 @@ import { detectSnapLocation, fetchFunction, DetectSnapLocationOptions, -} from './Snaps'; -import { getRpcMethodMiddleware } from './RPCMethods/RPCMethodMiddleware'; +} from '../Snaps'; +import { getRpcMethodMiddleware } from '../RPCMethods/RPCMethodMiddleware'; import { AuthenticationController, @@ -206,8 +206,8 @@ import { getCaveatSpecifications, getPermissionSpecifications, unrestrictedMethods, -} from './Permissions/specifications.js'; -import { backupVault } from './BackupVault'; +} from '../Permissions/specifications.js'; +import { backupVault } from '../BackupVault'; import { SignatureController, SignatureControllerActions, @@ -219,8 +219,8 @@ import { hasProperty, Hex, Json } from '@metamask/utils'; import { SwapsState } from '@metamask/swaps-controller/dist/SwapsController'; import { providerErrors } from '@metamask/rpc-errors'; -import { PPOM, ppomInit } from '../lib/ppom/PPOMView'; -import RNFSStorageBackend from '../lib/ppom/ppom-storage-backend'; +import { PPOM, ppomInit } from '../../lib/ppom/PPOMView'; +import RNFSStorageBackend from '../../lib/ppom/ppom-storage-backend'; import { AccountsController, AccountsControllerActions, @@ -233,22 +233,22 @@ import { lowerCase } from 'lodash'; import { networkIdUpdated, networkIdWillUpdate, -} from '../core/redux/slices/inpageProvider'; +} from '../../core/redux/slices/inpageProvider'; import SmartTransactionsController, { type SmartTransactionsControllerActions, type SmartTransactionsControllerEvents, type SmartTransactionsControllerState, } from '@metamask/smart-transactions-controller'; -import { getAllowedSmartTransactionsChainIds } from '../../app/constants/smartTransactions'; -import { selectShouldUseSmartTransaction } from '../selectors/smartTransactionsController'; -import { selectSwapsChainFeatureFlags } from '../reducers/swaps'; +import { getAllowedSmartTransactionsChainIds } from '../../../app/constants/smartTransactions'; +import { selectShouldUseSmartTransaction } from '../../selectors/smartTransactionsController'; +import { selectSwapsChainFeatureFlags } from '../../reducers/swaps'; import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; -import { submitSmartTransactionHook } from '../util/smart-transactions/smart-publish-hook'; +import { submitSmartTransactionHook } from '../../util/smart-transactions/smart-publish-hook'; import { zeroAddress } from 'ethereumjs-util'; import { ApprovalType, toChecksumHexAddress } from '@metamask/controller-utils'; -import { ExtendedControllerMessenger } from './ExtendedControllerMessenger'; +import { ExtendedControllerMessenger } from '../ExtendedControllerMessenger'; import EthQuery from '@metamask/eth-query'; -import DomainProxyMap from '../lib/DomainProxyMap/DomainProxyMap'; +import DomainProxyMap from '../../lib/DomainProxyMap/DomainProxyMap'; import { MetaMetricsEventCategory, MetaMetricsEventName, @@ -258,17 +258,17 @@ import { getSmartTransactionMetricsSensitiveProperties as getSmartTransactionMetricsSensitivePropertiesType, } from '@metamask/smart-transactions-controller/dist/utils'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) -import { snapKeyringBuilder } from './SnapKeyring'; -import { removeAccountsFromPermissions } from './Permissions'; -import { keyringSnapPermissionsBuilder } from './SnapKeyring/keyringSnapsPermissions'; -import { HandleSnapRequestArgs } from './Snaps/types'; -import { handleSnapRequest } from './Snaps/utils'; +import { snapKeyringBuilder } from '../SnapKeyring'; +import { removeAccountsFromPermissions } from '../Permissions'; +import { keyringSnapPermissionsBuilder } from '../SnapKeyring/keyringSnapsPermissions'; +import { HandleSnapRequestArgs } from '../Snaps/types'; +import { handleSnapRequest } from '../Snaps/utils'; ///: END:ONLY_INCLUDE_IF -import { getSmartTransactionMetricsProperties } from '../util/smart-transactions'; -import { trace } from '../util/trace'; -import { MetricsEventBuilder } from './Analytics/MetricsEventBuilder'; -import { JsonMap } from './Analytics/MetaMetrics.types'; -import { isPooledStakingFeatureEnabled } from '../components/UI/Stake/constants'; +import { getSmartTransactionMetricsProperties } from '../../util/smart-transactions'; +import { trace } from '../../util/trace'; +import { MetricsEventBuilder } from '../Analytics/MetricsEventBuilder'; +import { JsonMap } from '../Analytics/MetaMetrics.types'; +import { isPooledStakingFeatureEnabled } from '../../components/UI/Stake/constants'; const NON_EMPTY = 'NON_EMPTY'; @@ -1992,13 +1992,19 @@ export class Engine { selectSelectedInternalAccountChecksummedAddress ] ) { - const balanceBN = hexToBN(accountsByChainId[toHexadecimal(chainId)][ - selectSelectedInternalAccountChecksummedAddress - ].balance); - const stakedBalanceBN = hexToBN(accountsByChainId[toHexadecimal(chainId)][ - selectSelectedInternalAccountChecksummedAddress - ].stakedBalance || '0x00'); - const totalAccountBalance = balanceBN.add(stakedBalanceBN).toString('hex'); + const balanceBN = hexToBN( + accountsByChainId[toHexadecimal(chainId)][ + selectSelectedInternalAccountChecksummedAddress + ].balance, + ); + const stakedBalanceBN = hexToBN( + accountsByChainId[toHexadecimal(chainId)][ + selectSelectedInternalAccountChecksummedAddress + ].stakedBalance || '0x00', + ); + const totalAccountBalance = balanceBN + .add(stakedBalanceBN) + .toString('hex'); ethFiat = weiToFiatNumber( totalAccountBalance, conversionRate, From 88cb52ae5ce5898ea96789a1f4e30595aa5c7e0c Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 19 Nov 2024 12:57:22 -0800 Subject: [PATCH 02/20] Abstract types from Engine --- app/core/Engine/index.ts | 148 +------------------------------- app/core/Engine/types.ts | 179 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 144 deletions(-) create mode 100644 app/core/Engine/types.ts diff --git a/app/core/Engine/index.ts b/app/core/Engine/index.ts index 497a13b9b69..12a5ed95526 100644 --- a/app/core/Engine/index.ts +++ b/app/core/Engine/index.ts @@ -7,9 +7,6 @@ import { AssetsContractController, CurrencyRateController, CurrencyRateState, - CurrencyRateStateChange, - GetCurrencyRateState, - GetTokenListState, NftController, NftDetectionController, NftControllerState, @@ -17,24 +14,12 @@ import { TokenDetectionController, TokenListController, TokenListState, - TokenListStateChange, TokenRatesController, TokenRatesControllerState, TokensController, TokensControllerState, CodefiTokenPricesServiceV2, - TokensControllerActions, - TokensControllerEvents, - TokenListControllerActions, - TokenListControllerEvents, TokenBalancesControllerState, - AssetsContractControllerGetERC20BalanceOfAction, - AssetsContractControllerGetERC721AssetNameAction, - AssetsContractControllerGetERC721AssetSymbolAction, - AssetsContractControllerGetERC721TokenURIAction, - AssetsContractControllerGetERC721OwnerOfAction, - AssetsContractControllerGetERC1155BalanceOfAction, - AssetsContractControllerGetERC1155TokenURIAction, } from '@metamask/assets-controllers'; ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) import { AppState } from 'react-native'; @@ -42,8 +27,6 @@ import PREINSTALLED_SNAPS from '../../lib/snaps/preinstalled-snaps'; ///: END:ONLY_INCLUDE_IF import { AddressBookController, - AddressBookControllerActions, - AddressBookControllerEvents, AddressBookControllerState, } from '@metamask/address-book-controller'; import { BaseState } from '@metamask/base-controller'; @@ -51,86 +34,55 @@ import { ComposableController } from '@metamask/composable-controller'; import { KeyringController, KeyringControllerState, - KeyringControllerActions, - KeyringControllerEvents, ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) KeyringTypes, ///: END:ONLY_INCLUDE_IF } from '@metamask/keyring-controller'; import { NetworkController, - NetworkControllerActions, - NetworkControllerEvents, NetworkControllerMessenger, NetworkState, NetworkStatus, } from '@metamask/network-controller'; import { PhishingController, - PhishingControllerActions, - PhishingControllerEvents, PhishingControllerState, } from '@metamask/phishing-controller'; import { PreferencesController, - PreferencesControllerActions, - PreferencesControllerEvents, PreferencesState, } from '@metamask/preferences-controller'; import { TransactionController, - TransactionControllerEvents, TransactionControllerState, TransactionMeta, TransactionControllerOptions, } from '@metamask/transaction-controller'; -import { - GasFeeController, - GasFeeState, - GasFeeStateChange, - GetGasFeeState, -} from '@metamask/gas-fee-controller'; +import { GasFeeController, GasFeeState } from '@metamask/gas-fee-controller'; import { AcceptOptions, ApprovalController, - ApprovalControllerActions, - ApprovalControllerEvents, ApprovalControllerState, } from '@metamask/approval-controller'; import { SelectedNetworkController, SelectedNetworkControllerState, - SelectedNetworkControllerEvents, - SelectedNetworkControllerActions, } from '@metamask/selected-network-controller'; import { PermissionController, - PermissionControllerActions, - PermissionControllerEvents, PermissionControllerState, ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) SubjectMetadataController, - SubjectMetadataControllerActions, - SubjectMetadataControllerEvents, SubjectMetadataControllerState, ///: END:ONLY_INCLUDE_IF } from '@metamask/permission-controller'; import SwapsController, { swapsUtils } from '@metamask/swaps-controller'; -import { - PPOMController, - PPOMControllerActions, - PPOMControllerEvents, - PPOMState, -} from '@metamask/ppom-validator'; +import { PPOMController, PPOMState } from '@metamask/ppom-validator'; ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) import { JsonSnapsRegistry, - AllowedActions as SnapsAllowedActions, - AllowedEvents as SnapsAllowedEvents, SnapController, SnapsRegistryState, - SnapControllerEvents, - SnapControllerActions, PersistedSnapControllerState, SnapsRegistryMessenger, } from '@metamask/snaps-controllers'; @@ -150,8 +102,6 @@ import { MetaMaskKeyring as QRHardwareKeyring } from '@keystonehq/metamask-airga import { LoggingController, LoggingControllerState, - LoggingControllerActions, - LoggingControllerEvents, } from '@metamask/logging-controller'; import { LedgerKeyring, @@ -210,8 +160,6 @@ import { import { backupVault } from '../BackupVault'; import { SignatureController, - SignatureControllerActions, - SignatureControllerEvents, SignatureControllerOptions, } from '@metamask/signature-controller'; import { hasProperty, Hex, Json } from '@metamask/utils'; @@ -222,11 +170,9 @@ import { providerErrors } from '@metamask/rpc-errors'; import { PPOM, ppomInit } from '../../lib/ppom/PPOMView'; import RNFSStorageBackend from '../../lib/ppom/ppom-storage-backend'; import { + AccountsControllerState, AccountsController, - AccountsControllerActions, - AccountsControllerEvents, AccountsControllerMessenger, - AccountsControllerState, } from '@metamask/accounts-controller'; import { captureException } from '@sentry/react-native'; import { lowerCase } from 'lodash'; @@ -235,8 +181,6 @@ import { networkIdWillUpdate, } from '../../core/redux/slices/inpageProvider'; import SmartTransactionsController, { - type SmartTransactionsControllerActions, - type SmartTransactionsControllerEvents, type SmartTransactionsControllerState, } from '@metamask/smart-transactions-controller'; import { getAllowedSmartTransactionsChainIds } from '../../../app/constants/smartTransactions'; @@ -269,6 +213,7 @@ import { trace } from '../../util/trace'; import { MetricsEventBuilder } from '../Analytics/MetricsEventBuilder'; import { JsonMap } from '../Analytics/MetaMetrics.types'; import { isPooledStakingFeatureEnabled } from '../../components/UI/Stake/constants'; +import { ControllerMessenger } from './types'; const NON_EMPTY = 'NON_EMPTY'; @@ -279,82 +224,6 @@ const encryptor = new Encryptor({ // eslint-disable-next-line @typescript-eslint/no-explicit-any let currentChainId: any; -///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) -type AuthenticationControllerActions = AuthenticationController.AllowedActions; -type UserStorageControllerActions = UserStorageController.AllowedActions; -type NotificationsServicesControllerActions = - NotificationServicesController.AllowedActions; - -type SnapsGlobalActions = - | SnapControllerActions - | SubjectMetadataControllerActions - | PhishingControllerActions - | SnapsAllowedActions; - -type SnapsGlobalEvents = - | SnapControllerEvents - | SubjectMetadataControllerEvents - | PhishingControllerEvents - | SnapsAllowedEvents; -///: END:ONLY_INCLUDE_IF - -type GlobalActions = - | AddressBookControllerActions - | ApprovalControllerActions - | GetCurrencyRateState - | GetGasFeeState - | GetTokenListState - | KeyringControllerActions - | NetworkControllerActions - | PermissionControllerActions - | SignatureControllerActions - | LoggingControllerActions - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - | SnapsGlobalActions - | AuthenticationControllerActions - | UserStorageControllerActions - | NotificationsServicesControllerActions - ///: END:ONLY_INCLUDE_IF - | KeyringControllerActions - | AccountsControllerActions - | PreferencesControllerActions - | PPOMControllerActions - | TokensControllerActions - | TokenListControllerActions - | SelectedNetworkControllerActions - | SmartTransactionsControllerActions - | AssetsContractControllerGetERC20BalanceOfAction - | AssetsContractControllerGetERC721AssetNameAction - | AssetsContractControllerGetERC721AssetSymbolAction - | AssetsContractControllerGetERC721TokenURIAction - | AssetsContractControllerGetERC721OwnerOfAction - | AssetsContractControllerGetERC1155BalanceOfAction - | AssetsContractControllerGetERC1155TokenURIAction; - -type GlobalEvents = - | AddressBookControllerEvents - | ApprovalControllerEvents - | CurrencyRateStateChange - | GasFeeStateChange - | KeyringControllerEvents - | TokenListStateChange - | NetworkControllerEvents - | PermissionControllerEvents - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - | SnapsGlobalEvents - ///: END:ONLY_INCLUDE_IF - | SignatureControllerEvents - | LoggingControllerEvents - | KeyringControllerEvents - | PPOMControllerEvents - | AccountsControllerEvents - | PreferencesControllerEvents - | TokensControllerEvents - | TokenListControllerEvents - | TransactionControllerEvents - | SelectedNetworkControllerEvents - | SmartTransactionsControllerEvents; - type PermissionsByRpcMethod = ReturnType; type Permissions = PermissionsByRpcMethod[keyof PermissionsByRpcMethod]; @@ -453,15 +322,6 @@ type OptionalControllers = Pick; */ export type EngineContext = RequiredControllers & Partial; -/** - * Type definition for the controller messenger used in the Engine. - * It extends the base ControllerMessenger with global actions and events. - */ -export type ControllerMessenger = ExtendedControllerMessenger< - GlobalActions, - GlobalEvents ->; - export interface TransactionEventPayload { transactionMeta: TransactionMeta; actionId?: string; diff --git a/app/core/Engine/types.ts b/app/core/Engine/types.ts new file mode 100644 index 00000000000..d66d2237f65 --- /dev/null +++ b/app/core/Engine/types.ts @@ -0,0 +1,179 @@ +import { ExtendedControllerMessenger } from '../ExtendedControllerMessenger'; +import { + CurrencyRateStateChange, + GetCurrencyRateState, + GetTokenListState, + TokenListStateChange, + TokensControllerActions, + TokensControllerEvents, + TokenListControllerActions, + TokenListControllerEvents, + AssetsContractControllerGetERC20BalanceOfAction, + AssetsContractControllerGetERC721AssetNameAction, + AssetsContractControllerGetERC721AssetSymbolAction, + AssetsContractControllerGetERC721TokenURIAction, + AssetsContractControllerGetERC721OwnerOfAction, + AssetsContractControllerGetERC1155BalanceOfAction, + AssetsContractControllerGetERC1155TokenURIAction, +} from '@metamask/assets-controllers'; +import { + AddressBookControllerActions, + AddressBookControllerEvents, +} from '@metamask/address-book-controller'; +import { + KeyringControllerActions, + KeyringControllerEvents, +} from '@metamask/keyring-controller'; +import { + NetworkControllerActions, + NetworkControllerEvents, +} from '@metamask/network-controller'; +import { + PhishingControllerActions, + PhishingControllerEvents, +} from '@metamask/phishing-controller'; +import { + PreferencesControllerActions, + PreferencesControllerEvents, +} from '@metamask/preferences-controller'; +import { TransactionControllerEvents } from '@metamask/transaction-controller'; +import { + GasFeeStateChange, + GetGasFeeState, +} from '@metamask/gas-fee-controller'; +import { + ApprovalControllerActions, + ApprovalControllerEvents, +} from '@metamask/approval-controller'; +import { + SelectedNetworkControllerEvents, + SelectedNetworkControllerActions, +} from '@metamask/selected-network-controller'; +import { + PermissionControllerActions, + PermissionControllerEvents, + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + SubjectMetadataControllerActions, + SubjectMetadataControllerEvents, + ///: END:ONLY_INCLUDE_IF +} from '@metamask/permission-controller'; +import { + PPOMControllerActions, + PPOMControllerEvents, +} from '@metamask/ppom-validator'; +///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) +import { + AllowedActions as SnapsAllowedActions, + AllowedEvents as SnapsAllowedEvents, + SnapControllerEvents, + SnapControllerActions, +} from '@metamask/snaps-controllers'; +///: END:ONLY_INCLUDE_IF +import { + LoggingControllerActions, + LoggingControllerEvents, +} from '@metamask/logging-controller'; +import { + SignatureControllerActions, + SignatureControllerEvents, +} from '@metamask/signature-controller'; +import { + type SmartTransactionsControllerActions, + type SmartTransactionsControllerEvents, +} from '@metamask/smart-transactions-controller'; +///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) +import { + AuthenticationController, + UserStorageController, +} from '@metamask/profile-sync-controller'; +import { NotificationServicesController } from '@metamask/notification-services-controller'; +///: END:ONLY_INCLUDE_IF +import { + AccountsControllerActions, + AccountsControllerEvents, +} from './controllers/AccountsControllerService'; + +///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) +type AuthenticationControllerActions = AuthenticationController.AllowedActions; +type UserStorageControllerActions = UserStorageController.AllowedActions; +type NotificationsServicesControllerActions = + NotificationServicesController.AllowedActions; + +type SnapsGlobalActions = + | SnapControllerActions + | SubjectMetadataControllerActions + | PhishingControllerActions + | SnapsAllowedActions; + +type SnapsGlobalEvents = + | SnapControllerEvents + | SubjectMetadataControllerEvents + | PhishingControllerEvents + | SnapsAllowedEvents; +///: END:ONLY_INCLUDE_IF + +type GlobalActions = + | AddressBookControllerActions + | ApprovalControllerActions + | GetCurrencyRateState + | GetGasFeeState + | GetTokenListState + | KeyringControllerActions + | NetworkControllerActions + | PermissionControllerActions + | SignatureControllerActions + | LoggingControllerActions + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + | SnapsGlobalActions + | AuthenticationControllerActions + | UserStorageControllerActions + | NotificationsServicesControllerActions + ///: END:ONLY_INCLUDE_IF + | KeyringControllerActions + | AccountsControllerActions + | PreferencesControllerActions + | PPOMControllerActions + | TokensControllerActions + | TokenListControllerActions + | SelectedNetworkControllerActions + | SmartTransactionsControllerActions + | AssetsContractControllerGetERC20BalanceOfAction + | AssetsContractControllerGetERC721AssetNameAction + | AssetsContractControllerGetERC721AssetSymbolAction + | AssetsContractControllerGetERC721TokenURIAction + | AssetsContractControllerGetERC721OwnerOfAction + | AssetsContractControllerGetERC1155BalanceOfAction + | AssetsContractControllerGetERC1155TokenURIAction; + +type GlobalEvents = + | AddressBookControllerEvents + | ApprovalControllerEvents + | CurrencyRateStateChange + | GasFeeStateChange + | KeyringControllerEvents + | TokenListStateChange + | NetworkControllerEvents + | PermissionControllerEvents + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + | SnapsGlobalEvents + ///: END:ONLY_INCLUDE_IF + | SignatureControllerEvents + | LoggingControllerEvents + | KeyringControllerEvents + | PPOMControllerEvents + | AccountsControllerEvents + | PreferencesControllerEvents + | TokensControllerEvents + | TokenListControllerEvents + | TransactionControllerEvents + | SelectedNetworkControllerEvents + | SmartTransactionsControllerEvents; + +/** + * Type definition for the controller messenger used in the Engine. + * It extends the base ControllerMessenger with global actions and events. + */ +export type ControllerMessenger = ExtendedControllerMessenger< + GlobalActions, + GlobalEvents +>; From 7fea3f401a9fc0f6c38b99f7be33aa2631471070 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Wed, 20 Nov 2024 20:29:32 -0800 Subject: [PATCH 03/20] Reorganize engine file structure --- .../Engine/{index.test.ts => Engine.test.ts} | 4 +- app/core/Engine/Engine.ts | 2206 ++++++++++++++++ app/core/Engine/Engine.types.ts | 321 +++ app/core/Engine/index.ts | 2333 +---------------- app/core/Engine/types.ts | 179 -- 5 files changed, 2531 insertions(+), 2512 deletions(-) rename app/core/Engine/{index.test.ts => Engine.test.ts} (99%) create mode 100644 app/core/Engine/Engine.ts create mode 100644 app/core/Engine/Engine.types.ts delete mode 100644 app/core/Engine/types.ts diff --git a/app/core/Engine/index.test.ts b/app/core/Engine/Engine.test.ts similarity index 99% rename from app/core/Engine/index.test.ts rename to app/core/Engine/Engine.test.ts index 93f2770eb21..48c1f0d2433 100644 --- a/app/core/Engine/index.test.ts +++ b/app/core/Engine/Engine.test.ts @@ -1,8 +1,8 @@ import Engine, { Engine as EngineClass, - EngineState, TransactionEventPayload, -} from '.'; +} from './Engine'; +import { EngineState } from './Engine.types'; import { backgroundState } from '../../util/test/initial-root-state'; import { zeroAddress } from 'ethereumjs-util'; import { createMockAccountsControllerState } from '../../util/test/accountsControllerTestUtils'; diff --git a/app/core/Engine/Engine.ts b/app/core/Engine/Engine.ts new file mode 100644 index 00000000000..e3a95b77e9e --- /dev/null +++ b/app/core/Engine/Engine.ts @@ -0,0 +1,2206 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +import Crypto from 'react-native-quick-crypto'; +import { scrypt } from 'react-native-fast-crypto'; +import { + AccountTrackerController, + AssetsContractController, + CurrencyRateController, + NftController, + NftDetectionController, + TokenBalancesController, + TokenDetectionController, + TokenListController, + TokenRatesController, + TokensController, + CodefiTokenPricesServiceV2, +} from '@metamask/assets-controllers'; +///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) +import { AppState } from 'react-native'; +import PREINSTALLED_SNAPS from '../../lib/snaps/preinstalled-snaps'; +///: END:ONLY_INCLUDE_IF +import { AddressBookController } from '@metamask/address-book-controller'; +import { ComposableController } from '@metamask/composable-controller'; +import { + KeyringController, + KeyringControllerState, + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + KeyringTypes, + ///: END:ONLY_INCLUDE_IF +} from '@metamask/keyring-controller'; +import { + NetworkController, + NetworkControllerMessenger, + NetworkState, + NetworkStatus, +} from '@metamask/network-controller'; +import { PhishingController } from '@metamask/phishing-controller'; +import { PreferencesController } from '@metamask/preferences-controller'; +import { + TransactionController, + TransactionMeta, + TransactionControllerOptions, +} from '@metamask/transaction-controller'; +import { GasFeeController } from '@metamask/gas-fee-controller'; +import { + AcceptOptions, + ApprovalController, +} from '@metamask/approval-controller'; +import { SelectedNetworkController } from '@metamask/selected-network-controller'; +import { + PermissionController, + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + SubjectMetadataController, + ///: END:ONLY_INCLUDE_IF +} from '@metamask/permission-controller'; +import SwapsController, { swapsUtils } from '@metamask/swaps-controller'; +import { PPOMController } from '@metamask/ppom-validator'; +///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) +import { + JsonSnapsRegistry, + SnapController, + SnapsRegistryMessenger, +} from '@metamask/snaps-controllers'; + +import { WebViewExecutionService } from '@metamask/snaps-controllers/react-native'; +import { NotificationParameters } from '@metamask/snaps-rpc-methods/dist/restricted/notify.cjs'; +import { getSnapsWebViewPromise } from '../../lib/snaps'; +import { + buildSnapEndowmentSpecifications, + buildSnapRestrictedMethodSpecifications, +} from '@metamask/snaps-rpc-methods'; +import type { EnumToUnion, DialogType } from '@metamask/snaps-sdk'; +// eslint-disable-next-line import/no-nodejs-modules +import { Duplex } from 'stream'; +///: END:ONLY_INCLUDE_IF +import { MetaMaskKeyring as QRHardwareKeyring } from '@keystonehq/metamask-airgapped-keyring'; +import { LoggingController } from '@metamask/logging-controller'; +import { + LedgerKeyring, + LedgerMobileBridge, + LedgerTransportMiddleware, +} from '@metamask/eth-ledger-bridge-keyring'; +import { Encryptor, LEGACY_DERIVATION_OPTIONS } from '../Encryptor'; +import { + isMainnetByChainId, + fetchEstimatedMultiLayerL1Fee, + isTestNet, + deprecatedGetNetworkId, + getDecimalChainId, +} from '../../util/networks'; +import AppConstants from '../AppConstants'; +import { store } from '../../store'; +import { + renderFromTokenMinimalUnit, + balanceToFiatNumber, + weiToFiatNumber, + toHexadecimal, + addHexPrefix, + hexToBN, +} from '../../util/number'; +import NotificationManager from '../NotificationManager'; +import Logger from '../../util/Logger'; +import { isZero } from '../../util/lodash'; +import { MetaMetricsEvents, MetaMetrics } from '../Analytics'; + +///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) +import { + SnapBridge, + ExcludedSnapEndowments, + ExcludedSnapPermissions, + EndowmentPermissions, + detectSnapLocation, + fetchFunction, + DetectSnapLocationOptions, +} from '../Snaps'; +import { getRpcMethodMiddleware } from '../RPCMethods/RPCMethodMiddleware'; + +import { + AuthenticationController, + UserStorageController, +} from '@metamask/profile-sync-controller'; +import { + NotificationServicesController, + NotificationServicesPushController, +} from '@metamask/notification-services-controller'; +///: END:ONLY_INCLUDE_IF +import { + getCaveatSpecifications, + getPermissionSpecifications, + unrestrictedMethods, +} from '../Permissions/specifications.js'; +import { backupVault } from '../BackupVault'; +import { + SignatureController, + SignatureControllerOptions, +} from '@metamask/signature-controller'; +import { hasProperty, Hex, Json } from '@metamask/utils'; +import { providerErrors } from '@metamask/rpc-errors'; + +import { PPOM, ppomInit } from '../../lib/ppom/PPOMView'; +import RNFSStorageBackend from '../../lib/ppom/ppom-storage-backend'; +import { + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + AccountsControllerSetSelectedAccountAction, + AccountsControllerGetAccountByAddressAction, + AccountsControllerSetAccountNameAction, + ///: END:ONLY_INCLUDE_IF + createAccountsController, + AccountsControllerGetAccountAction, + AccountsControllerGetSelectedAccountAction, + AccountsControllerListAccountsAction, + AccountsControllerUpdateAccountMetadataAction, + AccountsControllerSelectedEvmAccountChangeEvent, + AccountsControllerSelectedAccountChangeEvent, + AccountsControllerAccountAddedEvent, + AccountsControllerAccountRenamedEvent, +} from './controllers/accountControllerUtils'; +import { captureException } from '@sentry/react-native'; +import { lowerCase } from 'lodash'; +import { + networkIdUpdated, + networkIdWillUpdate, +} from '../../core/redux/slices/inpageProvider'; +import SmartTransactionsController from '@metamask/smart-transactions-controller'; +import { getAllowedSmartTransactionsChainIds } from '../../../app/constants/smartTransactions'; +import { selectShouldUseSmartTransaction } from '../../selectors/smartTransactionsController'; +import { selectSwapsChainFeatureFlags } from '../../reducers/swaps'; +import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; +import { submitSmartTransactionHook } from '../../util/smart-transactions/smart-publish-hook'; +import { zeroAddress } from 'ethereumjs-util'; +import { ApprovalType, toChecksumHexAddress } from '@metamask/controller-utils'; +import { ExtendedControllerMessenger } from '../ExtendedControllerMessenger'; +import EthQuery from '@metamask/eth-query'; +import DomainProxyMap from '../../lib/DomainProxyMap/DomainProxyMap'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '@metamask/smart-transactions-controller/dist/constants'; +import { + getSmartTransactionMetricsProperties as getSmartTransactionMetricsPropertiesType, + getSmartTransactionMetricsSensitiveProperties as getSmartTransactionMetricsSensitivePropertiesType, +} from '@metamask/smart-transactions-controller/dist/utils'; +///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) +import { snapKeyringBuilder } from '../SnapKeyring'; +import { removeAccountsFromPermissions } from '../Permissions'; +import { keyringSnapPermissionsBuilder } from '../SnapKeyring/keyringSnapsPermissions'; +import { HandleSnapRequestArgs } from '../Snaps/types'; +import { handleSnapRequest } from '../Snaps/utils'; +///: END:ONLY_INCLUDE_IF +import { getSmartTransactionMetricsProperties } from '../../util/smart-transactions'; +import { trace } from '../../util/trace'; +import { MetricsEventBuilder } from '../Analytics/MetricsEventBuilder'; +import { JsonMap } from '../Analytics/MetaMetrics.types'; +import { isPooledStakingFeatureEnabled } from '../../components/UI/Stake/constants'; +import { ControllerMessenger, Controllers, EngineState } from './Engine.types'; + +const NON_EMPTY = 'NON_EMPTY'; + +const encryptor = new Encryptor({ + keyDerivationOptions: LEGACY_DERIVATION_OPTIONS, +}); +// TODO: Replace "any" with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let currentChainId: any; + +/** + * Controllers that area always instantiated + */ +type RequiredControllers = Omit; + +/** + * Controllers that are sometimes not instantiated + */ +type OptionalControllers = Pick; + +/** + * Combines required and optional controllers for the Engine context type. + */ +export type EngineContext = RequiredControllers & Partial; + +export interface TransactionEventPayload { + transactionMeta: TransactionMeta; + actionId?: string; + error?: string; +} + +/** + * Core controller responsible for composing other metamask controllers together + * and exposing convenience methods for common wallet operations. + */ +export class Engine { + /** + * The global Engine singleton + */ + static instance: Engine | null; + /** + * A collection of all controller instances + */ + context: EngineContext; + /** + * The global controller messenger. + */ + controllerMessenger: ControllerMessenger; + /** + * ComposableController reference containing all child controllers + */ + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + datamodel: any; + + /** + * Object containing the info for the latest incoming tx block + * for each address and network + */ + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + lastIncomingTxBlockInfo: any; + + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + /** + * Object that runs and manages the execution of Snaps + */ + snapExecutionService: WebViewExecutionService; + snapController: SnapController; + subjectMetadataController: SubjectMetadataController; + + ///: END:ONLY_INCLUDE_IF + + transactionController: TransactionController; + smartTransactionsController: SmartTransactionsController; + + keyringController: KeyringController; + + /** + * Creates a CoreController instance + */ + // eslint-disable-next-line @typescript-eslint/default-param-last + constructor( + initialState: Partial = {}, + initialKeyringState?: KeyringControllerState | null, + ) { + this.controllerMessenger = new ExtendedControllerMessenger(); + + const approvalController = new ApprovalController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'ApprovalController', + allowedEvents: [], + allowedActions: [], + }), + showApprovalRequest: () => undefined, + typesExcludedFromRateLimiting: [ + ApprovalType.Transaction, + ApprovalType.WatchAsset, + ], + }); + + const preferencesController = new PreferencesController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'PreferencesController', + allowedActions: [], + allowedEvents: ['KeyringController:stateChange'], + }), + state: { + ipfsGateway: AppConstants.IPFS_DEFAULT_GATEWAY_URL, + useTokenDetection: + initialState?.PreferencesController?.useTokenDetection ?? true, + useNftDetection: true, // set this to true to enable nft detection by default to new users + displayNftMedia: true, + securityAlertsEnabled: true, + smartTransactionsOptInStatus: true, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + ...initialState.PreferencesController, + }, + }); + + const networkControllerOpts = { + infuraProjectId: process.env.MM_INFURA_PROJECT_ID || NON_EMPTY, + state: initialState.NetworkController, + messenger: this.controllerMessenger.getRestricted({ + name: 'NetworkController', + allowedEvents: [], + allowedActions: [], + }) as unknown as NetworkControllerMessenger, + // Metrics event tracking is handled in this repository instead + // TODO: Use events for controller metric events + trackMetaMetricsEvent: () => { + // noop + }, + }; + const networkController = new NetworkController(networkControllerOpts); + + networkController.initializeProvider(); + + const assetsContractController = new AssetsContractController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'AssetsContractController', + allowedActions: [ + 'NetworkController:getNetworkClientById', + 'NetworkController:getNetworkConfigurationByNetworkClientId', + 'NetworkController:getSelectedNetworkClient', + 'NetworkController:getState', + ], + allowedEvents: [ + 'PreferencesController:stateChange', + 'NetworkController:networkDidChange', + ], + }), + chainId: networkController.getNetworkClientById( + networkController?.state.selectedNetworkClientId, + ).configuration.chainId, + }); + + // Create AccountsController + const accountsController = createAccountsController({ + controllerMessenger: this.controllerMessenger, + initialState: initialState.AccountsController, + }); + + const nftController = new NftController({ + chainId: networkController.getNetworkClientById( + networkController?.state.selectedNetworkClientId, + ).configuration.chainId, + useIpfsSubdomains: false, + messenger: this.controllerMessenger.getRestricted({ + name: 'NftController', + allowedActions: [ + `${approvalController.name}:addRequest`, + `${networkController.name}:getNetworkClientById`, + AccountsControllerGetAccountAction, + AccountsControllerGetSelectedAccountAction, + 'AssetsContractController:getERC721AssetName', + 'AssetsContractController:getERC721AssetSymbol', + 'AssetsContractController:getERC721TokenURI', + 'AssetsContractController:getERC721OwnerOf', + 'AssetsContractController:getERC1155BalanceOf', + 'AssetsContractController:getERC1155TokenURI', + ], + allowedEvents: [ + 'PreferencesController:stateChange', + 'NetworkController:networkDidChange', + AccountsControllerSelectedEvmAccountChangeEvent, + ], + }), + }); + + const loggingController = new LoggingController({ + messenger: this.controllerMessenger.getRestricted< + 'LoggingController', + never, + never + >({ + name: 'LoggingController', + allowedActions: [], + allowedEvents: [], + }), + state: initialState.LoggingController, + }); + const tokensController = new TokensController({ + chainId: networkController.getNetworkClientById( + networkController?.state.selectedNetworkClientId, + ).configuration.chainId, + // @ts-expect-error at this point in time the provider will be defined by the `networkController.initializeProvider` + provider: networkController.getProviderAndBlockTracker().provider, + state: initialState.TokensController, + messenger: this.controllerMessenger.getRestricted({ + name: 'TokensController', + allowedActions: [ + `${approvalController.name}:addRequest`, + 'NetworkController:getNetworkClientById', + AccountsControllerGetAccountAction, + AccountsControllerGetSelectedAccountAction, + ], + allowedEvents: [ + 'PreferencesController:stateChange', + 'NetworkController:networkDidChange', + 'TokenListController:stateChange', + AccountsControllerSelectedEvmAccountChangeEvent, + ], + }), + }); + const tokenListController = new TokenListController({ + chainId: networkController.getNetworkClientById( + networkController?.state.selectedNetworkClientId, + ).configuration.chainId, + onNetworkStateChange: (listener) => + this.controllerMessenger.subscribe( + AppConstants.NETWORK_STATE_CHANGE_EVENT, + listener, + ), + messenger: this.controllerMessenger.getRestricted({ + name: 'TokenListController', + allowedActions: [`${networkController.name}:getNetworkClientById`], + allowedEvents: [`${networkController.name}:stateChange`], + }), + }); + const currencyRateController = new CurrencyRateController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'CurrencyRateController', + allowedActions: [`${networkController.name}:getNetworkClientById`], + allowedEvents: [], + }), + state: initialState.CurrencyRateController, + }); + + const gasFeeController = new GasFeeController({ + // @ts-expect-error TODO: Resolve mismatch between base-controller versions. + messenger: this.controllerMessenger.getRestricted({ + name: 'GasFeeController', + allowedActions: [ + `${networkController.name}:getNetworkClientById`, + `${networkController.name}:getEIP1559Compatibility`, + `${networkController.name}:getState`, + ], + allowedEvents: [AppConstants.NETWORK_DID_CHANGE_EVENT], + }), + getProvider: () => + // @ts-expect-error at this point in time the provider will be defined by the `networkController.initializeProvider` + networkController.getProviderAndBlockTracker().provider, + getCurrentNetworkEIP1559Compatibility: async () => + (await networkController.getEIP1559Compatibility()) ?? false, + getCurrentNetworkLegacyGasAPICompatibility: () => { + const chainId = networkController.getNetworkClientById( + networkController?.state.selectedNetworkClientId, + ).configuration.chainId; + return ( + isMainnetByChainId(chainId) || + chainId === addHexPrefix(swapsUtils.BSC_CHAIN_ID) || + chainId === addHexPrefix(swapsUtils.POLYGON_CHAIN_ID) + ); + }, + clientId: AppConstants.SWAPS.CLIENT_ID, + legacyAPIEndpoint: + 'https://gas.api.cx.metamask.io/networks//gasPrices', + EIP1559APIEndpoint: + 'https://gas.api.cx.metamask.io/networks//suggestedGasFees', + }); + + const phishingController = new PhishingController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'PhishingController', + allowedActions: [], + allowedEvents: [], + }), + }); + phishingController.maybeUpdateState(); + + const additionalKeyrings = []; + + const qrKeyringBuilder = () => { + const keyring = new QRHardwareKeyring(); + // to fix the bug in #9560, forgetDevice will reset all keyring properties to default. + keyring.forgetDevice(); + return keyring; + }; + qrKeyringBuilder.type = QRHardwareKeyring.type; + + additionalKeyrings.push(qrKeyringBuilder); + + const bridge = new LedgerMobileBridge(new LedgerTransportMiddleware()); + const ledgerKeyringBuilder = () => new LedgerKeyring({ bridge }); + ledgerKeyringBuilder.type = LedgerKeyring.type; + + additionalKeyrings.push(ledgerKeyringBuilder); + + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + const snapKeyringBuildMessenger = this.controllerMessenger.getRestricted({ + name: 'SnapKeyringBuilder', + allowedActions: [ + 'ApprovalController:addRequest', + 'ApprovalController:acceptRequest', + 'ApprovalController:rejectRequest', + 'ApprovalController:startFlow', + 'ApprovalController:endFlow', + 'ApprovalController:showSuccess', + 'ApprovalController:showError', + 'PhishingController:testOrigin', + 'PhishingController:maybeUpdateState', + 'KeyringController:getAccounts', + AccountsControllerSetSelectedAccountAction, + AccountsControllerGetAccountByAddressAction, + AccountsControllerSetAccountNameAction, + ], + allowedEvents: [], + }); + + const getSnapController = () => this.snapController; + + // Necessary to persist the keyrings and update the accounts both within the keyring controller and accounts controller + const persistAndUpdateAccounts = async () => { + await this.keyringController.persistAllKeyrings(); + await accountsController.updateAccounts(); + }; + + additionalKeyrings.push( + snapKeyringBuilder( + snapKeyringBuildMessenger, + getSnapController, + persistAndUpdateAccounts, + (address) => this.removeAccount(address), + ), + ); + + ///: END:ONLY_INCLUDE_IF + + this.keyringController = new KeyringController({ + removeIdentity: preferencesController.removeIdentity.bind( + preferencesController, + ), + encryptor, + messenger: this.controllerMessenger.getRestricted({ + name: 'KeyringController', + allowedActions: [], + allowedEvents: [], + }), + state: initialKeyringState || initialState.KeyringController, + // @ts-expect-error To Do: Update the type of QRHardwareKeyring to Keyring + keyringBuilders: additionalKeyrings, + }); + + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + /** + * Gets the mnemonic of the user's primary keyring. + */ + const getPrimaryKeyringMnemonic = () => { + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [keyring]: any = this.keyringController.getKeyringsByType( + KeyringTypes.hd, + ); + if (!keyring.mnemonic) { + throw new Error('Primary keyring mnemonic unavailable.'); + } + + return keyring.mnemonic; + }; + + const getAppState = () => { + const state = AppState.currentState; + return state === 'active'; + }; + + const snapRestrictedMethods = { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + clearSnapState: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapController:clearSnapState', + ), + getMnemonic: getPrimaryKeyringMnemonic.bind(this), + getUnlockPromise: getAppState.bind(this), + getSnap: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapController:get', + ), + handleSnapRpcRequest: async (args: HandleSnapRequestArgs) => + await handleSnapRequest(this.controllerMessenger, args), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + getSnapState: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapController:getSnapState', + ), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + updateSnapState: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapController:updateSnapState', + ), + maybeUpdatePhishingList: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'PhishingController:maybeUpdateState', + ), + isOnPhishingList: (origin: string) => + this.controllerMessenger.call<'PhishingController:testOrigin'>( + 'PhishingController:testOrigin', + origin, + ).result, + showDialog: ( + origin: string, + type: EnumToUnion, + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + content: any, // should be Component from '@metamask/snaps-ui'; + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + placeholder?: any, + ) => + approvalController.addAndShowApprovalRequest({ + origin, + type, + requestData: { content, placeholder }, + }), + showInAppNotification: (origin: string, args: NotificationParameters) => { + Logger.log( + 'Snaps/ showInAppNotification called with args: ', + args, + ' and origin: ', + origin, + ); + }, + hasPermission: (origin: string, target: string) => + this.controllerMessenger.call<'PermissionController:hasPermission'>( + 'PermissionController:hasPermission', + origin, + target, + ), + }; + ///: END:ONLY_INCLUDE_IF + + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + const keyringSnapMethods = { + getAllowedKeyringMethods: (origin: string) => + keyringSnapPermissionsBuilder(origin), + getSnapKeyring: this.getSnapKeyring.bind(this), + }; + ///: END:ONLY_INCLUDE_IF + + const getSnapPermissionSpecifications = () => ({ + ...buildSnapEndowmentSpecifications(Object.keys(ExcludedSnapEndowments)), + ...buildSnapRestrictedMethodSpecifications( + Object.keys(ExcludedSnapPermissions), + { + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + ...snapRestrictedMethods, + ///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + ...keyringSnapMethods, + ///: END:ONLY_INCLUDE_IF + }, + ), + }); + + const accountTrackerController = new AccountTrackerController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'AccountTrackerController', + allowedActions: [ + AccountsControllerGetSelectedAccountAction, + AccountsControllerListAccountsAction, + 'PreferencesController:getState', + 'NetworkController:getState', + 'NetworkController:getNetworkClientById', + ], + allowedEvents: [ + AccountsControllerSelectedEvmAccountChangeEvent, + AccountsControllerSelectedAccountChangeEvent, + ], + }), + state: initialState.AccountTrackerController ?? { accounts: {} }, + getStakedBalanceForChain: + assetsContractController.getStakedBalanceForChain.bind( + assetsContractController, + ), + includeStakedAssets: isPooledStakingFeatureEnabled(), + }); + const permissionController = new PermissionController({ + // @ts-expect-error TODO: Resolve mismatch between base-controller versions. + messenger: this.controllerMessenger.getRestricted({ + name: 'PermissionController', + allowedActions: [ + `${approvalController.name}:addRequest`, + `${approvalController.name}:hasRequest`, + `${approvalController.name}:acceptRequest`, + `${approvalController.name}:rejectRequest`, + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + `SnapController:getPermitted`, + `SnapController:install`, + `SubjectMetadataController:getSubjectMetadata`, + ///: END:ONLY_INCLUDE_IF + ], + allowedEvents: [], + }), + state: initialState.PermissionController, + caveatSpecifications: getCaveatSpecifications({ + getInternalAccounts: + accountsController.listAccounts.bind(accountsController), + findNetworkClientIdByChainId: + networkController.findNetworkClientIdByChainId.bind( + networkController, + ), + }), + // @ts-expect-error Typecast permissionType from getPermissionSpecifications to be of type PermissionType.RestrictedMethod + permissionSpecifications: { + ...getPermissionSpecifications({ + getAllAccounts: () => this.keyringController.getAccounts(), + getInternalAccounts: + accountsController.listAccounts.bind(accountsController), + captureKeyringTypesWithMissingIdentities: ( + internalAccounts = [], + accounts = [], + ) => { + const accountsMissingIdentities = accounts.filter((address) => { + const lowerCaseAddress = lowerCase(address); + return !internalAccounts.some( + (account) => account.address.toLowerCase() === lowerCaseAddress, + ); + }); + const keyringTypesWithMissingIdentities = + accountsMissingIdentities.map((address) => + this.keyringController.getAccountKeyringType(address), + ); + + const internalAccountCount = internalAccounts.length; + + const accountTrackerCount = Object.keys( + accountTrackerController.state.accounts || {}, + ).length; + + captureException( + new Error( + `Attempt to get permission specifications failed because there were ${accounts.length} accounts, but ${internalAccountCount} identities, and the ${keyringTypesWithMissingIdentities} keyrings included accounts with missing identities. Meanwhile, there are ${accountTrackerCount} accounts in the account tracker.`, + ), + ); + }, + }), + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + ...getSnapPermissionSpecifications(), + ///: END:ONLY_INCLUDE_IF + }, + unrestrictedMethods, + }); + + const selectedNetworkController = new SelectedNetworkController({ + // @ts-expect-error TODO: Resolve mismatch between base-controller versions. + messenger: this.controllerMessenger.getRestricted({ + name: 'SelectedNetworkController', + allowedActions: [ + 'NetworkController:getNetworkClientById', + 'NetworkController:getState', + 'NetworkController:getSelectedNetworkClient', + 'PermissionController:hasPermissions', + 'PermissionController:getSubjectNames', + ], + allowedEvents: [ + 'NetworkController:stateChange', + 'PermissionController:stateChange', + ], + }), + state: initialState.SelectedNetworkController || { domains: {} }, + useRequestQueuePreference: !!process.env.MULTICHAIN_V1, + // TODO we need to modify core PreferencesController for better cross client support + onPreferencesStateChange: ( + listener: ({ useRequestQueue }: { useRequestQueue: boolean }) => void, + ) => listener({ useRequestQueue: !!process.env.MULTICHAIN_V1 }), + domainProxyMap: new DomainProxyMap(), + }); + + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + this.subjectMetadataController = new SubjectMetadataController({ + // @ts-expect-error TODO: Resolve mismatch between base-controller versions. + messenger: this.controllerMessenger.getRestricted({ + name: 'SubjectMetadataController', + allowedActions: [`${permissionController.name}:hasPermissions`], + allowedEvents: [], + }), + state: initialState.SubjectMetadataController || {}, + subjectCacheLimit: 100, + }); + + const setupSnapProvider = (snapId: string, connectionStream: Duplex) => { + Logger.log( + '[ENGINE LOG] Engine+setupSnapProvider: Setup stream for Snap', + snapId, + ); + // TO DO: + // Develop a simpler getRpcMethodMiddleware object for SnapBridge + // Consider developing an abstract class to derived custom implementations for each use case + const bridge = new SnapBridge({ + snapId, + connectionStream, + getRPCMethodMiddleware: ({ hostname, getProviderState }) => + getRpcMethodMiddleware({ + hostname, + getProviderState, + navigation: null, + getApprovedHosts: () => null, + setApprovedHosts: () => null, + approveHost: () => null, + title: { current: 'Snap' }, + icon: { current: undefined }, + isHomepage: () => false, + fromHomepage: { current: false }, + toggleUrlModal: () => null, + wizardScrollAdjusted: { current: false }, + tabId: false, + isWalletConnect: true, + isMMSDK: false, + url: { current: '' }, + analytics: {}, + injectHomePageScripts: () => null, + }), + }); + + bridge.setupProviderConnection(); + }; + + const requireAllowlist = process.env.METAMASK_BUILD_TYPE === 'main'; + const disableSnapInstallation = process.env.METAMASK_BUILD_TYPE === 'main'; + const allowLocalSnaps = process.env.METAMASK_BUILD_TYPE === 'flask'; + // @ts-expect-error TODO: Resolve mismatch between base-controller versions. + const snapsRegistryMessenger: SnapsRegistryMessenger = + this.controllerMessenger.getRestricted({ + name: 'SnapsRegistry', + allowedEvents: [], + allowedActions: [], + }); + const snapsRegistry = new JsonSnapsRegistry({ + state: initialState.SnapsRegistry, + messenger: snapsRegistryMessenger, + refetchOnAllowlistMiss: requireAllowlist, + url: { + registry: 'https://acl.execution.metamask.io/latest/registry.json', + signature: 'https://acl.execution.metamask.io/latest/signature.json', + }, + publicKey: + '0x025b65308f0f0fb8bc7f7ff87bfc296e0330eee5d3c1d1ee4a048b2fd6a86fa0a6', + }); + + this.snapExecutionService = new WebViewExecutionService({ + // @ts-expect-error TODO: Resolve mismatch between base-controller versions. + messenger: this.controllerMessenger.getRestricted({ + name: 'ExecutionService', + allowedActions: [], + allowedEvents: [], + }), + setupSnapProvider: setupSnapProvider.bind(this), + getWebView: () => getSnapsWebViewPromise, + }); + + const snapControllerMessenger = this.controllerMessenger.getRestricted({ + name: 'SnapController', + allowedEvents: [ + 'ExecutionService:unhandledError', + 'ExecutionService:outboundRequest', + 'ExecutionService:outboundResponse', + ], + allowedActions: [ + `${approvalController.name}:addRequest`, + `${permissionController.name}:getEndowments`, + `${permissionController.name}:getPermissions`, + `${permissionController.name}:hasPermission`, + `${permissionController.name}:hasPermissions`, + `${permissionController.name}:requestPermissions`, + `${permissionController.name}:revokeAllPermissions`, + `${permissionController.name}:revokePermissions`, + `${permissionController.name}:revokePermissionForAllSubjects`, + `${permissionController.name}:getSubjectNames`, + `${permissionController.name}:updateCaveat`, + `${approvalController.name}:addRequest`, + `${approvalController.name}:updateRequestState`, + `${permissionController.name}:grantPermissions`, + `${this.subjectMetadataController.name}:getSubjectMetadata`, + `${this.subjectMetadataController.name}:addSubjectMetadata`, + `${phishingController.name}:maybeUpdateState`, + `${phishingController.name}:testOrigin`, + `${snapsRegistry.name}:get`, + `${snapsRegistry.name}:getMetadata`, + `${snapsRegistry.name}:update`, + 'ExecutionService:executeSnap', + 'ExecutionService:terminateSnap', + 'ExecutionService:terminateAllSnaps', + 'ExecutionService:handleRpcRequest', + 'SnapsRegistry:get', + 'SnapsRegistry:getMetadata', + 'SnapsRegistry:update', + 'SnapsRegistry:resolveVersion', + ], + }); + + this.snapController = new SnapController({ + environmentEndowmentPermissions: Object.values(EndowmentPermissions), + featureFlags: { + requireAllowlist, + allowLocalSnaps, + disableSnapInstallation, + }, + state: initialState.SnapController || undefined, + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + messenger: snapControllerMessenger as any, + detectSnapLocation: ( + location: string | URL, + options?: DetectSnapLocationOptions, + ) => + detectSnapLocation(location, { + ...options, + fetch: fetchFunction, + }), + //@ts-expect-error types need to be aligned with snaps-controllers + preinstalledSnaps: PREINSTALLED_SNAPS, + //@ts-expect-error types need to be aligned between new encryptor and snaps-controllers + encryptor, + getMnemonic: getPrimaryKeyringMnemonic.bind(this), + getFeatureFlags: () => ({ + disableSnaps: + store.getState().settings.basicFunctionalityEnabled === false, + }), + }); + + const authenticationController = new AuthenticationController.Controller({ + state: initialState.AuthenticationController, + messenger: this.controllerMessenger.getRestricted({ + name: 'AuthenticationController', + allowedActions: [ + 'KeyringController:getState', + 'KeyringController:getAccounts', + + 'SnapController:handleRequest', + 'UserStorageController:enableProfileSyncing', + ], + allowedEvents: ['KeyringController:unlock', 'KeyringController:lock'], + }), + metametrics: { + agent: 'mobile', + getMetaMetricsId: async () => + (await MetaMetrics.getInstance().getMetaMetricsId()) || '', + }, + }); + + const userStorageController = new UserStorageController.Controller({ + getMetaMetricsState: () => MetaMetrics.getInstance().isEnabled(), + env: { + isAccountSyncingEnabled: Boolean(process.env.IS_TEST), + }, + config: { + accountSyncing: { + onAccountAdded: (profileId) => { + MetaMetrics.getInstance().trackEvent( + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.ACCOUNTS_SYNC_ADDED, + ) + .addProperties({ + profile_id: profileId, + }) + .build(), + ); + }, + onAccountNameUpdated: (profileId) => { + MetaMetrics.getInstance().trackEvent( + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.ACCOUNTS_SYNC_NAME_UPDATED, + ) + .addProperties({ + profile_id: profileId, + }) + .build(), + ); + }, + }, + }, + state: initialState.UserStorageController, + messenger: this.controllerMessenger.getRestricted({ + name: 'UserStorageController', + allowedActions: [ + 'SnapController:handleRequest', + 'KeyringController:getState', + 'KeyringController:addNewAccount', + 'AuthenticationController:getBearerToken', + 'AuthenticationController:getSessionProfile', + 'AuthenticationController:isSignedIn', + 'AuthenticationController:performSignOut', + 'AuthenticationController:performSignIn', + 'NotificationServicesController:disableNotificationServices', + 'NotificationServicesController:selectIsNotificationServicesEnabled', + AccountsControllerListAccountsAction, + AccountsControllerUpdateAccountMetadataAction, + ], + allowedEvents: [ + 'KeyringController:unlock', + 'KeyringController:lock', + AccountsControllerAccountAddedEvent, + AccountsControllerAccountRenamedEvent, + ], + }), + nativeScryptCrypto: scrypt, + }); + + const notificationServicesController = + new NotificationServicesController.Controller({ + messenger: this.controllerMessenger.getRestricted({ + name: 'NotificationServicesController', + allowedActions: [ + 'KeyringController:getState', + 'KeyringController:getAccounts', + 'AuthenticationController:getBearerToken', + 'AuthenticationController:isSignedIn', + 'UserStorageController:enableProfileSyncing', + 'UserStorageController:getStorageKey', + 'UserStorageController:performGetStorage', + 'UserStorageController:performSetStorage', + 'NotificationServicesPushController:enablePushNotifications', + 'NotificationServicesPushController:disablePushNotifications', + 'NotificationServicesPushController:updateTriggerPushNotifications', + ], + allowedEvents: [ + 'KeyringController:unlock', + 'KeyringController:lock', + 'KeyringController:stateChange', + ], + }), + state: initialState.NotificationServicesController, + env: { + isPushIntegrated: false, + featureAnnouncements: { + platform: 'mobile', + accessToken: process.env + .FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN as string, + spaceId: process.env.FEATURES_ANNOUNCEMENTS_SPACE_ID as string, + }, + }, + }); + + const notificationServicesPushControllerMessenger = + this.controllerMessenger.getRestricted({ + name: 'NotificationServicesPushController', + allowedActions: ['AuthenticationController:getBearerToken'], + allowedEvents: [], + }); + + const notificationServicesPushController = + new NotificationServicesPushController.Controller({ + messenger: notificationServicesPushControllerMessenger, + state: initialState.NotificationServicesPushController || { + fcmToken: '', + }, + env: { + apiKey: process.env.FIREBASE_API_KEY ?? '', + authDomain: process.env.FIREBASE_AUTH_DOMAIN ?? '', + storageBucket: process.env.FIREBASE_STORAGE_BUCKET ?? '', + projectId: process.env.FIREBASE_PROJECT_ID ?? '', + messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID ?? '', + appId: process.env.FIREBASE_APP_ID ?? '', + measurementId: process.env.FIREBASE_MEASUREMENT_ID ?? '', + vapidKey: process.env.VAPID_KEY ?? '', + }, + config: { + isPushEnabled: true, + platform: 'mobile', + // TODO: Implement optionability for push notification handlers (depending of the platform) on the NotificationServicesPushController. + onPushNotificationReceived: () => Promise.resolve(undefined), + onPushNotificationClicked: () => Promise.resolve(undefined), + }, + }); + ///: END:ONLY_INCLUDE_IF + + this.transactionController = new TransactionController({ + // @ts-expect-error at this point in time the provider will be defined by the `networkController.initializeProvider` + blockTracker: networkController.getProviderAndBlockTracker().blockTracker, + disableHistory: true, + disableSendFlowHistory: true, + disableSwaps: true, + // @ts-expect-error TransactionController is missing networkClientId argument in type + getCurrentNetworkEIP1559Compatibility: + networkController.getEIP1559Compatibility.bind(networkController), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + getExternalPendingTransactions: (address: string) => + this.smartTransactionsController.getTransactions({ + addressFrom: address, + status: SmartTransactionStatuses.PENDING, + }), + getGasFeeEstimates: + gasFeeController.fetchGasFeeEstimates.bind(gasFeeController), + // but only breaking change is Node version and bumped dependencies + getNetworkClientRegistry: + networkController.getNetworkClientRegistry.bind(networkController), + getNetworkState: () => networkController.state, + hooks: { + publish: (transactionMeta) => { + const shouldUseSmartTransaction = selectShouldUseSmartTransaction( + store.getState(), + ); + + return submitSmartTransactionHook({ + transactionMeta, + transactionController: this.transactionController, + smartTransactionsController: this.smartTransactionsController, + shouldUseSmartTransaction, + approvalController, + // @ts-expect-error TODO: Resolve mismatch between base-controller versions. + controllerMessenger: this.controllerMessenger, + featureFlags: selectSwapsChainFeatureFlags(store.getState()), + }) as Promise<{ transactionHash: string }>; + }, + }, + incomingTransactions: { + isEnabled: () => { + const currentHexChainId = networkController.getNetworkClientById( + networkController?.state.selectedNetworkClientId, + ).configuration.chainId; + + const showIncomingTransactions = + preferencesController?.state?.showIncomingTransactions; + + return Boolean( + hasProperty(showIncomingTransactions, currentChainId) && + showIncomingTransactions?.[currentHexChainId], + ); + }, + updateTransactions: true, + }, + isSimulationEnabled: () => + preferencesController.state.useTransactionSimulations, + messenger: this.controllerMessenger.getRestricted({ + name: 'TransactionController', + allowedActions: [ + AccountsControllerGetSelectedAccountAction, + `${approvalController.name}:addRequest`, + `${networkController.name}:getNetworkClientById`, + `${networkController.name}:findNetworkClientIdByChainId`, + ], + allowedEvents: [`NetworkController:stateChange`], + }), + onNetworkStateChange: (listener) => + this.controllerMessenger.subscribe( + AppConstants.NETWORK_STATE_CHANGE_EVENT, + listener, + ), + pendingTransactions: { + isResubmitEnabled: () => false, + }, + // @ts-expect-error at this point in time the provider will be defined by the `networkController.initializeProvider` + provider: networkController.getProviderAndBlockTracker().provider, + sign: this.keyringController.signTransaction.bind( + this.keyringController, + ) as unknown as TransactionControllerOptions['sign'], + state: initialState.TransactionController, + }); + + const codefiTokenApiV2 = new CodefiTokenPricesServiceV2(); + + const smartTransactionsControllerTrackMetaMetricsEvent = ( + params: { + event: MetaMetricsEventName; + category: MetaMetricsEventCategory; + properties?: ReturnType< + typeof getSmartTransactionMetricsPropertiesType + >; + sensitiveProperties?: ReturnType< + typeof getSmartTransactionMetricsSensitivePropertiesType + >; + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options?: { + metaMetricsId?: string; + }, + ) => { + MetaMetrics.getInstance().trackEvent( + MetricsEventBuilder.createEventBuilder({ + category: params.event, + }) + .addProperties(params.properties || {}) + .addSensitiveProperties(params.sensitiveProperties || {}) + .build(), + ); + }; + this.smartTransactionsController = new SmartTransactionsController({ + // @ts-expect-error TODO: resolve types + supportedChainIds: getAllowedSmartTransactionsChainIds(), + getNonceLock: this.transactionController.getNonceLock.bind( + this.transactionController, + ), + confirmExternalTransaction: + this.transactionController.confirmExternalTransaction.bind( + this.transactionController, + ), + trackMetaMetricsEvent: smartTransactionsControllerTrackMetaMetricsEvent, + state: initialState.SmartTransactionsController, + // @ts-expect-error TODO: Resolve mismatch between base-controller versions. + messenger: this.controllerMessenger.getRestricted({ + name: 'SmartTransactionsController', + allowedActions: ['NetworkController:getNetworkClientById'], + allowedEvents: ['NetworkController:stateChange'], + }), + // @ts-expect-error TODO: Resolve mismatch between smart-transactions-controller and transaction-controller + getTransactions: this.transactionController.getTransactions.bind( + this.transactionController, + ), + getMetaMetricsProps: () => Promise.resolve({}), // Return MetaMetrics props once we enable HW wallets for smart transactions. + }); + + const controllers: Controllers[keyof Controllers][] = [ + this.keyringController, + accountTrackerController, + new AddressBookController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'AddressBookController', + allowedActions: [], + allowedEvents: [], + }), + state: initialState.AddressBookController, + }), + assetsContractController, + nftController, + tokensController, + tokenListController, + new TokenDetectionController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'TokenDetectionController', + allowedActions: [ + AccountsControllerGetSelectedAccountAction, + 'NetworkController:getNetworkClientById', + 'NetworkController:getNetworkConfigurationByNetworkClientId', + 'NetworkController:getState', + 'KeyringController:getState', + 'PreferencesController:getState', + 'TokenListController:getState', + 'TokensController:getState', + 'TokensController:addDetectedTokens', + AccountsControllerGetAccountAction, + ], + allowedEvents: [ + 'KeyringController:lock', + 'KeyringController:unlock', + 'PreferencesController:stateChange', + 'NetworkController:networkDidChange', + 'TokenListController:stateChange', + 'TokensController:stateChange', + AccountsControllerSelectedEvmAccountChangeEvent, + ], + }), + trackMetaMetricsEvent: () => + MetaMetrics.getInstance().trackEvent( + MetaMetricsEvents.TOKEN_DETECTED, + { + token_standard: 'ERC20', + asset_type: 'token', + chain_id: getDecimalChainId( + networkController.getNetworkClientById( + networkController?.state.selectedNetworkClientId, + ).configuration.chainId, + ), + }, + ), + getBalancesInSingleCall: + assetsContractController.getBalancesInSingleCall.bind( + assetsContractController, + ), + }), + + new NftDetectionController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'NftDetectionController', + allowedEvents: [ + 'NetworkController:stateChange', + 'PreferencesController:stateChange', + ], + allowedActions: [ + 'ApprovalController:addRequest', + 'NetworkController:getState', + 'NetworkController:getNetworkClientById', + 'PreferencesController:getState', + AccountsControllerGetSelectedAccountAction, + ], + }), + disabled: false, + addNft: nftController.addNft.bind(nftController), + getNftState: () => nftController.state, + }), + currencyRateController, + networkController, + phishingController, + preferencesController, + new TokenBalancesController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'TokenBalancesController', + allowedActions: [ + AccountsControllerGetSelectedAccountAction, + 'AssetsContractController:getERC20BalanceOf', + ], + allowedEvents: ['TokensController:stateChange'], + }), + interval: 180000, + tokens: [ + ...tokensController.state.tokens, + ...tokensController.state.detectedTokens, + ], + state: initialState.TokenBalancesController, + }), + new TokenRatesController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'TokenRatesController', + allowedActions: [ + 'TokensController:getState', + 'NetworkController:getNetworkClientById', + 'NetworkController:getState', + AccountsControllerGetAccountAction, + AccountsControllerGetSelectedAccountAction, + ], + allowedEvents: [ + 'TokensController:stateChange', + 'NetworkController:stateChange', + AccountsControllerSelectedEvmAccountChangeEvent, + ], + }), + tokenPricesService: codefiTokenApiV2, + interval: 30 * 60 * 1000, + state: initialState.TokenRatesController || { marketData: {} }, + }), + this.transactionController, + this.smartTransactionsController, + new SwapsController( + { + fetchGasFeeEstimates: () => gasFeeController.fetchGasFeeEstimates(), + // @ts-expect-error TODO: Resolve mismatch between gas fee and swaps controller types + fetchEstimatedMultiLayerL1Fee, + }, + { + clientId: AppConstants.SWAPS.CLIENT_ID, + fetchAggregatorMetadataThreshold: + AppConstants.SWAPS.CACHE_AGGREGATOR_METADATA_THRESHOLD, + fetchTokensThreshold: AppConstants.SWAPS.CACHE_TOKENS_THRESHOLD, + fetchTopAssetsThreshold: + AppConstants.SWAPS.CACHE_TOP_ASSETS_THRESHOLD, + supportedChainIds: [ + swapsUtils.ETH_CHAIN_ID, + swapsUtils.BSC_CHAIN_ID, + swapsUtils.SWAPS_TESTNET_CHAIN_ID, + swapsUtils.POLYGON_CHAIN_ID, + swapsUtils.AVALANCHE_CHAIN_ID, + swapsUtils.ARBITRUM_CHAIN_ID, + swapsUtils.OPTIMISM_CHAIN_ID, + swapsUtils.ZKSYNC_ERA_CHAIN_ID, + swapsUtils.LINEA_CHAIN_ID, + swapsUtils.BASE_CHAIN_ID, + ], + }, + ), + gasFeeController, + approvalController, + permissionController, + selectedNetworkController, + new SignatureController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'SignatureController', + allowedActions: [ + `${approvalController.name}:addRequest`, + `${this.keyringController.name}:signPersonalMessage`, + `${this.keyringController.name}:signMessage`, + `${this.keyringController.name}:signTypedMessage`, + `${loggingController.name}:add`, + `${networkController.name}:getNetworkClientById`, + ], + allowedEvents: [], + }), + // This casting expected due to mismatch of browser and react-native version of Sentry traceContext + trace: trace as unknown as SignatureControllerOptions['trace'], + }), + loggingController, + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + this.snapController, + this.subjectMetadataController, + authenticationController, + userStorageController, + notificationServicesController, + notificationServicesPushController, + ///: END:ONLY_INCLUDE_IF + accountsController, + new PPOMController({ + chainId: networkController.getNetworkClientById( + networkController?.state.selectedNetworkClientId, + ).configuration.chainId, + blockaidPublicKey: process.env.BLOCKAID_PUBLIC_KEY as string, + cdnBaseUrl: process.env.BLOCKAID_FILE_CDN as string, + messenger: this.controllerMessenger.getRestricted({ + name: 'PPOMController', + allowedActions: ['NetworkController:getNetworkClientById'], + allowedEvents: [`${networkController.name}:networkDidChange`], + }), + onPreferencesChange: (listener) => + this.controllerMessenger.subscribe( + `${preferencesController.name}:stateChange`, + listener, + ), + // TODO: Replace "any" with type + provider: + // eslint-disable-next-line @typescript-eslint/no-explicit-any + networkController.getProviderAndBlockTracker().provider as any, + ppomProvider: { + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + PPOM: PPOM as any, + ppomInit, + }, + storageBackend: new RNFSStorageBackend('PPOMDB'), + securityAlertsEnabled: + initialState.PreferencesController?.securityAlertsEnabled ?? false, + state: initialState.PPOMController, + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + nativeCrypto: Crypto as any, + }), + ]; + + // set initial state + // TODO: Pass initial state into each controller constructor instead + // This is being set post-construction for now to ensure it's functionally equivalent with + // how the `ComponsedController` used to set initial state. + // + // The check for `controller.subscribe !== undefined` is to filter out BaseControllerV2 + // controllers. They should be initialized via the constructor instead. + for (const controller of controllers) { + if ( + hasProperty(initialState, controller.name) && + // Use `in` operator here because the `subscribe` function is one level up the prototype chain + 'subscribe' in controller && + controller.subscribe !== undefined + ) { + // The following type error can be addressed by passing initial state into controller constructors instead + // @ts-expect-error No type-level guarantee that the correct state is being applied to the correct controller here. + controller.update(initialState[controller.name]); + } + } + + this.datamodel = new ComposableController( + // @ts-expect-error The ComposableController needs to be updated to support BaseControllerV2 + controllers, + this.controllerMessenger, + ); + this.context = controllers.reduce>( + (context, controller) => ({ + ...context, + [controller.name]: controller, + }), + {}, + ) as typeof this.context; + + const { NftController: nfts } = this.context; + + if (process.env.MM_OPENSEA_KEY) { + nfts.setApiKey(process.env.MM_OPENSEA_KEY); + } + + this.controllerMessenger.subscribe( + 'TransactionController:incomingTransactionBlockReceived', + (blockNumber: number) => { + NotificationManager.gotIncomingTransaction(blockNumber); + }, + ); + + this.controllerMessenger.subscribe( + AppConstants.NETWORK_STATE_CHANGE_EVENT, + (state: NetworkState) => { + if ( + state.networksMetadata[state.selectedNetworkClientId].status === + NetworkStatus.Available && + networkController.getNetworkClientById( + networkController?.state.selectedNetworkClientId, + ).configuration.chainId !== currentChainId + ) { + // We should add a state or event emitter saying the provider changed + setTimeout(() => { + this.configureControllersOnNetworkChange(); + currentChainId = networkController.getNetworkClientById( + networkController?.state.selectedNetworkClientId, + ).configuration.chainId; + }, 500); + } + }, + ); + + this.controllerMessenger.subscribe( + AppConstants.NETWORK_STATE_CHANGE_EVENT, + async () => { + try { + const networkId = await deprecatedGetNetworkId(); + store.dispatch(networkIdUpdated(networkId)); + } catch (error) { + console.error( + error, + `Network ID not changed, current chainId: ${ + networkController.getNetworkClientById( + networkController?.state.selectedNetworkClientId, + ).configuration.chainId + }`, + ); + } + }, + ); + + this.controllerMessenger.subscribe( + `${networkController.name}:networkWillChange`, + () => { + store.dispatch(networkIdWillUpdate()); + }, + ); + + this.configureControllersOnNetworkChange(); + this.startPolling(); + this.handleVaultBackup(); + this._addTransactionControllerListeners(); + + Engine.instance = this; + } + + // Logs the "Transaction Finalized" event after a transaction was either confirmed, dropped or failed. + _handleTransactionFinalizedEvent = async ( + transactionEventPayload: TransactionEventPayload, + properties: JsonMap, + ) => { + const shouldUseSmartTransaction = selectShouldUseSmartTransaction( + store.getState(), + ); + if ( + !shouldUseSmartTransaction || + !transactionEventPayload.transactionMeta + ) { + MetaMetrics.getInstance().trackEvent( + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.TRANSACTION_FINALIZED, + ) + .addProperties(properties) + .build(), + ); + return; + } + const { transactionMeta } = transactionEventPayload; + const { SmartTransactionsController } = this.context; + const waitForSmartTransaction = true; + const smartTransactionMetricsProperties = + await getSmartTransactionMetricsProperties( + SmartTransactionsController, + transactionMeta, + waitForSmartTransaction, + this.controllerMessenger, + ); + MetaMetrics.getInstance().trackEvent( + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.TRANSACTION_FINALIZED, + ) + .addProperties(smartTransactionMetricsProperties) + .addProperties(properties) + .build(), + ); + }; + + _handleTransactionDropped = async ( + transactionEventPayload: TransactionEventPayload, + ) => { + const properties = { status: 'dropped' }; + await this._handleTransactionFinalizedEvent( + transactionEventPayload, + properties, + ); + }; + + _handleTransactionConfirmed = async (transactionMeta: TransactionMeta) => { + const properties = { status: 'confirmed' }; + await this._handleTransactionFinalizedEvent( + { transactionMeta }, + properties, + ); + }; + + _handleTransactionFailed = async ( + transactionEventPayload: TransactionEventPayload, + ) => { + const properties = { status: 'failed' }; + await this._handleTransactionFinalizedEvent( + transactionEventPayload, + properties, + ); + }; + + _addTransactionControllerListeners() { + this.controllerMessenger.subscribe( + 'TransactionController:transactionDropped', + this._handleTransactionDropped, + ); + + this.controllerMessenger.subscribe( + 'TransactionController:transactionConfirmed', + this._handleTransactionConfirmed, + ); + + this.controllerMessenger.subscribe( + 'TransactionController:transactionFailed', + this._handleTransactionFailed, + ); + } + + handleVaultBackup() { + this.controllerMessenger.subscribe( + AppConstants.KEYRING_STATE_CHANGE_EVENT, + (state: KeyringControllerState) => + backupVault(state) + .then((result) => { + if (result.success) { + Logger.log('Engine', 'Vault back up successful'); + } else { + Logger.log('Engine', 'Vault backup failed', result.error); + } + }) + .catch((error) => { + Logger.error(error, 'Engine Vault backup failed'); + }), + ); + } + + startPolling() { + const { + TokenDetectionController, + TokenListController, + TransactionController, + TokenRatesController, + } = this.context; + + TokenListController.start(); + TokenDetectionController.start(); + // leaving the reference of TransactionController here, rather than importing it from utils to avoid circular dependency + TransactionController.startIncomingTransactionPolling(); + TokenRatesController.start(); + } + + configureControllersOnNetworkChange() { + const { AccountTrackerController, NetworkController, SwapsController } = + this.context; + const { provider } = NetworkController.getProviderAndBlockTracker(); + + // Skip configuration if this is called before the provider is initialized + if (!provider) { + return; + } + provider.sendAsync = provider.sendAsync.bind(provider); + + SwapsController.configure({ + provider, + chainId: NetworkController.getNetworkClientById( + NetworkController?.state.selectedNetworkClientId, + ).configuration.chainId, + pollCountLimit: AppConstants.SWAPS.POLL_COUNT_LIMIT, + }); + AccountTrackerController.refresh(); + } + + getTotalFiatAccountBalance = (): { + ethFiat: number; + tokenFiat: number; + tokenFiat1dAgo: number; + ethFiat1dAgo: number; + } => { + const { + CurrencyRateController, + AccountsController, + AccountTrackerController, + TokenBalancesController, + TokenRatesController, + TokensController, + NetworkController, + } = this.context; + + const selectedInternalAccount = AccountsController.getAccount( + AccountsController.state.internalAccounts.selectedAccount, + ); + + if (selectedInternalAccount) { + const selectSelectedInternalAccountChecksummedAddress = + toChecksumHexAddress(selectedInternalAccount.address); + const { currentCurrency } = CurrencyRateController.state; + const { chainId, ticker } = NetworkController.getNetworkClientById( + NetworkController?.state.selectedNetworkClientId, + ).configuration; + const { settings: { showFiatOnTestnets } = {} } = store.getState(); + + if (isTestNet(chainId) && !showFiatOnTestnets) { + return { ethFiat: 0, tokenFiat: 0, ethFiat1dAgo: 0, tokenFiat1dAgo: 0 }; + } + + const conversionRate = + CurrencyRateController.state?.currencyRates?.[ticker]?.conversionRate ?? + 0; + + const { accountsByChainId } = AccountTrackerController.state; + const { tokens } = TokensController.state; + const { marketData } = TokenRatesController.state; + const tokenExchangeRates = marketData?.[toHexadecimal(chainId)]; + + let ethFiat = 0; + let ethFiat1dAgo = 0; + let tokenFiat = 0; + let tokenFiat1dAgo = 0; + const decimalsToShow = (currentCurrency === 'usd' && 2) || undefined; + if ( + accountsByChainId?.[toHexadecimal(chainId)]?.[ + selectSelectedInternalAccountChecksummedAddress + ] + ) { + const balanceBN = hexToBN( + accountsByChainId[toHexadecimal(chainId)][ + selectSelectedInternalAccountChecksummedAddress + ].balance, + ); + const stakedBalanceBN = hexToBN( + accountsByChainId[toHexadecimal(chainId)][ + selectSelectedInternalAccountChecksummedAddress + ].stakedBalance || '0x00', + ); + const totalAccountBalance = balanceBN + .add(stakedBalanceBN) + .toString('hex'); + ethFiat = weiToFiatNumber( + totalAccountBalance, + conversionRate, + decimalsToShow, + ); + } + + const ethPricePercentChange1d = + tokenExchangeRates?.[zeroAddress() as Hex]?.pricePercentChange1d; + + ethFiat1dAgo = + ethPricePercentChange1d !== undefined + ? ethFiat / (1 + ethPricePercentChange1d / 100) + : ethFiat; + + if (tokens.length > 0) { + const { contractBalances: tokenBalances } = + TokenBalancesController.state; + tokens.forEach( + (item: { address: string; balance?: string; decimals: number }) => { + const exchangeRate = + tokenExchangeRates?.[item.address as Hex]?.price; + + const tokenBalance = + item.balance || + (item.address in tokenBalances + ? renderFromTokenMinimalUnit( + tokenBalances[item.address], + item.decimals, + ) + : undefined); + const tokenBalanceFiat = balanceToFiatNumber( + // TODO: Fix this by handling or eliminating the undefined case + // @ts-expect-error This variable can be `undefined`, which would break here. + tokenBalance, + conversionRate, + exchangeRate, + decimalsToShow, + ); + + const tokenPricePercentChange1d = + tokenExchangeRates?.[item.address as Hex]?.pricePercentChange1d; + + const tokenBalance1dAgo = + tokenPricePercentChange1d !== undefined + ? tokenBalanceFiat / (1 + tokenPricePercentChange1d / 100) + : tokenBalanceFiat; + + tokenFiat += tokenBalanceFiat; + tokenFiat1dAgo += tokenBalance1dAgo; + }, + ); + } + + return { + ethFiat: ethFiat ?? 0, + ethFiat1dAgo: ethFiat1dAgo ?? 0, + tokenFiat: tokenFiat ?? 0, + tokenFiat1dAgo: tokenFiat1dAgo ?? 0, + }; + } + // if selectedInternalAccount is undefined, return default 0 value. + return { + ethFiat: 0, + tokenFiat: 0, + ethFiat1dAgo: 0, + tokenFiat1dAgo: 0, + }; + }; + + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + getSnapKeyring = async () => { + let [snapKeyring] = this.keyringController.getKeyringsByType( + KeyringTypes.snap, + ); + if (!snapKeyring) { + snapKeyring = await this.keyringController.addNewKeyring( + KeyringTypes.snap, + ); + } + return snapKeyring; + }; + + /** + * Removes an account from state / storage. + * + * @param {string} address - A hex address + */ + removeAccount = async (address: string) => { + // Remove all associated permissions + await removeAccountsFromPermissions([address]); + // Remove account from the keyring + await this.keyringController.removeAccount(address as Hex); + return address; + }; + ///: END:ONLY_INCLUDE_IF + + /** + * Returns true or false whether the user has funds or not + */ + hasFunds = () => { + try { + const { + engine: { backgroundState }, + } = store.getState(); + // TODO: Check `allNfts[currentChainId]` property instead + // @ts-expect-error This property does not exist + const nfts = backgroundState.NftController.nfts; + const tokens = backgroundState.TokensController.tokens; + const tokenBalances = + backgroundState.TokenBalancesController.contractBalances; + + let tokenFound = false; + tokens.forEach((token: { address: string | number }) => { + if ( + tokenBalances[token.address] && + !isZero(tokenBalances[token.address]) + ) { + tokenFound = true; + } + }); + + const fiatBalance = this.getTotalFiatAccountBalance() || 0; + const totalFiatBalance = fiatBalance.ethFiat + fiatBalance.ethFiat; + + return totalFiatBalance > 0 || tokenFound || nfts.length > 0; + } catch (e) { + Logger.log('Error while getting user funds', e); + } + }; + + resetState = async () => { + // Whenever we are gonna start a new wallet + // either imported or created, we need to + // get rid of the old data from state + const { + TransactionController, + TokensController, + NftController, + TokenBalancesController, + TokenRatesController, + PermissionController, + // SelectedNetworkController, + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + SnapController, + ///: END:ONLY_INCLUDE_IF + LoggingController, + } = this.context; + + // Remove all permissions. + PermissionController?.clearState?.(); + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + SnapController.clearState(); + ///: END:ONLY_INCLUDE_IF + + // Clear selected network + // TODO implement this method on SelectedNetworkController + // SelectedNetworkController.unsetAllDomains() + + //Clear assets info + TokensController.reset(); + NftController.reset(); + + TokenBalancesController.reset(); + TokenRatesController.reset(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (TransactionController as any).update(() => ({ + methodData: {}, + transactions: [], + lastFetchedBlockNumbers: {}, + submitHistory: [], + swapsTransactions: {}, + })); + + LoggingController.clear(); + }; + + removeAllListeners() { + this.controllerMessenger.clearSubscriptions(); + } + + async destroyEngineInstance() { + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Object.values(this.context).forEach((controller: any) => { + if (controller.destroy) { + controller.destroy(); + } + }); + this.removeAllListeners(); + await this.resetState(); + Engine.instance = null; + } + + rejectPendingApproval( + id: string, + reason: Error = providerErrors.userRejectedRequest(), + opts: { ignoreMissing?: boolean; logErrors?: boolean } = {}, + ) { + const { ApprovalController } = this.context; + + if (opts.ignoreMissing && !ApprovalController.has({ id })) { + return; + } + + try { + ApprovalController.reject(id, reason); + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + if (opts.logErrors !== false) { + Logger.error( + error, + 'Reject while rejecting pending connection request', + ); + } + } + } + + async acceptPendingApproval( + id: string, + requestData?: Record, + opts: AcceptOptions & { handleErrors?: boolean } = { + waitForResult: false, + deleteAfterResult: false, + handleErrors: true, + }, + ) { + const { ApprovalController } = this.context; + + try { + return await ApprovalController.accept(id, requestData, { + waitForResult: opts.waitForResult, + deleteAfterResult: opts.deleteAfterResult, + }); + } catch (err) { + if (opts.handleErrors === false) { + throw err; + } + } + } + + // This should be used instead of directly calling PreferencesController.setSelectedAddress or AccountsController.setSelectedAccount + setSelectedAccount(address: string) { + const { AccountsController, PreferencesController } = this.context; + const account = AccountsController.getAccountByAddress(address); + if (account) { + AccountsController.setSelectedAccount(account.id); + PreferencesController.setSelectedAddress(address); + } else { + throw new Error(`No account found for address: ${address}`); + } + } + + /** + * This should be used instead of directly calling PreferencesController.setAccountLabel or AccountsController.setAccountName in order to keep the names in sync + * We are currently incrementally migrating the accounts data to the AccountsController so we must keep these values + * in sync until the migration is complete. + */ + setAccountLabel(address: string, label: string) { + const { AccountsController, PreferencesController } = this.context; + const accountToBeNamed = AccountsController.getAccountByAddress(address); + if (accountToBeNamed === undefined) { + throw new Error(`No account found for address: ${address}`); + } + AccountsController.setAccountName(accountToBeNamed.id, label); + PreferencesController.setAccountLabel(address, label); + } + + getGlobalEthQuery(): EthQuery { + const { NetworkController } = this.context; + const { provider } = NetworkController.getSelectedNetworkClient() ?? {}; + + if (!provider) { + throw new Error('No selected network client'); + } + + return new EthQuery(provider); + } +} + +/** + * Assert that the given Engine instance has been initialized + * + * @param instance - Either an Engine instance, or null + */ +function assertEngineExists( + instance: Engine | null, +): asserts instance is Engine { + if (!instance) { + throw new Error('Engine does not exist'); + } +} + +let instance: Engine | null; + +export default { + get context() { + assertEngineExists(instance); + return instance.context; + }, + + get controllerMessenger() { + assertEngineExists(instance); + return instance.controllerMessenger; + }, + + get state() { + assertEngineExists(instance); + const { + AccountTrackerController, + AddressBookController, + AssetsContractController, + NftController, + TokenListController, + CurrencyRateController, + KeyringController, + NetworkController, + PreferencesController, + PhishingController, + PPOMController, + TokenBalancesController, + TokenRatesController, + TransactionController, + SmartTransactionsController, + SwapsController, + GasFeeController, + TokensController, + TokenDetectionController, + NftDetectionController, + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + SnapController, + SubjectMetadataController, + AuthenticationController, + UserStorageController, + NotificationServicesController, + NotificationServicesPushController, + ///: END:ONLY_INCLUDE_IF + PermissionController, + SelectedNetworkController, + ApprovalController, + LoggingController, + AccountsController, + } = instance.datamodel.state; + + // normalize `null` currencyRate to `0` + // TODO: handle `null` currencyRate by hiding fiat values instead + const modifiedCurrencyRateControllerState = { + ...CurrencyRateController, + conversionRate: + CurrencyRateController.conversionRate === null + ? 0 + : CurrencyRateController.conversionRate, + }; + + return { + AccountTrackerController, + AddressBookController, + AssetsContractController, + NftController, + TokenListController, + CurrencyRateController: modifiedCurrencyRateControllerState, + KeyringController, + NetworkController, + PhishingController, + PPOMController, + PreferencesController, + TokenBalancesController, + TokenRatesController, + TokensController, + TransactionController, + SmartTransactionsController, + SwapsController, + GasFeeController, + TokenDetectionController, + NftDetectionController, + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + SnapController, + SubjectMetadataController, + AuthenticationController, + UserStorageController, + NotificationServicesController, + NotificationServicesPushController, + ///: END:ONLY_INCLUDE_IF + PermissionController, + SelectedNetworkController, + ApprovalController, + LoggingController, + AccountsController, + }; + }, + + get datamodel() { + assertEngineExists(instance); + return instance.datamodel; + }, + + getTotalFiatAccountBalance() { + assertEngineExists(instance); + return instance.getTotalFiatAccountBalance(); + }, + + hasFunds() { + assertEngineExists(instance); + return instance.hasFunds(); + }, + + resetState() { + assertEngineExists(instance); + return instance.resetState(); + }, + + destroyEngine() { + instance?.destroyEngineInstance(); + instance = null; + }, + + init(state: Partial | undefined, keyringState = null) { + instance = Engine.instance || new Engine(state, keyringState); + Object.freeze(instance); + return instance; + }, + + acceptPendingApproval: async ( + id: string, + requestData?: Record, + opts?: AcceptOptions & { handleErrors?: boolean }, + ) => instance?.acceptPendingApproval(id, requestData, opts), + + rejectPendingApproval: ( + id: string, + reason: Error, + opts: { + ignoreMissing?: boolean; + logErrors?: boolean; + } = {}, + ) => instance?.rejectPendingApproval(id, reason, opts), + + setSelectedAddress: (address: string) => { + assertEngineExists(instance); + instance.setSelectedAccount(address); + }, + + setAccountLabel: (address: string, label: string) => { + assertEngineExists(instance); + instance.setAccountLabel(address, label); + }, + + getGlobalEthQuery: (): EthQuery => { + assertEngineExists(instance); + return instance.getGlobalEthQuery(); + }, + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + getSnapKeyring: () => { + assertEngineExists(instance); + return instance.getSnapKeyring(); + }, + removeAccount: async (address: string) => { + assertEngineExists(instance); + return await instance.removeAccount(address); + }, + ///: END:ONLY_INCLUDE_IF +}; diff --git a/app/core/Engine/Engine.types.ts b/app/core/Engine/Engine.types.ts new file mode 100644 index 00000000000..80d1f73a506 --- /dev/null +++ b/app/core/Engine/Engine.types.ts @@ -0,0 +1,321 @@ +import { ExtendedControllerMessenger } from '../ExtendedControllerMessenger'; +import { + AccountTrackerController, + AccountTrackerControllerState, + CurrencyRateController, + CurrencyRateStateChange, + GetCurrencyRateState, + CurrencyRateState, + NftController, + NftControllerState, + NftDetectionController, + TokenListController, + TokenListControllerActions, + TokenListControllerEvents, + GetTokenListState, + TokenListStateChange, + TokenListState, + TokensController, + TokensControllerActions, + TokensControllerEvents, + TokensControllerState, + TokenBalancesController, + TokenBalancesControllerState, + TokenDetectionController, + TokenRatesController, + TokenRatesControllerState, + AssetsContractController, + AssetsContractControllerGetERC20BalanceOfAction, + AssetsContractControllerGetERC721AssetNameAction, + AssetsContractControllerGetERC721AssetSymbolAction, + AssetsContractControllerGetERC721TokenURIAction, + AssetsContractControllerGetERC721OwnerOfAction, + AssetsContractControllerGetERC1155BalanceOfAction, + AssetsContractControllerGetERC1155TokenURIAction, +} from '@metamask/assets-controllers'; +import { + AddressBookController, + AddressBookControllerActions, + AddressBookControllerEvents, + AddressBookControllerState, +} from '@metamask/address-book-controller'; +import { + KeyringController, + KeyringControllerActions, + KeyringControllerEvents, + KeyringControllerState, +} from '@metamask/keyring-controller'; +import { + NetworkController, + NetworkControllerActions, + NetworkControllerEvents, + NetworkState, +} from '@metamask/network-controller'; +import { + PhishingController, + PhishingControllerActions, + PhishingControllerEvents, + PhishingControllerState, +} from '@metamask/phishing-controller'; +import { + PreferencesController, + PreferencesControllerActions, + PreferencesControllerEvents, + PreferencesState, +} from '@metamask/preferences-controller'; +import { + TransactionController, + TransactionControllerEvents, + TransactionControllerState, +} from '@metamask/transaction-controller'; +import { + GasFeeController, + GasFeeStateChange, + GetGasFeeState, + GasFeeState, +} from '@metamask/gas-fee-controller'; +import { + ApprovalController, + ApprovalControllerActions, + ApprovalControllerEvents, + ApprovalControllerState, +} from '@metamask/approval-controller'; +import { + SelectedNetworkController, + SelectedNetworkControllerEvents, + SelectedNetworkControllerActions, + SelectedNetworkControllerState, +} from '@metamask/selected-network-controller'; +import { + PermissionController, + PermissionControllerActions, + PermissionControllerEvents, + PermissionControllerState, + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + SubjectMetadataController, + SubjectMetadataControllerActions, + SubjectMetadataControllerEvents, + SubjectMetadataControllerState, + ///: END:ONLY_INCLUDE_IF +} from '@metamask/permission-controller'; +import SwapsController from '@metamask/swaps-controller'; +import { SwapsState } from '@metamask/swaps-controller/dist/SwapsController'; +import { + PPOMController, + PPOMControllerActions, + PPOMControllerEvents, + PPOMState, +} from '@metamask/ppom-validator'; +///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) +import { + SnapController, + AllowedActions as SnapsAllowedActions, + AllowedEvents as SnapsAllowedEvents, + SnapControllerEvents, + SnapControllerActions, + SnapsRegistryState, + PersistedSnapControllerState, +} from '@metamask/snaps-controllers'; +///: END:ONLY_INCLUDE_IF +import { + LoggingController, + LoggingControllerActions, + LoggingControllerEvents, + LoggingControllerState, +} from '@metamask/logging-controller'; +import { + SignatureController, + SignatureControllerActions, + SignatureControllerEvents, +} from '@metamask/signature-controller'; +import SmartTransactionsController, { + type SmartTransactionsControllerActions, + type SmartTransactionsControllerEvents, + SmartTransactionsControllerState, +} from '@metamask/smart-transactions-controller'; +///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) +import { + AuthenticationController, + UserStorageController, +} from '@metamask/profile-sync-controller'; +import { + NotificationServicesPushController, + NotificationServicesController, +} from '@metamask/notification-services-controller'; +///: END:ONLY_INCLUDE_IF +import { + AccountsController, + AccountsControllerActions, + AccountsControllerEvents, + AccountsControllerState, +} from '@metamask/accounts-controller'; +import { BaseState } from '@metamask/base-controller'; +import { getPermissionSpecifications } from '../Permissions/specifications.js'; + +type PermissionsByRpcMethod = ReturnType; +type Permissions = PermissionsByRpcMethod[keyof PermissionsByRpcMethod]; + +///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) +type AuthenticationControllerActions = AuthenticationController.AllowedActions; +type UserStorageControllerActions = UserStorageController.AllowedActions; +type NotificationsServicesControllerActions = + NotificationServicesController.AllowedActions; + +type SnapsGlobalActions = + | SnapControllerActions + | SubjectMetadataControllerActions + | PhishingControllerActions + | SnapsAllowedActions; + +type SnapsGlobalEvents = + | SnapControllerEvents + | SubjectMetadataControllerEvents + | PhishingControllerEvents + | SnapsAllowedEvents; +///: END:ONLY_INCLUDE_IF + +type GlobalActions = + | AddressBookControllerActions + | ApprovalControllerActions + | GetCurrencyRateState + | GetGasFeeState + | GetTokenListState + | KeyringControllerActions + | NetworkControllerActions + | PermissionControllerActions + | SignatureControllerActions + | LoggingControllerActions + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + | SnapsGlobalActions + | AuthenticationControllerActions + | UserStorageControllerActions + | NotificationsServicesControllerActions + ///: END:ONLY_INCLUDE_IF + | KeyringControllerActions + | AccountsControllerActions + | PreferencesControllerActions + | PPOMControllerActions + | TokensControllerActions + | TokenListControllerActions + | SelectedNetworkControllerActions + | SmartTransactionsControllerActions + | AssetsContractControllerGetERC20BalanceOfAction + | AssetsContractControllerGetERC721AssetNameAction + | AssetsContractControllerGetERC721AssetSymbolAction + | AssetsContractControllerGetERC721TokenURIAction + | AssetsContractControllerGetERC721OwnerOfAction + | AssetsContractControllerGetERC1155BalanceOfAction + | AssetsContractControllerGetERC1155TokenURIAction; + +type GlobalEvents = + | AddressBookControllerEvents + | ApprovalControllerEvents + | CurrencyRateStateChange + | GasFeeStateChange + | KeyringControllerEvents + | TokenListStateChange + | NetworkControllerEvents + | PermissionControllerEvents + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + | SnapsGlobalEvents + ///: END:ONLY_INCLUDE_IF + | SignatureControllerEvents + | LoggingControllerEvents + | KeyringControllerEvents + | PPOMControllerEvents + | AccountsControllerEvents + | PreferencesControllerEvents + | TokensControllerEvents + | TokenListControllerEvents + | TransactionControllerEvents + | SelectedNetworkControllerEvents + | SmartTransactionsControllerEvents; + +/** + * Type definition for the controller messenger used in the Engine. + * It extends the base ControllerMessenger with global actions and events. + */ +export type ControllerMessenger = ExtendedControllerMessenger< + GlobalActions, + GlobalEvents +>; + +/** + * All mobile controllers, keyed by name + */ +export interface Controllers { + AccountsController: AccountsController; + AccountTrackerController: AccountTrackerController; + AddressBookController: AddressBookController; + ApprovalController: ApprovalController; + AssetsContractController: AssetsContractController; + CurrencyRateController: CurrencyRateController; + GasFeeController: GasFeeController; + KeyringController: KeyringController; + LoggingController: LoggingController; + NetworkController: NetworkController; + NftController: NftController; + NftDetectionController: NftDetectionController; + // TODO: Fix permission types + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + PermissionController: PermissionController; + SelectedNetworkController: SelectedNetworkController; + PhishingController: PhishingController; + PreferencesController: PreferencesController; + PPOMController: PPOMController; + TokenBalancesController: TokenBalancesController; + TokenListController: TokenListController; + TokenDetectionController: TokenDetectionController; + TokenRatesController: TokenRatesController; + TokensController: TokensController; + TransactionController: TransactionController; + SmartTransactionsController: SmartTransactionsController; + SignatureController: SignatureController; + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + SnapController: SnapController; + SubjectMetadataController: SubjectMetadataController; + AuthenticationController: AuthenticationController.Controller; + UserStorageController: UserStorageController.Controller; + NotificationServicesController: NotificationServicesController.Controller; + NotificationServicesPushController: NotificationServicesPushController.Controller; + ///: END:ONLY_INCLUDE_IF + SwapsController: SwapsController; +} + +export interface EngineState { + AccountTrackerController: AccountTrackerControllerState; + AddressBookController: AddressBookControllerState; + AssetsContractController: BaseState; + NftController: NftControllerState; + TokenListController: TokenListState; + CurrencyRateController: CurrencyRateState; + KeyringController: KeyringControllerState; + NetworkController: NetworkState; + PreferencesController: PreferencesState; + PhishingController: PhishingControllerState; + TokenBalancesController: TokenBalancesControllerState; + TokenRatesController: TokenRatesControllerState; + TransactionController: TransactionControllerState; + SmartTransactionsController: SmartTransactionsControllerState; + SwapsController: SwapsState; + GasFeeController: GasFeeState; + TokensController: TokensControllerState; + TokenDetectionController: BaseState; + NftDetectionController: BaseState; + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + SnapController: PersistedSnapControllerState; + SnapsRegistry: SnapsRegistryState; + SubjectMetadataController: SubjectMetadataControllerState; + AuthenticationController: AuthenticationController.AuthenticationControllerState; + UserStorageController: UserStorageController.UserStorageControllerState; + NotificationServicesController: NotificationServicesController.NotificationServicesControllerState; + NotificationServicesPushController: NotificationServicesPushController.NotificationServicesPushControllerState; + ///: END:ONLY_INCLUDE_IF + PermissionController: PermissionControllerState; + ApprovalController: ApprovalControllerState; + LoggingController: LoggingControllerState; + PPOMController: PPOMState; + AccountsController: AccountsControllerState; + SelectedNetworkController: SelectedNetworkControllerState; +} diff --git a/app/core/Engine/index.ts b/app/core/Engine/index.ts index 12a5ed95526..a36cbc7ae71 100644 --- a/app/core/Engine/index.ts +++ b/app/core/Engine/index.ts @@ -1,2331 +1,2 @@ -/* eslint-disable @typescript-eslint/no-shadow */ -import Crypto from 'react-native-quick-crypto'; -import { scrypt } from 'react-native-fast-crypto'; -import { - AccountTrackerController, - AccountTrackerControllerState, - AssetsContractController, - CurrencyRateController, - CurrencyRateState, - NftController, - NftDetectionController, - NftControllerState, - TokenBalancesController, - TokenDetectionController, - TokenListController, - TokenListState, - TokenRatesController, - TokenRatesControllerState, - TokensController, - TokensControllerState, - CodefiTokenPricesServiceV2, - TokenBalancesControllerState, -} from '@metamask/assets-controllers'; -///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) -import { AppState } from 'react-native'; -import PREINSTALLED_SNAPS from '../../lib/snaps/preinstalled-snaps'; -///: END:ONLY_INCLUDE_IF -import { - AddressBookController, - AddressBookControllerState, -} from '@metamask/address-book-controller'; -import { BaseState } from '@metamask/base-controller'; -import { ComposableController } from '@metamask/composable-controller'; -import { - KeyringController, - KeyringControllerState, - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - KeyringTypes, - ///: END:ONLY_INCLUDE_IF -} from '@metamask/keyring-controller'; -import { - NetworkController, - NetworkControllerMessenger, - NetworkState, - NetworkStatus, -} from '@metamask/network-controller'; -import { - PhishingController, - PhishingControllerState, -} from '@metamask/phishing-controller'; -import { - PreferencesController, - PreferencesState, -} from '@metamask/preferences-controller'; -import { - TransactionController, - TransactionControllerState, - TransactionMeta, - TransactionControllerOptions, -} from '@metamask/transaction-controller'; -import { GasFeeController, GasFeeState } from '@metamask/gas-fee-controller'; -import { - AcceptOptions, - ApprovalController, - ApprovalControllerState, -} from '@metamask/approval-controller'; -import { - SelectedNetworkController, - SelectedNetworkControllerState, -} from '@metamask/selected-network-controller'; -import { - PermissionController, - PermissionControllerState, - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - SubjectMetadataController, - SubjectMetadataControllerState, - ///: END:ONLY_INCLUDE_IF -} from '@metamask/permission-controller'; -import SwapsController, { swapsUtils } from '@metamask/swaps-controller'; -import { PPOMController, PPOMState } from '@metamask/ppom-validator'; -///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) -import { - JsonSnapsRegistry, - SnapController, - SnapsRegistryState, - PersistedSnapControllerState, - SnapsRegistryMessenger, -} from '@metamask/snaps-controllers'; - -import { WebViewExecutionService } from '@metamask/snaps-controllers/react-native'; -import { NotificationParameters } from '@metamask/snaps-rpc-methods/dist/restricted/notify.cjs'; -import { getSnapsWebViewPromise } from '../../lib/snaps'; -import { - buildSnapEndowmentSpecifications, - buildSnapRestrictedMethodSpecifications, -} from '@metamask/snaps-rpc-methods'; -import type { EnumToUnion, DialogType } from '@metamask/snaps-sdk'; -// eslint-disable-next-line import/no-nodejs-modules -import { Duplex } from 'stream'; -///: END:ONLY_INCLUDE_IF -import { MetaMaskKeyring as QRHardwareKeyring } from '@keystonehq/metamask-airgapped-keyring'; -import { - LoggingController, - LoggingControllerState, -} from '@metamask/logging-controller'; -import { - LedgerKeyring, - LedgerMobileBridge, - LedgerTransportMiddleware, -} from '@metamask/eth-ledger-bridge-keyring'; -import { Encryptor, LEGACY_DERIVATION_OPTIONS } from '../Encryptor'; -import { - isMainnetByChainId, - fetchEstimatedMultiLayerL1Fee, - isTestNet, - deprecatedGetNetworkId, - getDecimalChainId, -} from '../../util/networks'; -import AppConstants from '../AppConstants'; -import { store } from '../../store'; -import { - renderFromTokenMinimalUnit, - balanceToFiatNumber, - weiToFiatNumber, - toHexadecimal, - addHexPrefix, - hexToBN, -} from '../../util/number'; -import NotificationManager from '../NotificationManager'; -import Logger from '../../util/Logger'; -import { isZero } from '../../util/lodash'; -import { MetaMetricsEvents, MetaMetrics } from '../Analytics'; - -///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) -import { - SnapBridge, - ExcludedSnapEndowments, - ExcludedSnapPermissions, - EndowmentPermissions, - detectSnapLocation, - fetchFunction, - DetectSnapLocationOptions, -} from '../Snaps'; -import { getRpcMethodMiddleware } from '../RPCMethods/RPCMethodMiddleware'; - -import { - AuthenticationController, - UserStorageController, -} from '@metamask/profile-sync-controller'; -import { - NotificationServicesController, - NotificationServicesPushController, -} from '@metamask/notification-services-controller'; -///: END:ONLY_INCLUDE_IF -import { - getCaveatSpecifications, - getPermissionSpecifications, - unrestrictedMethods, -} from '../Permissions/specifications.js'; -import { backupVault } from '../BackupVault'; -import { - SignatureController, - SignatureControllerOptions, -} from '@metamask/signature-controller'; -import { hasProperty, Hex, Json } from '@metamask/utils'; -// TODO: Export this type from the package directly -import { SwapsState } from '@metamask/swaps-controller/dist/SwapsController'; -import { providerErrors } from '@metamask/rpc-errors'; - -import { PPOM, ppomInit } from '../../lib/ppom/PPOMView'; -import RNFSStorageBackend from '../../lib/ppom/ppom-storage-backend'; -import { - AccountsControllerState, - AccountsController, - AccountsControllerMessenger, -} from '@metamask/accounts-controller'; -import { captureException } from '@sentry/react-native'; -import { lowerCase } from 'lodash'; -import { - networkIdUpdated, - networkIdWillUpdate, -} from '../../core/redux/slices/inpageProvider'; -import SmartTransactionsController, { - type SmartTransactionsControllerState, -} from '@metamask/smart-transactions-controller'; -import { getAllowedSmartTransactionsChainIds } from '../../../app/constants/smartTransactions'; -import { selectShouldUseSmartTransaction } from '../../selectors/smartTransactionsController'; -import { selectSwapsChainFeatureFlags } from '../../reducers/swaps'; -import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; -import { submitSmartTransactionHook } from '../../util/smart-transactions/smart-publish-hook'; -import { zeroAddress } from 'ethereumjs-util'; -import { ApprovalType, toChecksumHexAddress } from '@metamask/controller-utils'; -import { ExtendedControllerMessenger } from '../ExtendedControllerMessenger'; -import EthQuery from '@metamask/eth-query'; -import DomainProxyMap from '../../lib/DomainProxyMap/DomainProxyMap'; -import { - MetaMetricsEventCategory, - MetaMetricsEventName, -} from '@metamask/smart-transactions-controller/dist/constants'; -import { - getSmartTransactionMetricsProperties as getSmartTransactionMetricsPropertiesType, - getSmartTransactionMetricsSensitiveProperties as getSmartTransactionMetricsSensitivePropertiesType, -} from '@metamask/smart-transactions-controller/dist/utils'; -///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) -import { snapKeyringBuilder } from '../SnapKeyring'; -import { removeAccountsFromPermissions } from '../Permissions'; -import { keyringSnapPermissionsBuilder } from '../SnapKeyring/keyringSnapsPermissions'; -import { HandleSnapRequestArgs } from '../Snaps/types'; -import { handleSnapRequest } from '../Snaps/utils'; -///: END:ONLY_INCLUDE_IF -import { getSmartTransactionMetricsProperties } from '../../util/smart-transactions'; -import { trace } from '../../util/trace'; -import { MetricsEventBuilder } from '../Analytics/MetricsEventBuilder'; -import { JsonMap } from '../Analytics/MetaMetrics.types'; -import { isPooledStakingFeatureEnabled } from '../../components/UI/Stake/constants'; -import { ControllerMessenger } from './types'; - -const NON_EMPTY = 'NON_EMPTY'; - -const encryptor = new Encryptor({ - keyDerivationOptions: LEGACY_DERIVATION_OPTIONS, -}); -// TODO: Replace "any" with type -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let currentChainId: any; - -type PermissionsByRpcMethod = ReturnType; -type Permissions = PermissionsByRpcMethod[keyof PermissionsByRpcMethod]; - -export interface EngineState { - AccountTrackerController: AccountTrackerControllerState; - AddressBookController: AddressBookControllerState; - AssetsContractController: BaseState; - NftController: NftControllerState; - TokenListController: TokenListState; - CurrencyRateController: CurrencyRateState; - KeyringController: KeyringControllerState; - NetworkController: NetworkState; - PreferencesController: PreferencesState; - PhishingController: PhishingControllerState; - TokenBalancesController: TokenBalancesControllerState; - TokenRatesController: TokenRatesControllerState; - TransactionController: TransactionControllerState; - SmartTransactionsController: SmartTransactionsControllerState; - SwapsController: SwapsState; - GasFeeController: GasFeeState; - TokensController: TokensControllerState; - TokenDetectionController: BaseState; - NftDetectionController: BaseState; - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - SnapController: PersistedSnapControllerState; - SnapsRegistry: SnapsRegistryState; - SubjectMetadataController: SubjectMetadataControllerState; - AuthenticationController: AuthenticationController.AuthenticationControllerState; - UserStorageController: UserStorageController.UserStorageControllerState; - NotificationServicesController: NotificationServicesController.NotificationServicesControllerState; - NotificationServicesPushController: NotificationServicesPushController.NotificationServicesPushControllerState; - ///: END:ONLY_INCLUDE_IF - PermissionController: PermissionControllerState; - ApprovalController: ApprovalControllerState; - LoggingController: LoggingControllerState; - PPOMController: PPOMState; - AccountsController: AccountsControllerState; - SelectedNetworkController: SelectedNetworkControllerState; -} - -/** - * All mobile controllers, keyed by name - */ -interface Controllers { - AccountsController: AccountsController; - AccountTrackerController: AccountTrackerController; - AddressBookController: AddressBookController; - ApprovalController: ApprovalController; - AssetsContractController: AssetsContractController; - CurrencyRateController: CurrencyRateController; - GasFeeController: GasFeeController; - KeyringController: KeyringController; - LoggingController: LoggingController; - NetworkController: NetworkController; - NftController: NftController; - NftDetectionController: NftDetectionController; - // TODO: Fix permission types - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - PermissionController: PermissionController; - SelectedNetworkController: SelectedNetworkController; - PhishingController: PhishingController; - PreferencesController: PreferencesController; - PPOMController: PPOMController; - TokenBalancesController: TokenBalancesController; - TokenListController: TokenListController; - TokenDetectionController: TokenDetectionController; - TokenRatesController: TokenRatesController; - TokensController: TokensController; - TransactionController: TransactionController; - SmartTransactionsController: SmartTransactionsController; - SignatureController: SignatureController; - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - SnapController: SnapController; - SubjectMetadataController: SubjectMetadataController; - AuthenticationController: AuthenticationController.Controller; - UserStorageController: UserStorageController.Controller; - NotificationServicesController: NotificationServicesController.Controller; - NotificationServicesPushController: NotificationServicesPushController.Controller; - ///: END:ONLY_INCLUDE_IF - SwapsController: SwapsController; -} - -/** - * Controllers that area always instantiated - */ -type RequiredControllers = Omit; - -/** - * Controllers that are sometimes not instantiated - */ -type OptionalControllers = Pick; - -/** - * Combines required and optional controllers for the Engine context type. - */ -export type EngineContext = RequiredControllers & Partial; - -export interface TransactionEventPayload { - transactionMeta: TransactionMeta; - actionId?: string; - error?: string; -} - -/** - * Core controller responsible for composing other metamask controllers together - * and exposing convenience methods for common wallet operations. - */ -export class Engine { - /** - * The global Engine singleton - */ - static instance: Engine | null; - /** - * A collection of all controller instances - */ - context: EngineContext; - /** - * The global controller messenger. - */ - controllerMessenger: ControllerMessenger; - /** - * ComposableController reference containing all child controllers - */ - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - datamodel: any; - - /** - * Object containing the info for the latest incoming tx block - * for each address and network - */ - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - lastIncomingTxBlockInfo: any; - - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - /** - * Object that runs and manages the execution of Snaps - */ - snapExecutionService: WebViewExecutionService; - snapController: SnapController; - subjectMetadataController: SubjectMetadataController; - - ///: END:ONLY_INCLUDE_IF - - transactionController: TransactionController; - smartTransactionsController: SmartTransactionsController; - - keyringController: KeyringController; - - /** - * Creates a CoreController instance - */ - // eslint-disable-next-line @typescript-eslint/default-param-last - constructor( - initialState: Partial = {}, - initialKeyringState?: KeyringControllerState | null, - ) { - this.controllerMessenger = new ExtendedControllerMessenger(); - - const approvalController = new ApprovalController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'ApprovalController', - allowedEvents: [], - allowedActions: [], - }), - showApprovalRequest: () => undefined, - typesExcludedFromRateLimiting: [ - ApprovalType.Transaction, - ApprovalType.WatchAsset, - ], - }); - - const preferencesController = new PreferencesController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'PreferencesController', - allowedActions: [], - allowedEvents: ['KeyringController:stateChange'], - }), - state: { - ipfsGateway: AppConstants.IPFS_DEFAULT_GATEWAY_URL, - useTokenDetection: - initialState?.PreferencesController?.useTokenDetection ?? true, - useNftDetection: true, // set this to true to enable nft detection by default to new users - displayNftMedia: true, - securityAlertsEnabled: true, - smartTransactionsOptInStatus: true, - tokenSortConfig: { - key: 'tokenFiatAmount', - order: 'dsc', - sortCallback: 'stringNumeric', - }, - ...initialState.PreferencesController, - }, - }); - - const networkControllerOpts = { - infuraProjectId: process.env.MM_INFURA_PROJECT_ID || NON_EMPTY, - state: initialState.NetworkController, - messenger: this.controllerMessenger.getRestricted({ - name: 'NetworkController', - allowedEvents: [], - allowedActions: [], - }) as unknown as NetworkControllerMessenger, - // Metrics event tracking is handled in this repository instead - // TODO: Use events for controller metric events - trackMetaMetricsEvent: () => { - // noop - }, - }; - const networkController = new NetworkController(networkControllerOpts); - - networkController.initializeProvider(); - - const assetsContractController = new AssetsContractController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'AssetsContractController', - allowedActions: [ - 'NetworkController:getNetworkClientById', - 'NetworkController:getNetworkConfigurationByNetworkClientId', - 'NetworkController:getSelectedNetworkClient', - 'NetworkController:getState', - ], - allowedEvents: [ - 'PreferencesController:stateChange', - 'NetworkController:networkDidChange', - ], - }), - chainId: networkController.getNetworkClientById( - networkController?.state.selectedNetworkClientId, - ).configuration.chainId, - }); - const accountsControllerMessenger: AccountsControllerMessenger = - this.controllerMessenger.getRestricted({ - name: 'AccountsController', - allowedEvents: [ - 'SnapController:stateChange', - 'KeyringController:accountRemoved', - 'KeyringController:stateChange', - ], - allowedActions: [ - 'KeyringController:getAccounts', - 'KeyringController:getKeyringsByType', - 'KeyringController:getKeyringForAccount', - ], - }); - - const defaultAccountsControllerState: AccountsControllerState = { - internalAccounts: { - accounts: {}, - selectedAccount: '', - }, - }; - - const accountsController = new AccountsController({ - messenger: accountsControllerMessenger, - state: initialState.AccountsController ?? defaultAccountsControllerState, - }); - - const nftController = new NftController({ - chainId: networkController.getNetworkClientById( - networkController?.state.selectedNetworkClientId, - ).configuration.chainId, - useIpfsSubdomains: false, - messenger: this.controllerMessenger.getRestricted({ - name: 'NftController', - allowedActions: [ - `${approvalController.name}:addRequest`, - `${networkController.name}:getNetworkClientById`, - 'AccountsController:getAccount', - 'AccountsController:getSelectedAccount', - 'AssetsContractController:getERC721AssetName', - 'AssetsContractController:getERC721AssetSymbol', - 'AssetsContractController:getERC721TokenURI', - 'AssetsContractController:getERC721OwnerOf', - 'AssetsContractController:getERC1155BalanceOf', - 'AssetsContractController:getERC1155TokenURI', - ], - allowedEvents: [ - 'PreferencesController:stateChange', - 'NetworkController:networkDidChange', - 'AccountsController:selectedEvmAccountChange', - ], - }), - }); - - const loggingController = new LoggingController({ - messenger: this.controllerMessenger.getRestricted< - 'LoggingController', - never, - never - >({ - name: 'LoggingController', - allowedActions: [], - allowedEvents: [], - }), - state: initialState.LoggingController, - }); - const tokensController = new TokensController({ - chainId: networkController.getNetworkClientById( - networkController?.state.selectedNetworkClientId, - ).configuration.chainId, - // @ts-expect-error at this point in time the provider will be defined by the `networkController.initializeProvider` - provider: networkController.getProviderAndBlockTracker().provider, - state: initialState.TokensController, - messenger: this.controllerMessenger.getRestricted({ - name: 'TokensController', - allowedActions: [ - `${approvalController.name}:addRequest`, - 'NetworkController:getNetworkClientById', - 'AccountsController:getAccount', - 'AccountsController:getSelectedAccount', - ], - allowedEvents: [ - 'PreferencesController:stateChange', - 'NetworkController:networkDidChange', - 'TokenListController:stateChange', - 'AccountsController:selectedEvmAccountChange', - ], - }), - }); - const tokenListController = new TokenListController({ - chainId: networkController.getNetworkClientById( - networkController?.state.selectedNetworkClientId, - ).configuration.chainId, - onNetworkStateChange: (listener) => - this.controllerMessenger.subscribe( - AppConstants.NETWORK_STATE_CHANGE_EVENT, - listener, - ), - messenger: this.controllerMessenger.getRestricted({ - name: 'TokenListController', - allowedActions: [`${networkController.name}:getNetworkClientById`], - allowedEvents: [`${networkController.name}:stateChange`], - }), - }); - const currencyRateController = new CurrencyRateController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'CurrencyRateController', - allowedActions: [`${networkController.name}:getNetworkClientById`], - allowedEvents: [], - }), - state: initialState.CurrencyRateController, - }); - - const gasFeeController = new GasFeeController({ - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. - messenger: this.controllerMessenger.getRestricted({ - name: 'GasFeeController', - allowedActions: [ - `${networkController.name}:getNetworkClientById`, - `${networkController.name}:getEIP1559Compatibility`, - `${networkController.name}:getState`, - ], - allowedEvents: [AppConstants.NETWORK_DID_CHANGE_EVENT], - }), - getProvider: () => - // @ts-expect-error at this point in time the provider will be defined by the `networkController.initializeProvider` - networkController.getProviderAndBlockTracker().provider, - getCurrentNetworkEIP1559Compatibility: async () => - (await networkController.getEIP1559Compatibility()) ?? false, - getCurrentNetworkLegacyGasAPICompatibility: () => { - const chainId = networkController.getNetworkClientById( - networkController?.state.selectedNetworkClientId, - ).configuration.chainId; - return ( - isMainnetByChainId(chainId) || - chainId === addHexPrefix(swapsUtils.BSC_CHAIN_ID) || - chainId === addHexPrefix(swapsUtils.POLYGON_CHAIN_ID) - ); - }, - clientId: AppConstants.SWAPS.CLIENT_ID, - legacyAPIEndpoint: - 'https://gas.api.cx.metamask.io/networks//gasPrices', - EIP1559APIEndpoint: - 'https://gas.api.cx.metamask.io/networks//suggestedGasFees', - }); - - const phishingController = new PhishingController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'PhishingController', - allowedActions: [], - allowedEvents: [], - }), - }); - phishingController.maybeUpdateState(); - - const additionalKeyrings = []; - - const qrKeyringBuilder = () => { - const keyring = new QRHardwareKeyring(); - // to fix the bug in #9560, forgetDevice will reset all keyring properties to default. - keyring.forgetDevice(); - return keyring; - }; - qrKeyringBuilder.type = QRHardwareKeyring.type; - - additionalKeyrings.push(qrKeyringBuilder); - - const bridge = new LedgerMobileBridge(new LedgerTransportMiddleware()); - const ledgerKeyringBuilder = () => new LedgerKeyring({ bridge }); - ledgerKeyringBuilder.type = LedgerKeyring.type; - - additionalKeyrings.push(ledgerKeyringBuilder); - - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - const snapKeyringBuildMessenger = this.controllerMessenger.getRestricted({ - name: 'SnapKeyringBuilder', - allowedActions: [ - 'ApprovalController:addRequest', - 'ApprovalController:acceptRequest', - 'ApprovalController:rejectRequest', - 'ApprovalController:startFlow', - 'ApprovalController:endFlow', - 'ApprovalController:showSuccess', - 'ApprovalController:showError', - 'PhishingController:testOrigin', - 'PhishingController:maybeUpdateState', - 'KeyringController:getAccounts', - 'AccountsController:setSelectedAccount', - 'AccountsController:getAccountByAddress', - 'AccountsController:setAccountName', - ], - allowedEvents: [], - }); - - const getSnapController = () => this.snapController; - - // Necessary to persist the keyrings and update the accounts both within the keyring controller and accounts controller - const persistAndUpdateAccounts = async () => { - await this.keyringController.persistAllKeyrings(); - await accountsController.updateAccounts(); - }; - - additionalKeyrings.push( - snapKeyringBuilder( - snapKeyringBuildMessenger, - getSnapController, - persistAndUpdateAccounts, - (address) => this.removeAccount(address), - ), - ); - - ///: END:ONLY_INCLUDE_IF - - this.keyringController = new KeyringController({ - removeIdentity: preferencesController.removeIdentity.bind( - preferencesController, - ), - encryptor, - messenger: this.controllerMessenger.getRestricted({ - name: 'KeyringController', - allowedActions: [], - allowedEvents: [], - }), - state: initialKeyringState || initialState.KeyringController, - // @ts-expect-error To Do: Update the type of QRHardwareKeyring to Keyring - keyringBuilders: additionalKeyrings, - }); - - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - /** - * Gets the mnemonic of the user's primary keyring. - */ - const getPrimaryKeyringMnemonic = () => { - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [keyring]: any = this.keyringController.getKeyringsByType( - KeyringTypes.hd, - ); - if (!keyring.mnemonic) { - throw new Error('Primary keyring mnemonic unavailable.'); - } - - return keyring.mnemonic; - }; - - const getAppState = () => { - const state = AppState.currentState; - return state === 'active'; - }; - - const snapRestrictedMethods = { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - clearSnapState: this.controllerMessenger.call.bind( - this.controllerMessenger, - 'SnapController:clearSnapState', - ), - getMnemonic: getPrimaryKeyringMnemonic.bind(this), - getUnlockPromise: getAppState.bind(this), - getSnap: this.controllerMessenger.call.bind( - this.controllerMessenger, - 'SnapController:get', - ), - handleSnapRpcRequest: async (args: HandleSnapRequestArgs) => - await handleSnapRequest(this.controllerMessenger, args), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - getSnapState: this.controllerMessenger.call.bind( - this.controllerMessenger, - 'SnapController:getSnapState', - ), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - updateSnapState: this.controllerMessenger.call.bind( - this.controllerMessenger, - 'SnapController:updateSnapState', - ), - maybeUpdatePhishingList: this.controllerMessenger.call.bind( - this.controllerMessenger, - 'PhishingController:maybeUpdateState', - ), - isOnPhishingList: (origin: string) => - this.controllerMessenger.call<'PhishingController:testOrigin'>( - 'PhishingController:testOrigin', - origin, - ).result, - showDialog: ( - origin: string, - type: EnumToUnion, - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - content: any, // should be Component from '@metamask/snaps-ui'; - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - placeholder?: any, - ) => - approvalController.addAndShowApprovalRequest({ - origin, - type, - requestData: { content, placeholder }, - }), - showInAppNotification: (origin: string, args: NotificationParameters) => { - Logger.log( - 'Snaps/ showInAppNotification called with args: ', - args, - ' and origin: ', - origin, - ); - }, - hasPermission: (origin: string, target: string) => - this.controllerMessenger.call<'PermissionController:hasPermission'>( - 'PermissionController:hasPermission', - origin, - target, - ), - }; - ///: END:ONLY_INCLUDE_IF - - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - const keyringSnapMethods = { - getAllowedKeyringMethods: (origin: string) => - keyringSnapPermissionsBuilder(origin), - getSnapKeyring: this.getSnapKeyring.bind(this), - }; - ///: END:ONLY_INCLUDE_IF - - const getSnapPermissionSpecifications = () => ({ - ...buildSnapEndowmentSpecifications(Object.keys(ExcludedSnapEndowments)), - ...buildSnapRestrictedMethodSpecifications( - Object.keys(ExcludedSnapPermissions), - { - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - ...snapRestrictedMethods, - ///: END:ONLY_INCLUDE_IF - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - ...keyringSnapMethods, - ///: END:ONLY_INCLUDE_IF - }, - ), - }); - - const accountTrackerController = new AccountTrackerController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'AccountTrackerController', - allowedActions: [ - 'AccountsController:getSelectedAccount', - 'AccountsController:listAccounts', - 'PreferencesController:getState', - 'NetworkController:getState', - 'NetworkController:getNetworkClientById', - ], - allowedEvents: [ - 'AccountsController:selectedEvmAccountChange', - 'AccountsController:selectedAccountChange', - ], - }), - state: initialState.AccountTrackerController ?? { accounts: {} }, - getStakedBalanceForChain: - assetsContractController.getStakedBalanceForChain.bind( - assetsContractController, - ), - includeStakedAssets: isPooledStakingFeatureEnabled(), - }); - const permissionController = new PermissionController({ - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. - messenger: this.controllerMessenger.getRestricted({ - name: 'PermissionController', - allowedActions: [ - `${approvalController.name}:addRequest`, - `${approvalController.name}:hasRequest`, - `${approvalController.name}:acceptRequest`, - `${approvalController.name}:rejectRequest`, - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - `SnapController:getPermitted`, - `SnapController:install`, - `SubjectMetadataController:getSubjectMetadata`, - ///: END:ONLY_INCLUDE_IF - ], - allowedEvents: [], - }), - state: initialState.PermissionController, - caveatSpecifications: getCaveatSpecifications({ - getInternalAccounts: - accountsController.listAccounts.bind(accountsController), - findNetworkClientIdByChainId: - networkController.findNetworkClientIdByChainId.bind( - networkController, - ), - }), - // @ts-expect-error Typecast permissionType from getPermissionSpecifications to be of type PermissionType.RestrictedMethod - permissionSpecifications: { - ...getPermissionSpecifications({ - getAllAccounts: () => this.keyringController.getAccounts(), - getInternalAccounts: - accountsController.listAccounts.bind(accountsController), - captureKeyringTypesWithMissingIdentities: ( - internalAccounts = [], - accounts = [], - ) => { - const accountsMissingIdentities = accounts.filter((address) => { - const lowerCaseAddress = lowerCase(address); - return !internalAccounts.some( - (account) => account.address.toLowerCase() === lowerCaseAddress, - ); - }); - const keyringTypesWithMissingIdentities = - accountsMissingIdentities.map((address) => - this.keyringController.getAccountKeyringType(address), - ); - - const internalAccountCount = internalAccounts.length; - - const accountTrackerCount = Object.keys( - accountTrackerController.state.accounts || {}, - ).length; - - captureException( - new Error( - `Attempt to get permission specifications failed because there were ${accounts.length} accounts, but ${internalAccountCount} identities, and the ${keyringTypesWithMissingIdentities} keyrings included accounts with missing identities. Meanwhile, there are ${accountTrackerCount} accounts in the account tracker.`, - ), - ); - }, - }), - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - ...getSnapPermissionSpecifications(), - ///: END:ONLY_INCLUDE_IF - }, - unrestrictedMethods, - }); - - const selectedNetworkController = new SelectedNetworkController({ - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. - messenger: this.controllerMessenger.getRestricted({ - name: 'SelectedNetworkController', - allowedActions: [ - 'NetworkController:getNetworkClientById', - 'NetworkController:getState', - 'NetworkController:getSelectedNetworkClient', - 'PermissionController:hasPermissions', - 'PermissionController:getSubjectNames', - ], - allowedEvents: [ - 'NetworkController:stateChange', - 'PermissionController:stateChange', - ], - }), - state: initialState.SelectedNetworkController || { domains: {} }, - useRequestQueuePreference: !!process.env.MULTICHAIN_V1, - // TODO we need to modify core PreferencesController for better cross client support - onPreferencesStateChange: ( - listener: ({ useRequestQueue }: { useRequestQueue: boolean }) => void, - ) => listener({ useRequestQueue: !!process.env.MULTICHAIN_V1 }), - domainProxyMap: new DomainProxyMap(), - }); - - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - this.subjectMetadataController = new SubjectMetadataController({ - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. - messenger: this.controllerMessenger.getRestricted({ - name: 'SubjectMetadataController', - allowedActions: [`${permissionController.name}:hasPermissions`], - allowedEvents: [], - }), - state: initialState.SubjectMetadataController || {}, - subjectCacheLimit: 100, - }); - - const setupSnapProvider = (snapId: string, connectionStream: Duplex) => { - Logger.log( - '[ENGINE LOG] Engine+setupSnapProvider: Setup stream for Snap', - snapId, - ); - // TO DO: - // Develop a simpler getRpcMethodMiddleware object for SnapBridge - // Consider developing an abstract class to derived custom implementations for each use case - const bridge = new SnapBridge({ - snapId, - connectionStream, - getRPCMethodMiddleware: ({ hostname, getProviderState }) => - getRpcMethodMiddleware({ - hostname, - getProviderState, - navigation: null, - getApprovedHosts: () => null, - setApprovedHosts: () => null, - approveHost: () => null, - title: { current: 'Snap' }, - icon: { current: undefined }, - isHomepage: () => false, - fromHomepage: { current: false }, - toggleUrlModal: () => null, - wizardScrollAdjusted: { current: false }, - tabId: false, - isWalletConnect: true, - isMMSDK: false, - url: { current: '' }, - analytics: {}, - injectHomePageScripts: () => null, - }), - }); - - bridge.setupProviderConnection(); - }; - - const requireAllowlist = process.env.METAMASK_BUILD_TYPE === 'main'; - const disableSnapInstallation = process.env.METAMASK_BUILD_TYPE === 'main'; - const allowLocalSnaps = process.env.METAMASK_BUILD_TYPE === 'flask'; - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. - const snapsRegistryMessenger: SnapsRegistryMessenger = - this.controllerMessenger.getRestricted({ - name: 'SnapsRegistry', - allowedEvents: [], - allowedActions: [], - }); - const snapsRegistry = new JsonSnapsRegistry({ - state: initialState.SnapsRegistry, - messenger: snapsRegistryMessenger, - refetchOnAllowlistMiss: requireAllowlist, - url: { - registry: 'https://acl.execution.metamask.io/latest/registry.json', - signature: 'https://acl.execution.metamask.io/latest/signature.json', - }, - publicKey: - '0x025b65308f0f0fb8bc7f7ff87bfc296e0330eee5d3c1d1ee4a048b2fd6a86fa0a6', - }); - - this.snapExecutionService = new WebViewExecutionService({ - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. - messenger: this.controllerMessenger.getRestricted({ - name: 'ExecutionService', - allowedActions: [], - allowedEvents: [], - }), - setupSnapProvider: setupSnapProvider.bind(this), - getWebView: () => getSnapsWebViewPromise, - }); - - const snapControllerMessenger = this.controllerMessenger.getRestricted({ - name: 'SnapController', - allowedEvents: [ - 'ExecutionService:unhandledError', - 'ExecutionService:outboundRequest', - 'ExecutionService:outboundResponse', - ], - allowedActions: [ - `${approvalController.name}:addRequest`, - `${permissionController.name}:getEndowments`, - `${permissionController.name}:getPermissions`, - `${permissionController.name}:hasPermission`, - `${permissionController.name}:hasPermissions`, - `${permissionController.name}:requestPermissions`, - `${permissionController.name}:revokeAllPermissions`, - `${permissionController.name}:revokePermissions`, - `${permissionController.name}:revokePermissionForAllSubjects`, - `${permissionController.name}:getSubjectNames`, - `${permissionController.name}:updateCaveat`, - `${approvalController.name}:addRequest`, - `${approvalController.name}:updateRequestState`, - `${permissionController.name}:grantPermissions`, - `${this.subjectMetadataController.name}:getSubjectMetadata`, - `${this.subjectMetadataController.name}:addSubjectMetadata`, - `${phishingController.name}:maybeUpdateState`, - `${phishingController.name}:testOrigin`, - `${snapsRegistry.name}:get`, - `${snapsRegistry.name}:getMetadata`, - `${snapsRegistry.name}:update`, - 'ExecutionService:executeSnap', - 'ExecutionService:terminateSnap', - 'ExecutionService:terminateAllSnaps', - 'ExecutionService:handleRpcRequest', - 'SnapsRegistry:get', - 'SnapsRegistry:getMetadata', - 'SnapsRegistry:update', - 'SnapsRegistry:resolveVersion', - ], - }); - - this.snapController = new SnapController({ - environmentEndowmentPermissions: Object.values(EndowmentPermissions), - featureFlags: { - requireAllowlist, - allowLocalSnaps, - disableSnapInstallation, - }, - state: initialState.SnapController || undefined, - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - messenger: snapControllerMessenger as any, - detectSnapLocation: ( - location: string | URL, - options?: DetectSnapLocationOptions, - ) => - detectSnapLocation(location, { - ...options, - fetch: fetchFunction, - }), - //@ts-expect-error types need to be aligned with snaps-controllers - preinstalledSnaps: PREINSTALLED_SNAPS, - //@ts-expect-error types need to be aligned between new encryptor and snaps-controllers - encryptor, - getMnemonic: getPrimaryKeyringMnemonic.bind(this), - getFeatureFlags: () => ({ - disableSnaps: - store.getState().settings.basicFunctionalityEnabled === false, - }), - }); - - const authenticationController = new AuthenticationController.Controller({ - state: initialState.AuthenticationController, - messenger: this.controllerMessenger.getRestricted({ - name: 'AuthenticationController', - allowedActions: [ - 'KeyringController:getState', - 'KeyringController:getAccounts', - - 'SnapController:handleRequest', - 'UserStorageController:enableProfileSyncing', - ], - allowedEvents: ['KeyringController:unlock', 'KeyringController:lock'], - }), - metametrics: { - agent: 'mobile', - getMetaMetricsId: async () => - (await MetaMetrics.getInstance().getMetaMetricsId()) || '', - }, - }); - - const userStorageController = new UserStorageController.Controller({ - getMetaMetricsState: () => MetaMetrics.getInstance().isEnabled(), - env: { - isAccountSyncingEnabled: Boolean(process.env.IS_TEST), - }, - config: { - accountSyncing: { - onAccountAdded: (profileId) => { - MetaMetrics.getInstance().trackEvent( - MetricsEventBuilder.createEventBuilder( - MetaMetricsEvents.ACCOUNTS_SYNC_ADDED, - ) - .addProperties({ - profile_id: profileId, - }) - .build(), - ); - }, - onAccountNameUpdated: (profileId) => { - MetaMetrics.getInstance().trackEvent( - MetricsEventBuilder.createEventBuilder( - MetaMetricsEvents.ACCOUNTS_SYNC_NAME_UPDATED, - ) - .addProperties({ - profile_id: profileId, - }) - .build(), - ); - }, - }, - }, - state: initialState.UserStorageController, - messenger: this.controllerMessenger.getRestricted({ - name: 'UserStorageController', - allowedActions: [ - 'SnapController:handleRequest', - 'KeyringController:getState', - 'KeyringController:addNewAccount', - 'AuthenticationController:getBearerToken', - 'AuthenticationController:getSessionProfile', - 'AuthenticationController:isSignedIn', - 'AuthenticationController:performSignOut', - 'AuthenticationController:performSignIn', - 'NotificationServicesController:disableNotificationServices', - 'NotificationServicesController:selectIsNotificationServicesEnabled', - 'AccountsController:listAccounts', - 'AccountsController:updateAccountMetadata', - ], - allowedEvents: [ - 'KeyringController:unlock', - 'KeyringController:lock', - 'AccountsController:accountAdded', - 'AccountsController:accountRenamed', - ], - }), - nativeScryptCrypto: scrypt, - }); - - const notificationServicesController = - new NotificationServicesController.Controller({ - messenger: this.controllerMessenger.getRestricted({ - name: 'NotificationServicesController', - allowedActions: [ - 'KeyringController:getState', - 'KeyringController:getAccounts', - 'AuthenticationController:getBearerToken', - 'AuthenticationController:isSignedIn', - 'UserStorageController:enableProfileSyncing', - 'UserStorageController:getStorageKey', - 'UserStorageController:performGetStorage', - 'UserStorageController:performSetStorage', - 'NotificationServicesPushController:enablePushNotifications', - 'NotificationServicesPushController:disablePushNotifications', - 'NotificationServicesPushController:updateTriggerPushNotifications', - ], - allowedEvents: [ - 'KeyringController:unlock', - 'KeyringController:lock', - 'KeyringController:stateChange', - ], - }), - state: initialState.NotificationServicesController, - env: { - isPushIntegrated: false, - featureAnnouncements: { - platform: 'mobile', - accessToken: process.env - .FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN as string, - spaceId: process.env.FEATURES_ANNOUNCEMENTS_SPACE_ID as string, - }, - }, - }); - - const notificationServicesPushControllerMessenger = - this.controllerMessenger.getRestricted({ - name: 'NotificationServicesPushController', - allowedActions: ['AuthenticationController:getBearerToken'], - allowedEvents: [], - }); - - const notificationServicesPushController = - new NotificationServicesPushController.Controller({ - messenger: notificationServicesPushControllerMessenger, - state: initialState.NotificationServicesPushController || { - fcmToken: '', - }, - env: { - apiKey: process.env.FIREBASE_API_KEY ?? '', - authDomain: process.env.FIREBASE_AUTH_DOMAIN ?? '', - storageBucket: process.env.FIREBASE_STORAGE_BUCKET ?? '', - projectId: process.env.FIREBASE_PROJECT_ID ?? '', - messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID ?? '', - appId: process.env.FIREBASE_APP_ID ?? '', - measurementId: process.env.FIREBASE_MEASUREMENT_ID ?? '', - vapidKey: process.env.VAPID_KEY ?? '', - }, - config: { - isPushEnabled: true, - platform: 'mobile', - // TODO: Implement optionability for push notification handlers (depending of the platform) on the NotificationServicesPushController. - onPushNotificationReceived: () => Promise.resolve(undefined), - onPushNotificationClicked: () => Promise.resolve(undefined), - }, - }); - ///: END:ONLY_INCLUDE_IF - - this.transactionController = new TransactionController({ - // @ts-expect-error at this point in time the provider will be defined by the `networkController.initializeProvider` - blockTracker: networkController.getProviderAndBlockTracker().blockTracker, - disableHistory: true, - disableSendFlowHistory: true, - disableSwaps: true, - // @ts-expect-error TransactionController is missing networkClientId argument in type - getCurrentNetworkEIP1559Compatibility: - networkController.getEIP1559Compatibility.bind(networkController), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - getExternalPendingTransactions: (address: string) => - this.smartTransactionsController.getTransactions({ - addressFrom: address, - status: SmartTransactionStatuses.PENDING, - }), - getGasFeeEstimates: - gasFeeController.fetchGasFeeEstimates.bind(gasFeeController), - // but only breaking change is Node version and bumped dependencies - getNetworkClientRegistry: - networkController.getNetworkClientRegistry.bind(networkController), - getNetworkState: () => networkController.state, - hooks: { - publish: (transactionMeta) => { - const shouldUseSmartTransaction = selectShouldUseSmartTransaction( - store.getState(), - ); - - return submitSmartTransactionHook({ - transactionMeta, - transactionController: this.transactionController, - smartTransactionsController: this.smartTransactionsController, - shouldUseSmartTransaction, - approvalController, - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. - controllerMessenger: this.controllerMessenger, - featureFlags: selectSwapsChainFeatureFlags(store.getState()), - }) as Promise<{ transactionHash: string }>; - }, - }, - incomingTransactions: { - isEnabled: () => { - const currentHexChainId = networkController.getNetworkClientById( - networkController?.state.selectedNetworkClientId, - ).configuration.chainId; - - const showIncomingTransactions = - preferencesController?.state?.showIncomingTransactions; - - return Boolean( - hasProperty(showIncomingTransactions, currentChainId) && - showIncomingTransactions?.[currentHexChainId], - ); - }, - updateTransactions: true, - }, - isSimulationEnabled: () => - preferencesController.state.useTransactionSimulations, - messenger: this.controllerMessenger.getRestricted({ - name: 'TransactionController', - allowedActions: [ - `${accountsController.name}:getSelectedAccount`, - `${approvalController.name}:addRequest`, - `${networkController.name}:getNetworkClientById`, - `${networkController.name}:findNetworkClientIdByChainId`, - ], - allowedEvents: [`NetworkController:stateChange`], - }), - onNetworkStateChange: (listener) => - this.controllerMessenger.subscribe( - AppConstants.NETWORK_STATE_CHANGE_EVENT, - listener, - ), - pendingTransactions: { - isResubmitEnabled: () => false, - }, - // @ts-expect-error at this point in time the provider will be defined by the `networkController.initializeProvider` - provider: networkController.getProviderAndBlockTracker().provider, - sign: this.keyringController.signTransaction.bind( - this.keyringController, - ) as unknown as TransactionControllerOptions['sign'], - state: initialState.TransactionController, - }); - - const codefiTokenApiV2 = new CodefiTokenPricesServiceV2(); - - const smartTransactionsControllerTrackMetaMetricsEvent = ( - params: { - event: MetaMetricsEventName; - category: MetaMetricsEventCategory; - properties?: ReturnType< - typeof getSmartTransactionMetricsPropertiesType - >; - sensitiveProperties?: ReturnType< - typeof getSmartTransactionMetricsSensitivePropertiesType - >; - }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - options?: { - metaMetricsId?: string; - }, - ) => { - MetaMetrics.getInstance().trackEvent( - MetricsEventBuilder.createEventBuilder({ - category: params.event, - }) - .addProperties(params.properties || {}) - .addSensitiveProperties(params.sensitiveProperties || {}) - .build(), - ); - }; - this.smartTransactionsController = new SmartTransactionsController({ - // @ts-expect-error TODO: resolve types - supportedChainIds: getAllowedSmartTransactionsChainIds(), - getNonceLock: this.transactionController.getNonceLock.bind( - this.transactionController, - ), - confirmExternalTransaction: - this.transactionController.confirmExternalTransaction.bind( - this.transactionController, - ), - trackMetaMetricsEvent: smartTransactionsControllerTrackMetaMetricsEvent, - state: initialState.SmartTransactionsController, - // @ts-expect-error TODO: Resolve mismatch between base-controller versions. - messenger: this.controllerMessenger.getRestricted({ - name: 'SmartTransactionsController', - allowedActions: ['NetworkController:getNetworkClientById'], - allowedEvents: ['NetworkController:stateChange'], - }), - // @ts-expect-error TODO: Resolve mismatch between smart-transactions-controller and transaction-controller - getTransactions: this.transactionController.getTransactions.bind( - this.transactionController, - ), - getMetaMetricsProps: () => Promise.resolve({}), // Return MetaMetrics props once we enable HW wallets for smart transactions. - }); - - const controllers: Controllers[keyof Controllers][] = [ - this.keyringController, - accountTrackerController, - new AddressBookController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'AddressBookController', - allowedActions: [], - allowedEvents: [], - }), - state: initialState.AddressBookController, - }), - assetsContractController, - nftController, - tokensController, - tokenListController, - new TokenDetectionController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'TokenDetectionController', - allowedActions: [ - 'AccountsController:getSelectedAccount', - 'NetworkController:getNetworkClientById', - 'NetworkController:getNetworkConfigurationByNetworkClientId', - 'NetworkController:getState', - 'KeyringController:getState', - 'PreferencesController:getState', - 'TokenListController:getState', - 'TokensController:getState', - 'TokensController:addDetectedTokens', - 'AccountsController:getAccount', - ], - allowedEvents: [ - 'KeyringController:lock', - 'KeyringController:unlock', - 'PreferencesController:stateChange', - 'NetworkController:networkDidChange', - 'TokenListController:stateChange', - 'TokensController:stateChange', - 'AccountsController:selectedEvmAccountChange', - ], - }), - trackMetaMetricsEvent: () => - MetaMetrics.getInstance().trackEvent( - MetaMetricsEvents.TOKEN_DETECTED, - { - token_standard: 'ERC20', - asset_type: 'token', - chain_id: getDecimalChainId( - networkController.getNetworkClientById( - networkController?.state.selectedNetworkClientId, - ).configuration.chainId, - ), - }, - ), - getBalancesInSingleCall: - assetsContractController.getBalancesInSingleCall.bind( - assetsContractController, - ), - }), - - new NftDetectionController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'NftDetectionController', - allowedEvents: [ - 'NetworkController:stateChange', - 'PreferencesController:stateChange', - ], - allowedActions: [ - 'ApprovalController:addRequest', - 'NetworkController:getState', - 'NetworkController:getNetworkClientById', - 'PreferencesController:getState', - 'AccountsController:getSelectedAccount', - ], - }), - disabled: false, - addNft: nftController.addNft.bind(nftController), - getNftState: () => nftController.state, - }), - currencyRateController, - networkController, - phishingController, - preferencesController, - new TokenBalancesController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'TokenBalancesController', - allowedActions: [ - 'AccountsController:getSelectedAccount', - 'AssetsContractController:getERC20BalanceOf', - ], - allowedEvents: ['TokensController:stateChange'], - }), - interval: 180000, - tokens: [ - ...tokensController.state.tokens, - ...tokensController.state.detectedTokens, - ], - state: initialState.TokenBalancesController, - }), - new TokenRatesController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'TokenRatesController', - allowedActions: [ - 'TokensController:getState', - 'NetworkController:getNetworkClientById', - 'NetworkController:getState', - 'AccountsController:getAccount', - 'AccountsController:getSelectedAccount', - ], - allowedEvents: [ - 'TokensController:stateChange', - 'NetworkController:stateChange', - 'AccountsController:selectedEvmAccountChange', - ], - }), - tokenPricesService: codefiTokenApiV2, - interval: 30 * 60 * 1000, - state: initialState.TokenRatesController || { marketData: {} }, - }), - this.transactionController, - this.smartTransactionsController, - new SwapsController( - { - fetchGasFeeEstimates: () => gasFeeController.fetchGasFeeEstimates(), - // @ts-expect-error TODO: Resolve mismatch between gas fee and swaps controller types - fetchEstimatedMultiLayerL1Fee, - }, - { - clientId: AppConstants.SWAPS.CLIENT_ID, - fetchAggregatorMetadataThreshold: - AppConstants.SWAPS.CACHE_AGGREGATOR_METADATA_THRESHOLD, - fetchTokensThreshold: AppConstants.SWAPS.CACHE_TOKENS_THRESHOLD, - fetchTopAssetsThreshold: - AppConstants.SWAPS.CACHE_TOP_ASSETS_THRESHOLD, - supportedChainIds: [ - swapsUtils.ETH_CHAIN_ID, - swapsUtils.BSC_CHAIN_ID, - swapsUtils.SWAPS_TESTNET_CHAIN_ID, - swapsUtils.POLYGON_CHAIN_ID, - swapsUtils.AVALANCHE_CHAIN_ID, - swapsUtils.ARBITRUM_CHAIN_ID, - swapsUtils.OPTIMISM_CHAIN_ID, - swapsUtils.ZKSYNC_ERA_CHAIN_ID, - swapsUtils.LINEA_CHAIN_ID, - swapsUtils.BASE_CHAIN_ID, - ], - }, - ), - gasFeeController, - approvalController, - permissionController, - selectedNetworkController, - new SignatureController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'SignatureController', - allowedActions: [ - `${approvalController.name}:addRequest`, - `${this.keyringController.name}:signPersonalMessage`, - `${this.keyringController.name}:signMessage`, - `${this.keyringController.name}:signTypedMessage`, - `${loggingController.name}:add`, - `${networkController.name}:getNetworkClientById`, - ], - allowedEvents: [], - }), - // This casting expected due to mismatch of browser and react-native version of Sentry traceContext - trace: trace as unknown as SignatureControllerOptions['trace'], - }), - loggingController, - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - this.snapController, - this.subjectMetadataController, - authenticationController, - userStorageController, - notificationServicesController, - notificationServicesPushController, - ///: END:ONLY_INCLUDE_IF - accountsController, - new PPOMController({ - chainId: networkController.getNetworkClientById( - networkController?.state.selectedNetworkClientId, - ).configuration.chainId, - blockaidPublicKey: process.env.BLOCKAID_PUBLIC_KEY as string, - cdnBaseUrl: process.env.BLOCKAID_FILE_CDN as string, - messenger: this.controllerMessenger.getRestricted({ - name: 'PPOMController', - allowedActions: ['NetworkController:getNetworkClientById'], - allowedEvents: [`${networkController.name}:networkDidChange`], - }), - onPreferencesChange: (listener) => - this.controllerMessenger.subscribe( - `${preferencesController.name}:stateChange`, - listener, - ), - // TODO: Replace "any" with type - provider: - // eslint-disable-next-line @typescript-eslint/no-explicit-any - networkController.getProviderAndBlockTracker().provider as any, - ppomProvider: { - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - PPOM: PPOM as any, - ppomInit, - }, - storageBackend: new RNFSStorageBackend('PPOMDB'), - securityAlertsEnabled: - initialState.PreferencesController?.securityAlertsEnabled ?? false, - state: initialState.PPOMController, - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - nativeCrypto: Crypto as any, - }), - ]; - - // set initial state - // TODO: Pass initial state into each controller constructor instead - // This is being set post-construction for now to ensure it's functionally equivalent with - // how the `ComponsedController` used to set initial state. - // - // The check for `controller.subscribe !== undefined` is to filter out BaseControllerV2 - // controllers. They should be initialized via the constructor instead. - for (const controller of controllers) { - if ( - hasProperty(initialState, controller.name) && - // Use `in` operator here because the `subscribe` function is one level up the prototype chain - 'subscribe' in controller && - controller.subscribe !== undefined - ) { - // The following type error can be addressed by passing initial state into controller constructors instead - // @ts-expect-error No type-level guarantee that the correct state is being applied to the correct controller here. - controller.update(initialState[controller.name]); - } - } - - this.datamodel = new ComposableController( - // @ts-expect-error The ComposableController needs to be updated to support BaseControllerV2 - controllers, - this.controllerMessenger, - ); - this.context = controllers.reduce>( - (context, controller) => ({ - ...context, - [controller.name]: controller, - }), - {}, - ) as typeof this.context; - - const { NftController: nfts } = this.context; - - if (process.env.MM_OPENSEA_KEY) { - nfts.setApiKey(process.env.MM_OPENSEA_KEY); - } - - this.controllerMessenger.subscribe( - 'TransactionController:incomingTransactionBlockReceived', - (blockNumber: number) => { - NotificationManager.gotIncomingTransaction(blockNumber); - }, - ); - - this.controllerMessenger.subscribe( - AppConstants.NETWORK_STATE_CHANGE_EVENT, - (state: NetworkState) => { - if ( - state.networksMetadata[state.selectedNetworkClientId].status === - NetworkStatus.Available && - networkController.getNetworkClientById( - networkController?.state.selectedNetworkClientId, - ).configuration.chainId !== currentChainId - ) { - // We should add a state or event emitter saying the provider changed - setTimeout(() => { - this.configureControllersOnNetworkChange(); - currentChainId = networkController.getNetworkClientById( - networkController?.state.selectedNetworkClientId, - ).configuration.chainId; - }, 500); - } - }, - ); - - this.controllerMessenger.subscribe( - AppConstants.NETWORK_STATE_CHANGE_EVENT, - async () => { - try { - const networkId = await deprecatedGetNetworkId(); - store.dispatch(networkIdUpdated(networkId)); - } catch (error) { - console.error( - error, - `Network ID not changed, current chainId: ${ - networkController.getNetworkClientById( - networkController?.state.selectedNetworkClientId, - ).configuration.chainId - }`, - ); - } - }, - ); - - this.controllerMessenger.subscribe( - `${networkController.name}:networkWillChange`, - () => { - store.dispatch(networkIdWillUpdate()); - }, - ); - - this.configureControllersOnNetworkChange(); - this.startPolling(); - this.handleVaultBackup(); - this._addTransactionControllerListeners(); - - Engine.instance = this; - } - - // Logs the "Transaction Finalized" event after a transaction was either confirmed, dropped or failed. - _handleTransactionFinalizedEvent = async ( - transactionEventPayload: TransactionEventPayload, - properties: JsonMap, - ) => { - const shouldUseSmartTransaction = selectShouldUseSmartTransaction( - store.getState(), - ); - if ( - !shouldUseSmartTransaction || - !transactionEventPayload.transactionMeta - ) { - MetaMetrics.getInstance().trackEvent( - MetricsEventBuilder.createEventBuilder( - MetaMetricsEvents.TRANSACTION_FINALIZED, - ) - .addProperties(properties) - .build(), - ); - return; - } - const { transactionMeta } = transactionEventPayload; - const { SmartTransactionsController } = this.context; - const waitForSmartTransaction = true; - const smartTransactionMetricsProperties = - await getSmartTransactionMetricsProperties( - SmartTransactionsController, - transactionMeta, - waitForSmartTransaction, - this.controllerMessenger, - ); - MetaMetrics.getInstance().trackEvent( - MetricsEventBuilder.createEventBuilder( - MetaMetricsEvents.TRANSACTION_FINALIZED, - ) - .addProperties(smartTransactionMetricsProperties) - .addProperties(properties) - .build(), - ); - }; - - _handleTransactionDropped = async ( - transactionEventPayload: TransactionEventPayload, - ) => { - const properties = { status: 'dropped' }; - await this._handleTransactionFinalizedEvent( - transactionEventPayload, - properties, - ); - }; - - _handleTransactionConfirmed = async (transactionMeta: TransactionMeta) => { - const properties = { status: 'confirmed' }; - await this._handleTransactionFinalizedEvent( - { transactionMeta }, - properties, - ); - }; - - _handleTransactionFailed = async ( - transactionEventPayload: TransactionEventPayload, - ) => { - const properties = { status: 'failed' }; - await this._handleTransactionFinalizedEvent( - transactionEventPayload, - properties, - ); - }; - - _addTransactionControllerListeners() { - this.controllerMessenger.subscribe( - 'TransactionController:transactionDropped', - this._handleTransactionDropped, - ); - - this.controllerMessenger.subscribe( - 'TransactionController:transactionConfirmed', - this._handleTransactionConfirmed, - ); - - this.controllerMessenger.subscribe( - 'TransactionController:transactionFailed', - this._handleTransactionFailed, - ); - } - - handleVaultBackup() { - this.controllerMessenger.subscribe( - AppConstants.KEYRING_STATE_CHANGE_EVENT, - (state: KeyringControllerState) => - backupVault(state) - .then((result) => { - if (result.success) { - Logger.log('Engine', 'Vault back up successful'); - } else { - Logger.log('Engine', 'Vault backup failed', result.error); - } - }) - .catch((error) => { - Logger.error(error, 'Engine Vault backup failed'); - }), - ); - } - - startPolling() { - const { - TokenDetectionController, - TokenListController, - TransactionController, - TokenRatesController, - } = this.context; - - TokenListController.start(); - TokenDetectionController.start(); - // leaving the reference of TransactionController here, rather than importing it from utils to avoid circular dependency - TransactionController.startIncomingTransactionPolling(); - TokenRatesController.start(); - } - - configureControllersOnNetworkChange() { - const { AccountTrackerController, NetworkController, SwapsController } = - this.context; - const { provider } = NetworkController.getProviderAndBlockTracker(); - - // Skip configuration if this is called before the provider is initialized - if (!provider) { - return; - } - provider.sendAsync = provider.sendAsync.bind(provider); - - SwapsController.configure({ - provider, - chainId: NetworkController.getNetworkClientById( - NetworkController?.state.selectedNetworkClientId, - ).configuration.chainId, - pollCountLimit: AppConstants.SWAPS.POLL_COUNT_LIMIT, - }); - AccountTrackerController.refresh(); - } - - getTotalFiatAccountBalance = (): { - ethFiat: number; - tokenFiat: number; - tokenFiat1dAgo: number; - ethFiat1dAgo: number; - } => { - const { - CurrencyRateController, - AccountsController, - AccountTrackerController, - TokenBalancesController, - TokenRatesController, - TokensController, - NetworkController, - } = this.context; - - const selectedInternalAccount = AccountsController.getAccount( - AccountsController.state.internalAccounts.selectedAccount, - ); - - if (selectedInternalAccount) { - const selectSelectedInternalAccountChecksummedAddress = - toChecksumHexAddress(selectedInternalAccount.address); - const { currentCurrency } = CurrencyRateController.state; - const { chainId, ticker } = NetworkController.getNetworkClientById( - NetworkController?.state.selectedNetworkClientId, - ).configuration; - const { settings: { showFiatOnTestnets } = {} } = store.getState(); - - if (isTestNet(chainId) && !showFiatOnTestnets) { - return { ethFiat: 0, tokenFiat: 0, ethFiat1dAgo: 0, tokenFiat1dAgo: 0 }; - } - - const conversionRate = - CurrencyRateController.state?.currencyRates?.[ticker]?.conversionRate ?? - 0; - - const { accountsByChainId } = AccountTrackerController.state; - const { tokens } = TokensController.state; - const { marketData } = TokenRatesController.state; - const tokenExchangeRates = marketData?.[toHexadecimal(chainId)]; - - let ethFiat = 0; - let ethFiat1dAgo = 0; - let tokenFiat = 0; - let tokenFiat1dAgo = 0; - const decimalsToShow = (currentCurrency === 'usd' && 2) || undefined; - if ( - accountsByChainId?.[toHexadecimal(chainId)]?.[ - selectSelectedInternalAccountChecksummedAddress - ] - ) { - const balanceBN = hexToBN( - accountsByChainId[toHexadecimal(chainId)][ - selectSelectedInternalAccountChecksummedAddress - ].balance, - ); - const stakedBalanceBN = hexToBN( - accountsByChainId[toHexadecimal(chainId)][ - selectSelectedInternalAccountChecksummedAddress - ].stakedBalance || '0x00', - ); - const totalAccountBalance = balanceBN - .add(stakedBalanceBN) - .toString('hex'); - ethFiat = weiToFiatNumber( - totalAccountBalance, - conversionRate, - decimalsToShow, - ); - } - - const ethPricePercentChange1d = - tokenExchangeRates?.[zeroAddress() as Hex]?.pricePercentChange1d; - - ethFiat1dAgo = - ethPricePercentChange1d !== undefined - ? ethFiat / (1 + ethPricePercentChange1d / 100) - : ethFiat; - - if (tokens.length > 0) { - const { contractBalances: tokenBalances } = - TokenBalancesController.state; - tokens.forEach( - (item: { address: string; balance?: string; decimals: number }) => { - const exchangeRate = - tokenExchangeRates?.[item.address as Hex]?.price; - - const tokenBalance = - item.balance || - (item.address in tokenBalances - ? renderFromTokenMinimalUnit( - tokenBalances[item.address], - item.decimals, - ) - : undefined); - const tokenBalanceFiat = balanceToFiatNumber( - // TODO: Fix this by handling or eliminating the undefined case - // @ts-expect-error This variable can be `undefined`, which would break here. - tokenBalance, - conversionRate, - exchangeRate, - decimalsToShow, - ); - - const tokenPricePercentChange1d = - tokenExchangeRates?.[item.address as Hex]?.pricePercentChange1d; - - const tokenBalance1dAgo = - tokenPricePercentChange1d !== undefined - ? tokenBalanceFiat / (1 + tokenPricePercentChange1d / 100) - : tokenBalanceFiat; - - tokenFiat += tokenBalanceFiat; - tokenFiat1dAgo += tokenBalance1dAgo; - }, - ); - } - - return { - ethFiat: ethFiat ?? 0, - ethFiat1dAgo: ethFiat1dAgo ?? 0, - tokenFiat: tokenFiat ?? 0, - tokenFiat1dAgo: tokenFiat1dAgo ?? 0, - }; - } - // if selectedInternalAccount is undefined, return default 0 value. - return { - ethFiat: 0, - tokenFiat: 0, - ethFiat1dAgo: 0, - tokenFiat1dAgo: 0, - }; - }; - - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - getSnapKeyring = async () => { - let [snapKeyring] = this.keyringController.getKeyringsByType( - KeyringTypes.snap, - ); - if (!snapKeyring) { - snapKeyring = await this.keyringController.addNewKeyring( - KeyringTypes.snap, - ); - } - return snapKeyring; - }; - - /** - * Removes an account from state / storage. - * - * @param {string} address - A hex address - */ - removeAccount = async (address: string) => { - // Remove all associated permissions - await removeAccountsFromPermissions([address]); - // Remove account from the keyring - await this.keyringController.removeAccount(address as Hex); - return address; - }; - ///: END:ONLY_INCLUDE_IF - - /** - * Returns true or false whether the user has funds or not - */ - hasFunds = () => { - try { - const { - engine: { backgroundState }, - } = store.getState(); - // TODO: Check `allNfts[currentChainId]` property instead - // @ts-expect-error This property does not exist - const nfts = backgroundState.NftController.nfts; - const tokens = backgroundState.TokensController.tokens; - const tokenBalances = - backgroundState.TokenBalancesController.contractBalances; - - let tokenFound = false; - tokens.forEach((token: { address: string | number }) => { - if ( - tokenBalances[token.address] && - !isZero(tokenBalances[token.address]) - ) { - tokenFound = true; - } - }); - - const fiatBalance = this.getTotalFiatAccountBalance() || 0; - const totalFiatBalance = fiatBalance.ethFiat + fiatBalance.ethFiat; - - return totalFiatBalance > 0 || tokenFound || nfts.length > 0; - } catch (e) { - Logger.log('Error while getting user funds', e); - } - }; - - resetState = async () => { - // Whenever we are gonna start a new wallet - // either imported or created, we need to - // get rid of the old data from state - const { - TransactionController, - TokensController, - NftController, - TokenBalancesController, - TokenRatesController, - PermissionController, - // SelectedNetworkController, - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - SnapController, - ///: END:ONLY_INCLUDE_IF - LoggingController, - } = this.context; - - // Remove all permissions. - PermissionController?.clearState?.(); - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - SnapController.clearState(); - ///: END:ONLY_INCLUDE_IF - - // Clear selected network - // TODO implement this method on SelectedNetworkController - // SelectedNetworkController.unsetAllDomains() - - //Clear assets info - TokensController.reset(); - NftController.reset(); - - TokenBalancesController.reset(); - TokenRatesController.reset(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (TransactionController as any).update(() => ({ - methodData: {}, - transactions: [], - lastFetchedBlockNumbers: {}, - submitHistory: [], - swapsTransactions: {}, - })); - - LoggingController.clear(); - }; - - removeAllListeners() { - this.controllerMessenger.clearSubscriptions(); - } - - async destroyEngineInstance() { - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Object.values(this.context).forEach((controller: any) => { - if (controller.destroy) { - controller.destroy(); - } - }); - this.removeAllListeners(); - await this.resetState(); - Engine.instance = null; - } - - rejectPendingApproval( - id: string, - reason: Error = providerErrors.userRejectedRequest(), - opts: { ignoreMissing?: boolean; logErrors?: boolean } = {}, - ) { - const { ApprovalController } = this.context; - - if (opts.ignoreMissing && !ApprovalController.has({ id })) { - return; - } - - try { - ApprovalController.reject(id, reason); - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - if (opts.logErrors !== false) { - Logger.error( - error, - 'Reject while rejecting pending connection request', - ); - } - } - } - - async acceptPendingApproval( - id: string, - requestData?: Record, - opts: AcceptOptions & { handleErrors?: boolean } = { - waitForResult: false, - deleteAfterResult: false, - handleErrors: true, - }, - ) { - const { ApprovalController } = this.context; - - try { - return await ApprovalController.accept(id, requestData, { - waitForResult: opts.waitForResult, - deleteAfterResult: opts.deleteAfterResult, - }); - } catch (err) { - if (opts.handleErrors === false) { - throw err; - } - } - } - - // This should be used instead of directly calling PreferencesController.setSelectedAddress or AccountsController.setSelectedAccount - setSelectedAccount(address: string) { - const { AccountsController, PreferencesController } = this.context; - const account = AccountsController.getAccountByAddress(address); - if (account) { - AccountsController.setSelectedAccount(account.id); - PreferencesController.setSelectedAddress(address); - } else { - throw new Error(`No account found for address: ${address}`); - } - } - - /** - * This should be used instead of directly calling PreferencesController.setAccountLabel or AccountsController.setAccountName in order to keep the names in sync - * We are currently incrementally migrating the accounts data to the AccountsController so we must keep these values - * in sync until the migration is complete. - */ - setAccountLabel(address: string, label: string) { - const { AccountsController, PreferencesController } = this.context; - const accountToBeNamed = AccountsController.getAccountByAddress(address); - if (accountToBeNamed === undefined) { - throw new Error(`No account found for address: ${address}`); - } - AccountsController.setAccountName(accountToBeNamed.id, label); - PreferencesController.setAccountLabel(address, label); - } - - getGlobalEthQuery(): EthQuery { - const { NetworkController } = this.context; - const { provider } = NetworkController.getSelectedNetworkClient() ?? {}; - - if (!provider) { - throw new Error('No selected network client'); - } - - return new EthQuery(provider); - } -} - -/** - * Assert that the given Engine instance has been initialized - * - * @param instance - Either an Engine instance, or null - */ -function assertEngineExists( - instance: Engine | null, -): asserts instance is Engine { - if (!instance) { - throw new Error('Engine does not exist'); - } -} - -let instance: Engine | null; - -export default { - get context() { - assertEngineExists(instance); - return instance.context; - }, - - get controllerMessenger() { - assertEngineExists(instance); - return instance.controllerMessenger; - }, - - get state() { - assertEngineExists(instance); - const { - AccountTrackerController, - AddressBookController, - AssetsContractController, - NftController, - TokenListController, - CurrencyRateController, - KeyringController, - NetworkController, - PreferencesController, - PhishingController, - PPOMController, - TokenBalancesController, - TokenRatesController, - TransactionController, - SmartTransactionsController, - SwapsController, - GasFeeController, - TokensController, - TokenDetectionController, - NftDetectionController, - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - SnapController, - SubjectMetadataController, - AuthenticationController, - UserStorageController, - NotificationServicesController, - NotificationServicesPushController, - ///: END:ONLY_INCLUDE_IF - PermissionController, - SelectedNetworkController, - ApprovalController, - LoggingController, - AccountsController, - } = instance.datamodel.state; - - // normalize `null` currencyRate to `0` - // TODO: handle `null` currencyRate by hiding fiat values instead - const modifiedCurrencyRateControllerState = { - ...CurrencyRateController, - conversionRate: - CurrencyRateController.conversionRate === null - ? 0 - : CurrencyRateController.conversionRate, - }; - - return { - AccountTrackerController, - AddressBookController, - AssetsContractController, - NftController, - TokenListController, - CurrencyRateController: modifiedCurrencyRateControllerState, - KeyringController, - NetworkController, - PhishingController, - PPOMController, - PreferencesController, - TokenBalancesController, - TokenRatesController, - TokensController, - TransactionController, - SmartTransactionsController, - SwapsController, - GasFeeController, - TokenDetectionController, - NftDetectionController, - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - SnapController, - SubjectMetadataController, - AuthenticationController, - UserStorageController, - NotificationServicesController, - NotificationServicesPushController, - ///: END:ONLY_INCLUDE_IF - PermissionController, - SelectedNetworkController, - ApprovalController, - LoggingController, - AccountsController, - }; - }, - - get datamodel() { - assertEngineExists(instance); - return instance.datamodel; - }, - - getTotalFiatAccountBalance() { - assertEngineExists(instance); - return instance.getTotalFiatAccountBalance(); - }, - - hasFunds() { - assertEngineExists(instance); - return instance.hasFunds(); - }, - - resetState() { - assertEngineExists(instance); - return instance.resetState(); - }, - - destroyEngine() { - instance?.destroyEngineInstance(); - instance = null; - }, - - init(state: Partial | undefined, keyringState = null) { - instance = Engine.instance || new Engine(state, keyringState); - Object.freeze(instance); - return instance; - }, - - acceptPendingApproval: async ( - id: string, - requestData?: Record, - opts?: AcceptOptions & { handleErrors?: boolean }, - ) => instance?.acceptPendingApproval(id, requestData, opts), - - rejectPendingApproval: ( - id: string, - reason: Error, - opts: { - ignoreMissing?: boolean; - logErrors?: boolean; - } = {}, - ) => instance?.rejectPendingApproval(id, reason, opts), - - setSelectedAddress: (address: string) => { - assertEngineExists(instance); - instance.setSelectedAccount(address); - }, - - setAccountLabel: (address: string, label: string) => { - assertEngineExists(instance); - instance.setAccountLabel(address, label); - }, - - getGlobalEthQuery: (): EthQuery => { - assertEngineExists(instance); - return instance.getGlobalEthQuery(); - }, - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - getSnapKeyring: () => { - assertEngineExists(instance); - return instance.getSnapKeyring(); - }, - removeAccount: async (address: string) => { - assertEngineExists(instance); - return await instance.removeAccount(address); - }, - ///: END:ONLY_INCLUDE_IF -}; +export { default } from './Engine'; +export * from './Engine.types'; diff --git a/app/core/Engine/types.ts b/app/core/Engine/types.ts deleted file mode 100644 index d66d2237f65..00000000000 --- a/app/core/Engine/types.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { ExtendedControllerMessenger } from '../ExtendedControllerMessenger'; -import { - CurrencyRateStateChange, - GetCurrencyRateState, - GetTokenListState, - TokenListStateChange, - TokensControllerActions, - TokensControllerEvents, - TokenListControllerActions, - TokenListControllerEvents, - AssetsContractControllerGetERC20BalanceOfAction, - AssetsContractControllerGetERC721AssetNameAction, - AssetsContractControllerGetERC721AssetSymbolAction, - AssetsContractControllerGetERC721TokenURIAction, - AssetsContractControllerGetERC721OwnerOfAction, - AssetsContractControllerGetERC1155BalanceOfAction, - AssetsContractControllerGetERC1155TokenURIAction, -} from '@metamask/assets-controllers'; -import { - AddressBookControllerActions, - AddressBookControllerEvents, -} from '@metamask/address-book-controller'; -import { - KeyringControllerActions, - KeyringControllerEvents, -} from '@metamask/keyring-controller'; -import { - NetworkControllerActions, - NetworkControllerEvents, -} from '@metamask/network-controller'; -import { - PhishingControllerActions, - PhishingControllerEvents, -} from '@metamask/phishing-controller'; -import { - PreferencesControllerActions, - PreferencesControllerEvents, -} from '@metamask/preferences-controller'; -import { TransactionControllerEvents } from '@metamask/transaction-controller'; -import { - GasFeeStateChange, - GetGasFeeState, -} from '@metamask/gas-fee-controller'; -import { - ApprovalControllerActions, - ApprovalControllerEvents, -} from '@metamask/approval-controller'; -import { - SelectedNetworkControllerEvents, - SelectedNetworkControllerActions, -} from '@metamask/selected-network-controller'; -import { - PermissionControllerActions, - PermissionControllerEvents, - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - SubjectMetadataControllerActions, - SubjectMetadataControllerEvents, - ///: END:ONLY_INCLUDE_IF -} from '@metamask/permission-controller'; -import { - PPOMControllerActions, - PPOMControllerEvents, -} from '@metamask/ppom-validator'; -///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) -import { - AllowedActions as SnapsAllowedActions, - AllowedEvents as SnapsAllowedEvents, - SnapControllerEvents, - SnapControllerActions, -} from '@metamask/snaps-controllers'; -///: END:ONLY_INCLUDE_IF -import { - LoggingControllerActions, - LoggingControllerEvents, -} from '@metamask/logging-controller'; -import { - SignatureControllerActions, - SignatureControllerEvents, -} from '@metamask/signature-controller'; -import { - type SmartTransactionsControllerActions, - type SmartTransactionsControllerEvents, -} from '@metamask/smart-transactions-controller'; -///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) -import { - AuthenticationController, - UserStorageController, -} from '@metamask/profile-sync-controller'; -import { NotificationServicesController } from '@metamask/notification-services-controller'; -///: END:ONLY_INCLUDE_IF -import { - AccountsControllerActions, - AccountsControllerEvents, -} from './controllers/AccountsControllerService'; - -///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) -type AuthenticationControllerActions = AuthenticationController.AllowedActions; -type UserStorageControllerActions = UserStorageController.AllowedActions; -type NotificationsServicesControllerActions = - NotificationServicesController.AllowedActions; - -type SnapsGlobalActions = - | SnapControllerActions - | SubjectMetadataControllerActions - | PhishingControllerActions - | SnapsAllowedActions; - -type SnapsGlobalEvents = - | SnapControllerEvents - | SubjectMetadataControllerEvents - | PhishingControllerEvents - | SnapsAllowedEvents; -///: END:ONLY_INCLUDE_IF - -type GlobalActions = - | AddressBookControllerActions - | ApprovalControllerActions - | GetCurrencyRateState - | GetGasFeeState - | GetTokenListState - | KeyringControllerActions - | NetworkControllerActions - | PermissionControllerActions - | SignatureControllerActions - | LoggingControllerActions - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - | SnapsGlobalActions - | AuthenticationControllerActions - | UserStorageControllerActions - | NotificationsServicesControllerActions - ///: END:ONLY_INCLUDE_IF - | KeyringControllerActions - | AccountsControllerActions - | PreferencesControllerActions - | PPOMControllerActions - | TokensControllerActions - | TokenListControllerActions - | SelectedNetworkControllerActions - | SmartTransactionsControllerActions - | AssetsContractControllerGetERC20BalanceOfAction - | AssetsContractControllerGetERC721AssetNameAction - | AssetsContractControllerGetERC721AssetSymbolAction - | AssetsContractControllerGetERC721TokenURIAction - | AssetsContractControllerGetERC721OwnerOfAction - | AssetsContractControllerGetERC1155BalanceOfAction - | AssetsContractControllerGetERC1155TokenURIAction; - -type GlobalEvents = - | AddressBookControllerEvents - | ApprovalControllerEvents - | CurrencyRateStateChange - | GasFeeStateChange - | KeyringControllerEvents - | TokenListStateChange - | NetworkControllerEvents - | PermissionControllerEvents - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - | SnapsGlobalEvents - ///: END:ONLY_INCLUDE_IF - | SignatureControllerEvents - | LoggingControllerEvents - | KeyringControllerEvents - | PPOMControllerEvents - | AccountsControllerEvents - | PreferencesControllerEvents - | TokensControllerEvents - | TokenListControllerEvents - | TransactionControllerEvents - | SelectedNetworkControllerEvents - | SmartTransactionsControllerEvents; - -/** - * Type definition for the controller messenger used in the Engine. - * It extends the base ControllerMessenger with global actions and events. - */ -export type ControllerMessenger = ExtendedControllerMessenger< - GlobalActions, - GlobalEvents ->; From 0e5322be325926230f29f35424696026fe2d1ea8 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Wed, 20 Nov 2024 20:29:50 -0800 Subject: [PATCH 04/20] Create accounts controller utils --- .../controllers/accountControllerUtils.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 app/core/Engine/controllers/accountControllerUtils.ts diff --git a/app/core/Engine/controllers/accountControllerUtils.ts b/app/core/Engine/controllers/accountControllerUtils.ts new file mode 100644 index 00000000000..332c4932216 --- /dev/null +++ b/app/core/Engine/controllers/accountControllerUtils.ts @@ -0,0 +1,87 @@ +import { + AccountsController, + AccountsControllerMessenger, + AccountsControllerState, + AccountsControllerGetAccountByAddressAction as AccountsControllerGetAccountByAddressActionType, + AccountsControllerSetAccountNameAction as AccountsControllerSetAccountNameActionType, + AccountsControllerSetSelectedAccountAction as AccountsControllerSetSelectedAccountActionType, + AccountsControllerGetAccountAction as AccountsControllerGetAccountActionType, + AccountsControllerGetSelectedAccountAction as AccountsControllerGetSelectedAccountActionType, + AccountsControllerListAccountsAction as AccountsControllerListAccountsActionType, + AccountsControllerUpdateAccountMetadataAction as AccountsControllerUpdateAccountMetadataActionType, + AccountsControllerSelectedEvmAccountChangeEvent as AccountsControllerSelectedEvmAccountChangeEventType, + AccountsControllerSelectedAccountChangeEvent as AccountsControllerSelectedAccountChangeEventType, + AccountsControllerAccountAddedEvent as AccountsControllerAccountAddedEventType, + AccountsControllerAccountRenamedEvent as AccountsControllerAccountRenamedEventType, +} from '@metamask/accounts-controller'; +import { ControllerMessenger } from '../Engine.types'; + +/** + * Creates instance of AccountsController + * + * @param options.controllerMessenger - Controller messenger instance + * @param options.initialState - Initial state of AccountsController + * @returns - AccountsController instance + */ +export const createAccountsController = ({ + controllerMessenger, + initialState, +}: { + controllerMessenger: ControllerMessenger; + initialState?: AccountsControllerState; +}) => { + const accountsControllerMessenger: AccountsControllerMessenger = + controllerMessenger.getRestricted({ + name: 'AccountsController', + allowedEvents: [ + 'SnapController:stateChange', + 'KeyringController:accountRemoved', + 'KeyringController:stateChange', + ], + allowedActions: [ + 'KeyringController:getAccounts', + 'KeyringController:getKeyringsByType', + 'KeyringController:getKeyringForAccount', + ], + }); + + const defaultAccountsControllerState: AccountsControllerState = { + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, + }; + + const accountsController = new AccountsController({ + messenger: accountsControllerMessenger, + state: initialState ?? defaultAccountsControllerState, + }); + + return accountsController; +}; + +// Action types of AccountsController +export const AccountsControllerGetAccountByAddressAction: AccountsControllerGetAccountByAddressActionType['type'] = + 'AccountsController:getAccountByAddress'; +export const AccountsControllerSetAccountNameAction: AccountsControllerSetAccountNameActionType['type'] = + 'AccountsController:setAccountName'; +export const AccountsControllerGetAccountAction: AccountsControllerGetAccountActionType['type'] = + 'AccountsController:getAccount'; +export const AccountsControllerGetSelectedAccountAction: AccountsControllerGetSelectedAccountActionType['type'] = + 'AccountsController:getSelectedAccount'; +export const AccountsControllerSetSelectedAccountAction: AccountsControllerSetSelectedAccountActionType['type'] = + 'AccountsController:setSelectedAccount'; +export const AccountsControllerListAccountsAction: AccountsControllerListAccountsActionType['type'] = + 'AccountsController:listAccounts'; +export const AccountsControllerUpdateAccountMetadataAction: AccountsControllerUpdateAccountMetadataActionType['type'] = + 'AccountsController:updateAccountMetadata'; + +// Events of AccountsController +export const AccountsControllerSelectedEvmAccountChangeEvent: AccountsControllerSelectedEvmAccountChangeEventType['type'] = + 'AccountsController:selectedEvmAccountChange'; +export const AccountsControllerSelectedAccountChangeEvent: AccountsControllerSelectedAccountChangeEventType['type'] = + 'AccountsController:selectedAccountChange'; +export const AccountsControllerAccountAddedEvent: AccountsControllerAccountAddedEventType['type'] = + 'AccountsController:accountAdded'; +export const AccountsControllerAccountRenamedEvent: AccountsControllerAccountRenamedEventType['type'] = + 'AccountsController:accountRenamed'; From fa637e0994172ff66d4e53f3b4b2254f768c1ca7 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Wed, 20 Nov 2024 21:10:14 -0800 Subject: [PATCH 05/20] Create unit test for account controller utils --- .../accountControllerUtils.test.ts | 40 +++++++++++++++++++ .../controllers/accountControllerUtils.ts | 23 ++++++----- 2 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 app/core/Engine/controllers/accountControllerUtils.test.ts diff --git a/app/core/Engine/controllers/accountControllerUtils.test.ts b/app/core/Engine/controllers/accountControllerUtils.test.ts new file mode 100644 index 00000000000..a83423dfaed --- /dev/null +++ b/app/core/Engine/controllers/accountControllerUtils.test.ts @@ -0,0 +1,40 @@ +import { AccountsControllerState } from '@metamask/accounts-controller'; +import { ExtendedControllerMessenger } from '../../ExtendedControllerMessenger'; +import { + createAccountsController, + defaultAccountsControllerState, +} from './accountControllerUtils'; + +describe('accountControllerUtils', () => { + describe('createAccountsController', () => { + it('AccountsController state should be default state when no initial state is passed in', () => { + const controllerMessenger = new ExtendedControllerMessenger(); + const accountsController = createAccountsController({ + messenger: controllerMessenger, + }); + expect(accountsController.state).toEqual(defaultAccountsControllerState); + }); + it('AccountsController state should be initial state when initial state is passed in', () => { + const controllerMessenger = new ExtendedControllerMessenger(); + const initialAccountsControllerState: AccountsControllerState = { + internalAccounts: { + accounts: {}, + selectedAccount: '0x1', + }, + }; + const accountsController = createAccountsController({ + messenger: controllerMessenger, + initialState: initialAccountsControllerState, + }); + expect(accountsController.state).toEqual(initialAccountsControllerState); + }); + it('AccountsController name should be AccountsController', () => { + const controllerMessenger = new ExtendedControllerMessenger(); + const accountsControllerName = 'AccountsController'; + const accountsController = createAccountsController({ + messenger: controllerMessenger, + }); + expect(accountsController.name).toEqual(accountsControllerName); + }); + }); +}); diff --git a/app/core/Engine/controllers/accountControllerUtils.ts b/app/core/Engine/controllers/accountControllerUtils.ts index 332c4932216..f2d4ee7bbcf 100644 --- a/app/core/Engine/controllers/accountControllerUtils.ts +++ b/app/core/Engine/controllers/accountControllerUtils.ts @@ -16,22 +16,30 @@ import { } from '@metamask/accounts-controller'; import { ControllerMessenger } from '../Engine.types'; +// Default AccountsControllerState +export const defaultAccountsControllerState: AccountsControllerState = { + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, +}; + /** * Creates instance of AccountsController * - * @param options.controllerMessenger - Controller messenger instance + * @param options.messenger - Controller messenger instance * @param options.initialState - Initial state of AccountsController * @returns - AccountsController instance */ export const createAccountsController = ({ - controllerMessenger, + messenger, initialState, }: { - controllerMessenger: ControllerMessenger; + messenger: ControllerMessenger; initialState?: AccountsControllerState; }) => { const accountsControllerMessenger: AccountsControllerMessenger = - controllerMessenger.getRestricted({ + messenger.getRestricted({ name: 'AccountsController', allowedEvents: [ 'SnapController:stateChange', @@ -45,13 +53,6 @@ export const createAccountsController = ({ ], }); - const defaultAccountsControllerState: AccountsControllerState = { - internalAccounts: { - accounts: {}, - selectedAccount: '', - }, - }; - const accountsController = new AccountsController({ messenger: accountsControllerMessenger, state: initialState ?? defaultAccountsControllerState, From c0e6dae763f458703e214db066db4639f74c93a6 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Wed, 20 Nov 2024 21:10:29 -0800 Subject: [PATCH 06/20] Fix messenger naming --- app/core/Engine/Engine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/Engine/Engine.ts b/app/core/Engine/Engine.ts index e3a95b77e9e..094760ae704 100644 --- a/app/core/Engine/Engine.ts +++ b/app/core/Engine/Engine.ts @@ -355,7 +355,7 @@ export class Engine { // Create AccountsController const accountsController = createAccountsController({ - controllerMessenger: this.controllerMessenger, + messenger: this.controllerMessenger, initialState: initialState.AccountsController, }); From 994c0a5e8096882dc1d5873b0bed48a865c50787 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 21 Nov 2024 01:13:55 -0800 Subject: [PATCH 07/20] Report controller initialization failure to Sentry --- .../controllers/accountControllerUtils.ts | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/app/core/Engine/controllers/accountControllerUtils.ts b/app/core/Engine/controllers/accountControllerUtils.ts index f2d4ee7bbcf..8523a1beeac 100644 --- a/app/core/Engine/controllers/accountControllerUtils.ts +++ b/app/core/Engine/controllers/accountControllerUtils.ts @@ -15,6 +15,7 @@ import { AccountsControllerAccountRenamedEvent as AccountsControllerAccountRenamedEventType, } from '@metamask/accounts-controller'; import { ControllerMessenger } from '../Engine.types'; +import Logger from '../../../util/Logger'; // Default AccountsControllerState export const defaultAccountsControllerState: AccountsControllerState = { @@ -38,27 +39,33 @@ export const createAccountsController = ({ messenger: ControllerMessenger; initialState?: AccountsControllerState; }) => { - const accountsControllerMessenger: AccountsControllerMessenger = - messenger.getRestricted({ - name: 'AccountsController', - allowedEvents: [ - 'SnapController:stateChange', - 'KeyringController:accountRemoved', - 'KeyringController:stateChange', - ], - allowedActions: [ - 'KeyringController:getAccounts', - 'KeyringController:getKeyringsByType', - 'KeyringController:getKeyringForAccount', - ], - }); + try { + const accountsControllerMessenger: AccountsControllerMessenger = + messenger.getRestricted({ + name: 'AccountsController', + allowedEvents: [ + 'SnapController:stateChange', + 'KeyringController:accountRemoved', + 'KeyringController:stateChange', + ], + allowedActions: [ + 'KeyringController:getAccounts', + 'KeyringController:getKeyringsByType', + 'KeyringController:getKeyringForAccount', + ], + }); - const accountsController = new AccountsController({ - messenger: accountsControllerMessenger, - state: initialState ?? defaultAccountsControllerState, - }); + const accountsController = new AccountsController({ + messenger: accountsControllerMessenger, + state: initialState ?? defaultAccountsControllerState, + }); - return accountsController; + return accountsController; + } catch (error) { + // Report error while initializing AccountsController + // TODO: Direct to vault recovery to reset controller states + Logger.error(error as Error, 'Failed to initialize AccountsController'); + } }; // Action types of AccountsController From fa51123778e499d23613a94de5706bd484412330 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 21 Nov 2024 01:23:02 -0800 Subject: [PATCH 08/20] Ensure AccountsController return type --- app/core/Engine/controllers/accountControllerUtils.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/core/Engine/controllers/accountControllerUtils.ts b/app/core/Engine/controllers/accountControllerUtils.ts index 8523a1beeac..392c911c807 100644 --- a/app/core/Engine/controllers/accountControllerUtils.ts +++ b/app/core/Engine/controllers/accountControllerUtils.ts @@ -38,7 +38,9 @@ export const createAccountsController = ({ }: { messenger: ControllerMessenger; initialState?: AccountsControllerState; -}) => { +}): AccountsController => { + let accountsController = {} as AccountsController; + try { const accountsControllerMessenger: AccountsControllerMessenger = messenger.getRestricted({ @@ -55,17 +57,17 @@ export const createAccountsController = ({ ], }); - const accountsController = new AccountsController({ + accountsController = new AccountsController({ messenger: accountsControllerMessenger, state: initialState ?? defaultAccountsControllerState, }); - - return accountsController; } catch (error) { // Report error while initializing AccountsController // TODO: Direct to vault recovery to reset controller states Logger.error(error as Error, 'Failed to initialize AccountsController'); } + + return accountsController; }; // Action types of AccountsController From 8c0f7840efe760a103ebafbd5e26e7c88c88f49a Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 21 Nov 2024 01:27:55 -0800 Subject: [PATCH 09/20] Add missing s --- ...tControllerUtils.test.ts => accountsControllerUtils.test.ts} | 2 +- .../{accountControllerUtils.ts => accountsControllerUtils.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/core/Engine/controllers/{accountControllerUtils.test.ts => accountsControllerUtils.test.ts} (97%) rename app/core/Engine/controllers/{accountControllerUtils.ts => accountsControllerUtils.ts} (100%) diff --git a/app/core/Engine/controllers/accountControllerUtils.test.ts b/app/core/Engine/controllers/accountsControllerUtils.test.ts similarity index 97% rename from app/core/Engine/controllers/accountControllerUtils.test.ts rename to app/core/Engine/controllers/accountsControllerUtils.test.ts index a83423dfaed..1fddc89166d 100644 --- a/app/core/Engine/controllers/accountControllerUtils.test.ts +++ b/app/core/Engine/controllers/accountsControllerUtils.test.ts @@ -3,7 +3,7 @@ import { ExtendedControllerMessenger } from '../../ExtendedControllerMessenger'; import { createAccountsController, defaultAccountsControllerState, -} from './accountControllerUtils'; +} from './accountsControllerUtils'; describe('accountControllerUtils', () => { describe('createAccountsController', () => { diff --git a/app/core/Engine/controllers/accountControllerUtils.ts b/app/core/Engine/controllers/accountsControllerUtils.ts similarity index 100% rename from app/core/Engine/controllers/accountControllerUtils.ts rename to app/core/Engine/controllers/accountsControllerUtils.ts From 20305a75daa0bc54ba74ed251725d90ca9edda7c Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 21 Nov 2024 02:07:35 -0800 Subject: [PATCH 10/20] Add unit test for failed controller initialization --- .../accountsControllerUtils.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/app/core/Engine/controllers/accountsControllerUtils.test.ts b/app/core/Engine/controllers/accountsControllerUtils.test.ts index 1fddc89166d..be5eadd77b0 100644 --- a/app/core/Engine/controllers/accountsControllerUtils.test.ts +++ b/app/core/Engine/controllers/accountsControllerUtils.test.ts @@ -4,9 +4,33 @@ import { createAccountsController, defaultAccountsControllerState, } from './accountsControllerUtils'; +import { ControllerMessenger } from '../'; +import { withScope } from '@sentry/react-native'; +import { AGREED, METRICS_OPT_IN } from '../../../constants/storage'; +import StorageWrapper from '../../../store/storage-wrapper'; + +jest.mock('@sentry/react-native', () => ({ + withScope: jest.fn(), +})); +const mockedWithScope = jest.mocked(withScope); describe('accountControllerUtils', () => { describe('createAccountsController', () => { + beforeEach(() => { + StorageWrapper.getItem = jest.fn((key: string) => { + switch (key) { + case METRICS_OPT_IN: + return Promise.resolve(AGREED); + default: + return Promise.resolve(''); + } + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + it('AccountsController state should be default state when no initial state is passed in', () => { const controllerMessenger = new ExtendedControllerMessenger(); const accountsController = createAccountsController({ @@ -36,5 +60,14 @@ describe('accountControllerUtils', () => { }); expect(accountsController.name).toEqual(accountsControllerName); }); + it('should throw error when controller fails to initialize', async () => { + const controllerMessenger = + 'controllerMessenger' as unknown as ControllerMessenger; + const accountsController = await createAccountsController({ + messenger: controllerMessenger, + }); + expect(mockedWithScope).toHaveBeenCalledTimes(1); + expect(accountsController).toEqual({}); + }); }); }); From 2d6da2d66fe7bc4706f9895c81140f574b554bbd Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 21 Nov 2024 02:13:26 -0800 Subject: [PATCH 11/20] Clean up names --- app/core/Engine/Engine.ts | 2 +- app/core/Engine/controllers/accountsControllerUtils.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core/Engine/Engine.ts b/app/core/Engine/Engine.ts index 094760ae704..fe24c051589 100644 --- a/app/core/Engine/Engine.ts +++ b/app/core/Engine/Engine.ts @@ -153,7 +153,7 @@ import { AccountsControllerSelectedAccountChangeEvent, AccountsControllerAccountAddedEvent, AccountsControllerAccountRenamedEvent, -} from './controllers/accountControllerUtils'; +} from './controllers/accountsControllerUtils'; import { captureException } from '@sentry/react-native'; import { lowerCase } from 'lodash'; import { diff --git a/app/core/Engine/controllers/accountsControllerUtils.test.ts b/app/core/Engine/controllers/accountsControllerUtils.test.ts index be5eadd77b0..eba5c6999cb 100644 --- a/app/core/Engine/controllers/accountsControllerUtils.test.ts +++ b/app/core/Engine/controllers/accountsControllerUtils.test.ts @@ -14,7 +14,7 @@ jest.mock('@sentry/react-native', () => ({ })); const mockedWithScope = jest.mocked(withScope); -describe('accountControllerUtils', () => { +describe('accountControllersUtils', () => { describe('createAccountsController', () => { beforeEach(() => { StorageWrapper.getItem = jest.fn((key: string) => { From ad8684ec8c3eee09a23530f1acf469ab046c3660 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 21 Nov 2024 07:47:42 -0800 Subject: [PATCH 12/20] Update swaps controller state --- app/core/Engine/Engine.types.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/core/Engine/Engine.types.ts b/app/core/Engine/Engine.types.ts index 80d1f73a506..b9de998a56f 100644 --- a/app/core/Engine/Engine.types.ts +++ b/app/core/Engine/Engine.types.ts @@ -98,8 +98,9 @@ import { SubjectMetadataControllerState, ///: END:ONLY_INCLUDE_IF } from '@metamask/permission-controller'; -import SwapsController from '@metamask/swaps-controller'; -import { SwapsState } from '@metamask/swaps-controller/dist/SwapsController'; +import SwapsController, { + SwapsControllerState, +} from '@metamask/swaps-controller'; import { PPOMController, PPOMControllerActions, @@ -298,7 +299,7 @@ export interface EngineState { TokenRatesController: TokenRatesControllerState; TransactionController: TransactionControllerState; SmartTransactionsController: SmartTransactionsControllerState; - SwapsController: SwapsState; + SwapsController: SwapsControllerState; GasFeeController: GasFeeState; TokensController: TokensControllerState; TokenDetectionController: BaseState; From 6cce308822d79afa56c4e31d48ab9e40b1f79691 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 21 Nov 2024 10:10:12 -0800 Subject: [PATCH 13/20] Reorganize files --- app/core/Engine/Engine.test.ts | 7 +-- app/core/Engine/Engine.ts | 31 +++------- .../constants.ts} | 58 ------------------ .../controllers/AccountsController/index.ts | 2 + .../utils.test.ts} | 10 ++-- .../controllers/AccountsController/utils.ts | 60 +++++++++++++++++++ app/core/Engine/index.ts | 2 +- app/core/Engine/{Engine.types.ts => types.ts} | 28 ++++++++- 8 files changed, 105 insertions(+), 93 deletions(-) rename app/core/Engine/controllers/{accountsControllerUtils.ts => AccountsController/constants.ts} (61%) create mode 100644 app/core/Engine/controllers/AccountsController/index.ts rename app/core/Engine/controllers/{accountsControllerUtils.test.ts => AccountsController/utils.test.ts} (89%) create mode 100644 app/core/Engine/controllers/AccountsController/utils.ts rename app/core/Engine/{Engine.types.ts => types.ts} (93%) diff --git a/app/core/Engine/Engine.test.ts b/app/core/Engine/Engine.test.ts index 48c1f0d2433..3778cf049b7 100644 --- a/app/core/Engine/Engine.test.ts +++ b/app/core/Engine/Engine.test.ts @@ -1,8 +1,5 @@ -import Engine, { - Engine as EngineClass, - TransactionEventPayload, -} from './Engine'; -import { EngineState } from './Engine.types'; +import Engine, { Engine as EngineClass } from './Engine'; +import { EngineState, TransactionEventPayload } from './types'; import { backgroundState } from '../../util/test/initial-root-state'; import { zeroAddress } from 'ethereumjs-util'; import { createMockAccountsControllerState } from '../../util/test/accountsControllerTestUtils'; diff --git a/app/core/Engine/Engine.ts b/app/core/Engine/Engine.ts index f85327219c4..a385b7faf8c 100644 --- a/app/core/Engine/Engine.ts +++ b/app/core/Engine/Engine.ts @@ -153,7 +153,7 @@ import { AccountsControllerSelectedAccountChangeEvent, AccountsControllerAccountAddedEvent, AccountsControllerAccountRenamedEvent, -} from './controllers/accountsControllerUtils'; +} from './controllers/AccountsController'; import { captureException } from '@sentry/react-native'; import { lowerCase } from 'lodash'; import { @@ -191,7 +191,13 @@ import { trace } from '../../util/trace'; import { MetricsEventBuilder } from '../Analytics/MetricsEventBuilder'; import { JsonMap } from '../Analytics/MetaMetrics.types'; import { isPooledStakingFeatureEnabled } from '../../components/UI/Stake/constants'; -import { ControllerMessenger, Controllers, EngineState } from './Engine.types'; +import { + ControllerMessenger, + Controllers, + EngineState, + EngineContext, + TransactionEventPayload, +} from './types'; const NON_EMPTY = 'NON_EMPTY'; @@ -202,27 +208,6 @@ const encryptor = new Encryptor({ // eslint-disable-next-line @typescript-eslint/no-explicit-any let currentChainId: any; -/** - * Controllers that area always instantiated - */ -type RequiredControllers = Omit; - -/** - * Controllers that are sometimes not instantiated - */ -type OptionalControllers = Pick; - -/** - * Combines required and optional controllers for the Engine context type. - */ -export type EngineContext = RequiredControllers & Partial; - -export interface TransactionEventPayload { - transactionMeta: TransactionMeta; - actionId?: string; - error?: string; -} - /** * Core controller responsible for composing other metamask controllers together * and exposing convenience methods for common wallet operations. diff --git a/app/core/Engine/controllers/accountsControllerUtils.ts b/app/core/Engine/controllers/AccountsController/constants.ts similarity index 61% rename from app/core/Engine/controllers/accountsControllerUtils.ts rename to app/core/Engine/controllers/AccountsController/constants.ts index 392c911c807..5007533e2f0 100644 --- a/app/core/Engine/controllers/accountsControllerUtils.ts +++ b/app/core/Engine/controllers/AccountsController/constants.ts @@ -1,7 +1,4 @@ import { - AccountsController, - AccountsControllerMessenger, - AccountsControllerState, AccountsControllerGetAccountByAddressAction as AccountsControllerGetAccountByAddressActionType, AccountsControllerSetAccountNameAction as AccountsControllerSetAccountNameActionType, AccountsControllerSetSelectedAccountAction as AccountsControllerSetSelectedAccountActionType, @@ -14,61 +11,6 @@ import { AccountsControllerAccountAddedEvent as AccountsControllerAccountAddedEventType, AccountsControllerAccountRenamedEvent as AccountsControllerAccountRenamedEventType, } from '@metamask/accounts-controller'; -import { ControllerMessenger } from '../Engine.types'; -import Logger from '../../../util/Logger'; - -// Default AccountsControllerState -export const defaultAccountsControllerState: AccountsControllerState = { - internalAccounts: { - accounts: {}, - selectedAccount: '', - }, -}; - -/** - * Creates instance of AccountsController - * - * @param options.messenger - Controller messenger instance - * @param options.initialState - Initial state of AccountsController - * @returns - AccountsController instance - */ -export const createAccountsController = ({ - messenger, - initialState, -}: { - messenger: ControllerMessenger; - initialState?: AccountsControllerState; -}): AccountsController => { - let accountsController = {} as AccountsController; - - try { - const accountsControllerMessenger: AccountsControllerMessenger = - messenger.getRestricted({ - name: 'AccountsController', - allowedEvents: [ - 'SnapController:stateChange', - 'KeyringController:accountRemoved', - 'KeyringController:stateChange', - ], - allowedActions: [ - 'KeyringController:getAccounts', - 'KeyringController:getKeyringsByType', - 'KeyringController:getKeyringForAccount', - ], - }); - - accountsController = new AccountsController({ - messenger: accountsControllerMessenger, - state: initialState ?? defaultAccountsControllerState, - }); - } catch (error) { - // Report error while initializing AccountsController - // TODO: Direct to vault recovery to reset controller states - Logger.error(error as Error, 'Failed to initialize AccountsController'); - } - - return accountsController; -}; // Action types of AccountsController export const AccountsControllerGetAccountByAddressAction: AccountsControllerGetAccountByAddressActionType['type'] = diff --git a/app/core/Engine/controllers/AccountsController/index.ts b/app/core/Engine/controllers/AccountsController/index.ts new file mode 100644 index 00000000000..13c08e324b4 --- /dev/null +++ b/app/core/Engine/controllers/AccountsController/index.ts @@ -0,0 +1,2 @@ +export * from './constants'; +export * from './utils'; diff --git a/app/core/Engine/controllers/accountsControllerUtils.test.ts b/app/core/Engine/controllers/AccountsController/utils.test.ts similarity index 89% rename from app/core/Engine/controllers/accountsControllerUtils.test.ts rename to app/core/Engine/controllers/AccountsController/utils.test.ts index eba5c6999cb..3d501d45b08 100644 --- a/app/core/Engine/controllers/accountsControllerUtils.test.ts +++ b/app/core/Engine/controllers/AccountsController/utils.test.ts @@ -1,13 +1,13 @@ import { AccountsControllerState } from '@metamask/accounts-controller'; -import { ExtendedControllerMessenger } from '../../ExtendedControllerMessenger'; +import { ExtendedControllerMessenger } from '../../../ExtendedControllerMessenger'; import { createAccountsController, defaultAccountsControllerState, -} from './accountsControllerUtils'; -import { ControllerMessenger } from '../'; +} from './utils'; +import { ControllerMessenger } from '../../'; import { withScope } from '@sentry/react-native'; -import { AGREED, METRICS_OPT_IN } from '../../../constants/storage'; -import StorageWrapper from '../../../store/storage-wrapper'; +import { AGREED, METRICS_OPT_IN } from '../../../../constants/storage'; +import StorageWrapper from '../../../../store/storage-wrapper'; jest.mock('@sentry/react-native', () => ({ withScope: jest.fn(), diff --git a/app/core/Engine/controllers/AccountsController/utils.ts b/app/core/Engine/controllers/AccountsController/utils.ts new file mode 100644 index 00000000000..44325a3bd65 --- /dev/null +++ b/app/core/Engine/controllers/AccountsController/utils.ts @@ -0,0 +1,60 @@ +import { + AccountsController, + AccountsControllerMessenger, + AccountsControllerState, +} from '@metamask/accounts-controller'; +import { ControllerMessenger } from '../../types'; +import Logger from '../../../../util/Logger'; + +// Default AccountsControllerState +export const defaultAccountsControllerState: AccountsControllerState = { + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, +}; + +/** + * Creates instance of AccountsController + * + * @param options.messenger - Controller messenger instance + * @param options.initialState - Initial state of AccountsController + * @returns - AccountsController instance + */ +export const createAccountsController = ({ + messenger, + initialState, +}: { + messenger: ControllerMessenger; + initialState?: AccountsControllerState; +}): AccountsController => { + let accountsController = {} as AccountsController; + + try { + const accountsControllerMessenger: AccountsControllerMessenger = + messenger.getRestricted({ + name: 'AccountsController', + allowedEvents: [ + 'SnapController:stateChange', + 'KeyringController:accountRemoved', + 'KeyringController:stateChange', + ], + allowedActions: [ + 'KeyringController:getAccounts', + 'KeyringController:getKeyringsByType', + 'KeyringController:getKeyringForAccount', + ], + }); + + accountsController = new AccountsController({ + messenger: accountsControllerMessenger, + state: initialState ?? defaultAccountsControllerState, + }); + } catch (error) { + // Report error while initializing AccountsController + // TODO: Direct to vault recovery to reset controller states + Logger.error(error as Error, 'Failed to initialize AccountsController'); + } + + return accountsController; +}; diff --git a/app/core/Engine/index.ts b/app/core/Engine/index.ts index a36cbc7ae71..55587d2843a 100644 --- a/app/core/Engine/index.ts +++ b/app/core/Engine/index.ts @@ -1,2 +1,2 @@ export { default } from './Engine'; -export * from './Engine.types'; +export * from './types'; diff --git a/app/core/Engine/Engine.types.ts b/app/core/Engine/types.ts similarity index 93% rename from app/core/Engine/Engine.types.ts rename to app/core/Engine/types.ts index b9de998a56f..456cfb8759f 100644 --- a/app/core/Engine/Engine.types.ts +++ b/app/core/Engine/types.ts @@ -67,6 +67,7 @@ import { TransactionController, TransactionControllerEvents, TransactionControllerState, + TransactionMeta, } from '@metamask/transaction-controller'; import { GasFeeController, @@ -153,6 +154,16 @@ import { import { BaseState } from '@metamask/base-controller'; import { getPermissionSpecifications } from '../Permissions/specifications.js'; +/** + * Controllers that area always instantiated + */ +type RequiredControllers = Omit; + +/** + * Controllers that are sometimes not instantiated + */ +type OptionalControllers = Pick; + type PermissionsByRpcMethod = ReturnType; type Permissions = PermissionsByRpcMethod[keyof PermissionsByRpcMethod]; @@ -162,12 +173,12 @@ type UserStorageControllerActions = UserStorageController.AllowedActions; type NotificationsServicesControllerActions = NotificationServicesController.AllowedActions; +// TODO: Abstract this into controller utils for SnapsController type SnapsGlobalActions = | SnapControllerActions | SubjectMetadataControllerActions | PhishingControllerActions | SnapsAllowedActions; - type SnapsGlobalEvents = | SnapControllerEvents | SubjectMetadataControllerEvents @@ -232,6 +243,13 @@ type GlobalEvents = | SelectedNetworkControllerEvents | SmartTransactionsControllerEvents; +// TODO: Abstract this into controller utils for TransactionController +export interface TransactionEventPayload { + transactionMeta: TransactionMeta; + actionId?: string; + error?: string; +} + /** * Type definition for the controller messenger used in the Engine. * It extends the base ControllerMessenger with global actions and events. @@ -284,6 +302,14 @@ export interface Controllers { SwapsController: SwapsController; } +/** + * Combines required and optional controllers for the Engine context type. + */ +export type EngineContext = RequiredControllers & Partial; + +/** + * All engine state, keyed by controller name + */ export interface EngineState { AccountTrackerController: AccountTrackerControllerState; AddressBookController: AddressBookControllerState; From 96d04ffd6b57f5f27f415f025de0f35774b17bf3 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 21 Nov 2024 10:19:31 -0800 Subject: [PATCH 14/20] Update term for test --- app/core/Engine/controllers/AccountsController/utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/Engine/controllers/AccountsController/utils.test.ts b/app/core/Engine/controllers/AccountsController/utils.test.ts index 3d501d45b08..25f95dd2e14 100644 --- a/app/core/Engine/controllers/AccountsController/utils.test.ts +++ b/app/core/Engine/controllers/AccountsController/utils.test.ts @@ -60,7 +60,7 @@ describe('accountControllersUtils', () => { }); expect(accountsController.name).toEqual(accountsControllerName); }); - it('should throw error when controller fails to initialize', async () => { + it('should catch error when controller fails to initialize', async () => { const controllerMessenger = 'controllerMessenger' as unknown as ControllerMessenger; const accountsController = await createAccountsController({ From e4135058c73709b468776b5b77f392c52b879a7d Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 21 Nov 2024 10:48:45 -0800 Subject: [PATCH 15/20] Separate exports to prevent chance of circular deps --- app/core/Engine/Engine.ts | 4 ++-- app/core/Engine/controllers/AccountsController/index.ts | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 app/core/Engine/controllers/AccountsController/index.ts diff --git a/app/core/Engine/Engine.ts b/app/core/Engine/Engine.ts index a385b7faf8c..c70c0f1ea19 100644 --- a/app/core/Engine/Engine.ts +++ b/app/core/Engine/Engine.ts @@ -144,7 +144,6 @@ import { AccountsControllerGetAccountByAddressAction, AccountsControllerSetAccountNameAction, ///: END:ONLY_INCLUDE_IF - createAccountsController, AccountsControllerGetAccountAction, AccountsControllerGetSelectedAccountAction, AccountsControllerListAccountsAction, @@ -153,7 +152,8 @@ import { AccountsControllerSelectedAccountChangeEvent, AccountsControllerAccountAddedEvent, AccountsControllerAccountRenamedEvent, -} from './controllers/AccountsController'; +} from './controllers/AccountsController/constants'; +import { createAccountsController } from './controllers/AccountsController/utils'; import { captureException } from '@sentry/react-native'; import { lowerCase } from 'lodash'; import { diff --git a/app/core/Engine/controllers/AccountsController/index.ts b/app/core/Engine/controllers/AccountsController/index.ts deleted file mode 100644 index 13c08e324b4..00000000000 --- a/app/core/Engine/controllers/AccountsController/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './constants'; -export * from './utils'; From 36e83b4f70fa3b5b3fe279d8fef2015a5d744227 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 21 Nov 2024 14:17:50 -0800 Subject: [PATCH 16/20] Fix wrong path --- app/core/Engine/Engine.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core/Engine/Engine.test.ts b/app/core/Engine/Engine.test.ts index 3778cf049b7..b54e6d32fc5 100644 --- a/app/core/Engine/Engine.test.ts +++ b/app/core/Engine/Engine.test.ts @@ -15,10 +15,10 @@ import { RootState } from '../../reducers'; import { MetricsEventBuilder } from '../Analytics/MetricsEventBuilder'; jest.unmock('./Engine'); -jest.mock('../store', () => ({ +jest.mock('../../store', () => ({ store: { getState: jest.fn(() => ({ engine: {} })) }, })); -jest.mock('../selectors/smartTransactionsController', () => ({ +jest.mock('../../selectors/smartTransactionsController', () => ({ selectShouldUseSmartTransaction: jest.fn().mockReturnValue(false), })); From 8dfc80c0c274275c649825d8d330b3e787328e64 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 21 Nov 2024 14:51:15 -0800 Subject: [PATCH 17/20] Fix engine path --- app/components/Nav/Main/index.test.tsx | 2 +- app/components/UI/AccountOverview/index.test.tsx | 2 +- app/components/UI/AssetSearch/index.test.tsx | 2 +- app/components/Views/Asset/index.test.js | 2 +- app/components/Views/confirmations/Approval/index.test.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/components/Nav/Main/index.test.tsx b/app/components/Nav/Main/index.test.tsx index 33d004c74fe..1d6ed7941c9 100644 --- a/app/components/Nav/Main/index.test.tsx +++ b/app/components/Nav/Main/index.test.tsx @@ -10,7 +10,7 @@ import { MetaMetricsEvents } from '../../hooks/useMetrics'; import { renderHookWithProvider } from '../../../util/test/renderWithProvider'; import Engine from '../../../core/Engine'; -jest.mock('../../../core/Engine.ts', () => ({ +jest.mock('../../../core/Engine', () => ({ controllerMessenger: { subscribeOnceIf: jest.fn(), }, diff --git a/app/components/UI/AccountOverview/index.test.tsx b/app/components/UI/AccountOverview/index.test.tsx index 49fb5077401..9df49d8d11b 100644 --- a/app/components/UI/AccountOverview/index.test.tsx +++ b/app/components/UI/AccountOverview/index.test.tsx @@ -11,7 +11,7 @@ import { const mockedEngine = Engine; -jest.mock('../../../core/Engine.ts', () => { +jest.mock('../../../core/Engine', () => { const { MOCK_ACCOUNTS_CONTROLLER_STATE: mockAccountsControllerState } = jest.requireActual('../../../util/test/accountsControllerTestUtils'); return { diff --git a/app/components/UI/AssetSearch/index.test.tsx b/app/components/UI/AssetSearch/index.test.tsx index 9838088af3b..6009fe1393a 100644 --- a/app/components/UI/AssetSearch/index.test.tsx +++ b/app/components/UI/AssetSearch/index.test.tsx @@ -5,7 +5,7 @@ import { backgroundState } from '../../../util/test/initial-root-state'; import Engine from '../../../core/Engine'; const mockedEngine = Engine; -jest.mock('../../../core/Engine.ts', () => ({ +jest.mock('../../../core/Engine', () => ({ init: () => mockedEngine.init({}), context: { KeyringController: { diff --git a/app/components/Views/Asset/index.test.js b/app/components/Views/Asset/index.test.js index bc1dc5ef4d6..968ed7f4b69 100644 --- a/app/components/Views/Asset/index.test.js +++ b/app/components/Views/Asset/index.test.js @@ -13,7 +13,7 @@ const mockInitialState = { }, }; -jest.mock('../../../core/Engine.ts', () => { +jest.mock('../../../core/Engine', () => { const { MOCK_ADDRESS_1, } = require('../../../util/test/accountsControllerTestUtils'); diff --git a/app/components/Views/confirmations/Approval/index.test.tsx b/app/components/Views/confirmations/Approval/index.test.tsx index 2d135565e89..6c44f3ddfc5 100644 --- a/app/components/Views/confirmations/Approval/index.test.tsx +++ b/app/components/Views/confirmations/Approval/index.test.tsx @@ -10,7 +10,7 @@ const approvalState = { }, }; -jest.mock('../../../../core/Engine.ts', () => ({ +jest.mock('../../../../core/Engine', () => ({ rejectPendingApproval: jest.fn(), context: { KeyringController: { From 74543291e6f54e8ebebf1817c59d4de34f0823d9 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 21 Nov 2024 14:55:46 -0800 Subject: [PATCH 18/20] Update code owners --- .github/CODEOWNERS | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4931485e00b..0950da58cae 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,8 +8,10 @@ app/component-library/ @MetaMask/design-system-engineers # Platform Team patches/ @MetaMask/mobile-platform -app/core/Engine.ts @MetaMask/mobile-platform -app/core/Engine.test.js @MetaMask/mobile-platform +app/core/Engine/Engine.ts @MetaMask/mobile-platform +app/core/Engine/Engine.test.ts @MetaMask/mobile-platform +app/core/Engine/index.ts @MetaMask/mobile-platform +app/core/Engine/types.ts @MetaMask/mobile-platform app/core/Analytics/ @MetaMask/mobile-platform app/util/metrics/ @MetaMask/mobile-platform app/components/hooks/useMetrics/ @MetaMask/mobile-platform @@ -41,7 +43,8 @@ app/reducers/sdk @MetaMask/sdk-devs @MetaMask/mobile-platform app/util/walletconnect.js @MetaMask/sdk-devs @MetaMask/mobile-platform # Accounts Team -app/core/Encryptor/ @MetaMask/accounts-engineers +app/core/Encryptor/ @MetaMask/accounts-engineers +app/core/Engine/controller/AccountsController @MetaMask/accounts-engineers # Swaps Team app/components/UI/Swaps @MetaMask/swaps-engineers @MetaMask/mobile-platform From f69b6d3682966c456a6dd4e36f412a6511be5385 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 21 Nov 2024 15:12:12 -0800 Subject: [PATCH 19/20] Fix broken unit test --- app/components/Views/AccountActions/AccountActions.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/components/Views/AccountActions/AccountActions.test.tsx b/app/components/Views/AccountActions/AccountActions.test.tsx index 730761ea373..395735523c2 100644 --- a/app/components/Views/AccountActions/AccountActions.test.tsx +++ b/app/components/Views/AccountActions/AccountActions.test.tsx @@ -28,7 +28,6 @@ const initialState = { }; jest.mock('../../../core/Engine', () => ({ - ...jest.requireActual('../../../core/Engine'), context: { PreferencesController: { selectedAddress: `0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756`, From 6abfe31d9ca8fdc8c2627cbc447a74dd1c7bea12 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Fri, 22 Nov 2024 11:25:05 -0800 Subject: [PATCH 20/20] Fix unit test --- .../Views/confirmations/ApproveView/Approve/index.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/components/Views/confirmations/ApproveView/Approve/index.test.tsx b/app/components/Views/confirmations/ApproveView/Approve/index.test.tsx index 8af433251e1..91b67a3beba 100644 --- a/app/components/Views/confirmations/ApproveView/Approve/index.test.tsx +++ b/app/components/Views/confirmations/ApproveView/Approve/index.test.tsx @@ -21,7 +21,7 @@ jest.mock('../../../../../core/GasPolling/GasPolling', () => ({ stopGasPolling: jest.fn().mockResolvedValue(null), })); -jest.mock('../../../../../core/Engine.ts', () => ({ +jest.mock('../../../../../core/Engine', () => ({ controllerMessenger: { tryUnsubscribe: jest.fn(), subscribe: jest.fn(), @@ -59,7 +59,8 @@ const routeMock = { }; const hideModalMock = jest.fn(); -const renderComponent = ({ store }: { store: Store }) => render( +const renderComponent = ({ store }: { store: Store }) => + render(