diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 353064992c42..7c29ca597354 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -3,6 +3,7 @@ import { KeyringType } from '../shared/constants/keyring'; import { NetworkType } from '@metamask/controller-utils'; import { NetworkStatus } from '@metamask/network-controller'; import { CHAIN_IDS } from '../shared/constants/network'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; const state = { invalidCustomNetwork: { @@ -300,6 +301,63 @@ const state = { isUnlocked: true, isAccountMenuOpen: false, rpcUrl: 'https://rawtestrpc.metamask.io/', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'This is a Really Long Account Name', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '07c2cfec-36c9-46c4-8115-3836d3ac9047': { + address: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '15e69915-2a1a-4019-93b3-916e11fd432f': { + address: '0x9d0ba4ddac06032527b140912ec808ab9451b788', + id: '15e69915-2a1a-4019-93b3-916e11fd432f', + metadata: { + name: 'Account 3', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '784225f4-d30b-4e77-a900-c8bbce735b88': { + address: '0xeb9e64b93097bc15f01f13eae97015c57ab64823', + id: '784225f4-d30b-4e77-a900-c8bbce735b88', + metadata: { + name: 'Account 4', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, identities: { '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': { name: 'This is a Really Long Account Name', @@ -455,6 +513,57 @@ const state = { ], }, ], + '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': [ + { + address: '0x514910771AF9Ca656af840dff83E8264EcF986CA', + decimals: 18, + symbol: 'LINK', + image: + 'https://crypto.com/price/coin-data/icon/LINK/color_icon.png', + aggregators: [ + 'coinGecko', + 'oneInch', + 'paraswap', + 'zapper', + 'zerion', + ], + }, + { + address: '0xc00e94Cb662C3520282E6f5717214004A7f26888', + decimals: 18, + symbol: 'COMP', + image: + 'https://crypto.com/price/coin-data/icon/COMP/color_icon.png', + aggregators: [ + 'bancor', + 'cmc', + 'cryptocom', + 'coinGecko', + 'oneInch', + 'paraswap', + 'pmm', + 'zapper', + 'zerion', + 'zeroEx', + ], + }, + { + address: '0xfffffffFf15AbF397dA76f1dcc1A1604F45126DB', + decimals: 18, + symbol: 'FSW', + image: + 'https://assets.coingecko.com/coins/images/12256/thumb/falconswap.png?1598534184', + aggregators: [ + 'aave', + 'cmc', + 'coinGecko', + 'oneInch', + 'paraswap', + 'zapper', + 'zerion', + ], + }, + ], }, }, detectedTokens: [ diff --git a/.yarn/patches/@metamask-keyring-controller-npm-7.5.0-5dd5df31c7.patch b/.yarn/patches/@metamask-keyring-controller-npm-7.5.0-5dd5df31c7.patch new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index efeee77b77e1..6c9aceb930b1 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -498,6 +498,9 @@ "backupApprovalNotice": { "message": "Backup your Secret Recovery Phrase to keep your wallet and funds secure." }, + "backupKeyringSnapReminder": { + "message": "Be sure you can access any accounts created by this snap on your own before removing it." + }, "backupNow": { "message": "Backup now" }, @@ -2117,6 +2120,24 @@ "message": "JSON File", "description": "format for importing an account" }, + "keyringAccountName": { + "message": "Account name" + }, + "keyringAccountPublicAddress": { + "message": "Public Address" + }, + "keyringSnapRemovalResult1": { + "message": "$1 $2removed", + "description": "Displays the result after removal of a keyring snap. $1 is the snap name, $2 is whether it is successful or not" + }, + "keyringSnapRemovalResultNotSuccessful": { + "message": "not ", + "description": "Displays the `not` word in $2." + }, + "keyringSnapRemoveConfirmation": { + "message": "Type $1 to confirm you want to remove this snap:", + "description": "Asks user to input the name nap prior to deleting the snap. $1 is the snap name" + }, "keystone": { "message": "Keystone" }, @@ -3569,6 +3590,12 @@ "removeJWTDescription": { "message": "Are you sure you want to remove this token? All accounts assigned to this token will be removed from extension as well: " }, + "removeKeyringSnap": { + "message": "Removing this snap removes these accounts from MetaMask:" + }, + "removeKeyringSnapToolTip": { + "message": "The snap controls the accounts, and by removing it, the accounts will be removed from MetaMask, too, but they will remain in the blockchain." + }, "removeNFT": { "message": "Remove NFT" }, diff --git a/app/scripts/controllers/alert.js b/app/scripts/controllers/alert.js index 0c682dd38183..ffc87db6408e 100644 --- a/app/scripts/controllers/alert.js +++ b/app/scripts/controllers/alert.js @@ -38,7 +38,11 @@ export default class AlertController { * @param {AlertControllerOptions} [opts] - Controller configuration parameters */ constructor(opts = {}) { - const { initState = {}, preferencesStore } = opts; + const { + initState = {}, + getCurrentSelectedAccount, + controllerMessenger, + } = opts; const state = { ...defaultState, alertEnabledness: { @@ -49,18 +53,23 @@ export default class AlertController { this.store = new ObservableStore(state); - this.selectedAddress = preferencesStore.getState().selectedAddress; + this.selectedAddress = getCurrentSelectedAccount().address; - preferencesStore.subscribe(({ selectedAddress }) => { - const currentState = this.store.getState(); - if ( - currentState.unconnectedAccountAlertShownOrigins && - this.selectedAddress !== selectedAddress - ) { - this.selectedAddress = selectedAddress; - this.store.updateState({ unconnectedAccountAlertShownOrigins: {} }); - } - }); + this.controllerMessenger = controllerMessenger; + + this.controllerMessenger.subscribe( + 'AccountsController:selectedAccountChange', + (account) => { + const currentState = this.store.getState(); + if ( + currentState.unconnectedAccountAlertShownOrigins && + this.selectedAddress !== account.address + ) { + this.selectedAddress = account.address; + this.store.updateState({ unconnectedAccountAlertShownOrigins: {} }); + } + }, + ); } setAlertEnabledness(alertId, enabledness) { diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js index 86c8cc222856..368f452aeeb4 100644 --- a/app/scripts/controllers/detect-tokens.js +++ b/app/scripts/controllers/detect-tokens.js @@ -33,6 +33,7 @@ export default class DetectTokensController { * @param config.assetsContractController * @param config.trackMetaMetricsEvent * @param config.messenger + * @param config.getCurrentSelectedAccount */ constructor({ messenger, @@ -43,6 +44,7 @@ export default class DetectTokensController { tokensController, assetsContractController = null, trackMetaMetricsEvent, + getCurrentSelectedAccount, } = {}) { this.messenger = messenger; this.assetsContractController = assetsContractController; @@ -53,7 +55,7 @@ export default class DetectTokensController { this.tokenList = tokenList; this.useTokenDetection = this.preferences?.store.getState().useTokenDetection; - this.selectedAddress = this.preferences?.store.getState().selectedAddress; + this.selectedAddress = getCurrentSelectedAccount().address; this.tokenAddresses = this.tokensController?.state.tokens.map((token) => { return token.address; }); @@ -62,16 +64,22 @@ export default class DetectTokensController { this.chainId = this.getChainIdFromNetworkStore(); this._trackMetaMetricsEvent = trackMetaMetricsEvent; - preferences?.store.subscribe(({ selectedAddress, useTokenDetection }) => { - if ( - this.selectedAddress !== selectedAddress || - this.useTokenDetection !== useTokenDetection - ) { - this.selectedAddress = selectedAddress; - this.useTokenDetection = useTokenDetection; - this.restartTokenDetection({ selectedAddress }); - } - }); + messenger.subscribe( + 'AccountsController:selectedAccountChange', + (account) => { + const useTokenDetection = + this.preferences?.store.getState().useTokenDetection; + if ( + this.selectedAddress !== account.address || + this.useTokenDetection !== useTokenDetection + ) { + this.selectedAddress = account.address; + this.useTokenDetection = useTokenDetection; + this.restartTokenDetection({ selectedAddress: this.selectedAddress }); + } + }, + ); + tokensController?.subscribe( ({ tokens = [], ignoredTokens = [], detectedTokens = [] }) => { this.tokenAddresses = tokens.map((token) => { @@ -81,6 +89,7 @@ export default class DetectTokensController { this.detectedTokens = detectedTokens; }, ); + messenger.subscribe('NetworkController:stateChange', () => { if (this.chainId !== this.getChainIdFromNetworkStore()) { const chainId = this.getChainIdFromNetworkStore(); diff --git a/app/scripts/controllers/detect-tokens.test.js b/app/scripts/controllers/detect-tokens.test.js index 17927cfcfbd2..20b990b024a4 100644 --- a/app/scripts/controllers/detect-tokens.test.js +++ b/app/scripts/controllers/detect-tokens.test.js @@ -9,7 +9,9 @@ import { AssetsContractController, } from '@metamask/assets-controllers'; import { toHex } from '@metamask/controller-utils'; +import { EthMethod, EthAccountType } from '@metamask/keyring-api'; import { NetworkController } from '@metamask/network-controller'; +import { AccountsController } from '@metamask/accounts-controller'; import { NETWORK_TYPES } from '../../../shared/constants/network'; import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; import DetectTokensController from './detect-tokens'; @@ -23,6 +25,8 @@ describe('DetectTokensController', function () { provider, tokensController, tokenListController, + accountsController, + preferencesControllerMessenger, messenger; const noop = () => undefined; @@ -35,6 +39,7 @@ describe('DetectTokensController', function () { 'NetworkController:stateChange', 'KeyringController:lock', 'KeyringController:unlock', + 'AccountsController:selectedAccountChange', ], }); }; @@ -227,24 +232,84 @@ describe('DetectTokensController', function () { }); await tokenListController.start(); + preferencesControllerMessenger = new ControllerMessenger().getRestricted({ + name: 'PreferencesController', + allowedEvents: ['AccountsController:selectedAccountChange'], + }); + preferences = new PreferencesController({ network, provider, tokenListController, networkConfigurations: {}, onAccountRemoved: sinon.stub(), + controllerMessenger: preferencesControllerMessenger, }); - preferences.setAddresses([ - '0x7e57e2', - '0xbc86727e770de68b1060c91f6bb6945c73e10388', - ]); preferences.setUseTokenDetection(true); + const accountsControllerMessenger = new ControllerMessenger().getRestricted( + { + name: 'AccountsController', + allowedEvents: [ + 'SnapController:stateChange', + 'KeyringController:accountRemoved', + 'KeyringController:stateChange', + 'AccountsController:selectedAccountChange', + ], + }, + ); + + accountsController = new AccountsController({ + messenger: accountsControllerMessenger, + state: { + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '07c2cfec-36c9-46c4-8115-3836d3ac9047': { + address: '0xbc86727e770de68b1060c91f6bb6945c73e10388', + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, + }, + onSnapStateChange: sinon.spy(), + onKeyringStateChange: sinon.spy(), + }); + tokensController = new TokensController({ - config: { provider }, - onPreferencesStateChange: preferences.store.subscribe.bind( - preferences.store, - ), + config: { + provider, + selectedAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + }, + onPreferencesStateChange: (listener) => + accountsControllerMessenger.subscribe( + `AccountsController:selectedAccountChange`, + (newlySelectedInternalAccount) => { + listener({ selectedAddress: newlySelectedInternalAccount.address }); + }, + ), onNetworkStateChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, 'NetworkController:stateChange', @@ -273,6 +338,8 @@ describe('DetectTokensController', function () { new DetectTokensController({ messenger: getRestrictedMessenger(), interval: 1337, + getCurrentSelectedAccount: + accountsController.getSelectedAccount.bind(accountsController), }); assert.strictEqual(stub.getCall(0).args[1], 1337); stub.restore(); @@ -288,6 +355,8 @@ describe('DetectTokensController', function () { tokenList: tokenListController, tokensController, assetsContractController, + getCurrentSelectedAccount: + accountsController.getSelectedAccount.bind(accountsController), }); controller.isOpen = true; controller.isUnlocked = true; @@ -324,6 +393,8 @@ describe('DetectTokensController', function () { tokenList: tokenListController, tokensController, assetsContractController, + getCurrentSelectedAccount: + accountsController.getSelectedAccount.bind(accountsController), }); controller.isOpen = true; controller.isUnlocked = true; @@ -348,6 +419,8 @@ describe('DetectTokensController', function () { tokensController, assetsContractController, trackMetaMetricsEvent: noop, + getCurrentSelectedAccount: + accountsController.getSelectedAccount.bind(accountsController), }); controller.isOpen = true; controller.isUnlocked = true; @@ -399,6 +472,8 @@ describe('DetectTokensController', function () { tokensController, assetsContractController, trackMetaMetricsEvent: noop, + getCurrentSelectedAccount: + accountsController.getSelectedAccount.bind(accountsController), }); controller.isOpen = true; controller.isUnlocked = true; @@ -409,16 +484,26 @@ describe('DetectTokensController', function () { const existingTokenAddress = erc20ContractAddresses[0]; const existingToken = tokenList[existingTokenAddress]; - await tokensController.addDetectedTokens([ + accountsController.setSelectedAccount( + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + ); + + await tokensController.addDetectedTokens( + [ + { + address: existingToken.address, + symbol: existingToken.symbol, + decimals: existingToken.decimals, + aggregators: undefined, + image: undefined, + isERC721: undefined, + }, + ], { - address: existingToken.address, - symbol: existingToken.symbol, - decimals: existingToken.decimals, - aggregators: undefined, - image: undefined, - isERC721: undefined, + selectedAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x1', }, - ]); + ); const tokenAddressToAdd = erc20ContractAddresses[1]; const tokenToAdd = tokenList[tokenAddressToAdd]; sandbox @@ -456,13 +541,17 @@ describe('DetectTokensController', function () { tokenList: tokenListController, tokensController, assetsContractController, + getCurrentSelectedAccount: + accountsController.getSelectedAccount.bind(accountsController), }); controller.isOpen = true; controller.isUnlocked = true; const stub = sandbox.stub(controller, 'detectNewTokens'); - await preferences.setSelectedAddress( - '0xbc86727e770de68b1060c91f6bb6945c73e10388', - ); + messenger.publish('AccountsController:selectedAccountChange', { + id: 'mock-2', + address: '0x999', + }); + sandbox.assert.called(stub); }); @@ -475,6 +564,8 @@ describe('DetectTokensController', function () { tokenList: tokenListController, tokensController, assetsContractController, + getCurrentSelectedAccount: + accountsController.getSelectedAccount.bind(accountsController), }); controller.isOpen = true; controller.selectedAddress = '0x0'; @@ -495,6 +586,8 @@ describe('DetectTokensController', function () { tokenList: tokenListController, tokensController, assetsContractController, + getCurrentSelectedAccount: + accountsController.getSelectedAccount.bind(accountsController), }); controller.isOpen = true; @@ -514,6 +607,8 @@ describe('DetectTokensController', function () { tokenList: tokenListController, tokensController, assetsContractController, + getCurrentSelectedAccount: + accountsController.getSelectedAccount.bind(accountsController), }); controller.isOpen = true; controller.isUnlocked = false; @@ -534,10 +629,12 @@ describe('DetectTokensController', function () { network, tokensController, assetsContractController, + getCurrentSelectedAccount: + accountsController.getSelectedAccount.bind(accountsController), }); // trigger state update from preferences controller - await preferences.setSelectedAddress( - '0xbc86727e770de68b1060c91f6bb6945c73e10388', + accountsController.setSelectedAccount( + '07c2cfec-36c9-46c4-8115-3836d3ac9047', ); controller.isOpen = false; controller.isUnlocked = true; diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index d92e1ce5f808..beab0d317299 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -750,7 +750,7 @@ export default class MetaMetricsController { [MetaMetricsUserTrait.NftAutodetectionEnabled]: metamaskState.useNftDetection, [MetaMetricsUserTrait.NumberOfAccounts]: Object.values( - metamaskState.identities, + metamaskState.internalAccounts.accounts, ).length, [MetaMetricsUserTrait.NumberOfNftCollections]: this._getAllUniqueNFTAddressesLength(metamaskState.allNfts), diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index 09170d4eee8f..781318f61029 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -1,6 +1,7 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; import { toHex } from '@metamask/controller-utils'; +import { EthAccountType } from '@metamask/keyring-api'; import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app'; import { createSegmentMock } from '../lib/segment'; import { @@ -946,7 +947,37 @@ describe('MetaMetricsController', function () { }, 'network-configuration-id-3': { chainId: '0xaf' }, }, - identities: [{}, {}], + internalAccounts: { + accounts: { + 'mock-id': { + address: '0x0', + id: 'mock-id', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthAccountType)], + type: EthAccountType.Eoa, + }, + 'mock-id-2': { + address: '0x1', + id: 'mock-id-2', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthAccountType)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'mock-id', + }, ledgerTransportType: 'web-hid', openSeaEnabled: true, useNftDetection: false, @@ -999,7 +1030,37 @@ describe('MetaMetricsController', function () { }, ledgerTransportType: 'web-hid', openSeaEnabled: true, - identities: [{}, {}], + internalAccounts: { + accounts: { + 'mock-id': { + address: '0x0', + id: 'mock-id', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthAccountType)], + type: EthAccountType.Eoa, + }, + 'mock-id-2': { + address: '0x1', + id: 'mock-id-2', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthAccountType)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'mock-id', + }, useNftDetection: false, theme: 'default', useTokenDetection: true, @@ -1022,7 +1083,50 @@ describe('MetaMetricsController', function () { }, ledgerTransportType: 'web-hid', openSeaEnabled: false, - identities: [{}, {}, {}], + internalAccounts: { + accounts: { + 'mock-id': { + address: '0x0', + id: 'mock-id', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthAccountType)], + type: EthAccountType.Eoa, + }, + 'mock-id-2': { + address: '0x1', + id: 'mock-id-2', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthAccountType)], + type: EthAccountType.Eoa, + }, + 'mock-id-3': { + address: '0x2', + id: 'mock-id-3', + metadata: { + name: 'Account 3', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthAccountType)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'mock-id', + }, useNftDetection: false, theme: 'default', useTokenDetection: true, @@ -1051,7 +1155,37 @@ describe('MetaMetricsController', function () { }, ledgerTransportType: 'web-hid', openSeaEnabled: true, - identities: [{}, {}], + internalAccounts: { + accounts: { + 'mock-id': { + address: '0x0', + id: 'mock-id', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthAccountType)], + type: EthAccountType.Eoa, + }, + 'mock-id-2': { + address: '0x1', + id: 'mock-id-2', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthAccountType)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'mock-id', + }, useNftDetection: true, theme: 'default', useTokenDetection: true, @@ -1070,7 +1204,37 @@ describe('MetaMetricsController', function () { }, ledgerTransportType: 'web-hid', openSeaEnabled: true, - identities: [{}, {}], + internalAccounts: { + accounts: { + 'mock-id': { + address: '0x0', + id: 'mock-id', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthAccountType)], + type: EthAccountType.Eoa, + }, + 'mock-id-2': { + address: '0x1', + id: 'mock-id-2', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthAccountType)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'mock-id', + }, useNftDetection: true, theme: 'default', useTokenDetection: true, diff --git a/app/scripts/controllers/mmi-controller.js b/app/scripts/controllers/mmi-controller.js index d6e980e1be95..3010e451a085 100644 --- a/app/scripts/controllers/mmi-controller.js +++ b/app/scripts/controllers/mmi-controller.js @@ -1,7 +1,6 @@ import EventEmitter from 'events'; import log from 'loglevel'; import { captureException } from '@sentry/browser'; -import { isEqual } from 'lodash'; import { CUSTODIAN_TYPES } from '@metamask-institutional/custody-keyring'; import { updateCustodianTransactions, @@ -21,7 +20,6 @@ import { BUILD_QUOTE_ROUTE, CONNECT_HARDWARE_ROUTE, } from '../../../ui/helpers/constants/routes'; -import { previousValueComparator } from '../lib/util'; import { getPermissionBackgroundApiMethods } from './permissions'; export default class MMIController extends EventEmitter { @@ -33,7 +31,6 @@ export default class MMIController extends EventEmitter { this.keyringController = opts.keyringController; this.txController = opts.txController; this.securityProviderRequest = opts.securityProviderRequest; - this.preferencesController = opts.preferencesController; this.appStateController = opts.appStateController; this.transactionUpdateController = opts.transactionUpdateController; this.custodyController = opts.custodyController; @@ -47,6 +44,8 @@ export default class MMIController extends EventEmitter { this.signatureController = opts.signatureController; this.platform = opts.platform; this.extension = opts.extension; + this.accountsController = opts.accountsController; + this.controllerMessenger = opts.controllerMessenger; // Prepare event listener after transactionUpdateController gets initiated this.transactionUpdateController.prepareEventListener( @@ -63,15 +62,12 @@ export default class MMIController extends EventEmitter { }); } - this.preferencesController.store.subscribe( - previousValueComparator(async (prevState, currState) => { - const { identities: prevIdentities } = prevState; - const { identities: currIdentities } = currState; - if (isEqual(prevIdentities, currIdentities)) { - return; - } + this.controllerMessenger.subscribe( + 'AccountsController:stateChange', + async (_newState, _changes) => { + // Changes of selected account, name changes, account additions and deletions will trigger a rerun await this.prepareMmiPortfolio(); - }, this.preferencesController.store.getState()), + }, ); this.signatureController.hub.on( @@ -240,10 +236,15 @@ export default class MMIController extends EventEmitter { const newAccounts = Object.keys(accounts); // Check if any address is already added - const identities = Object.keys( - this.preferencesController.store.getState().identities, - ); - if (newAccounts.some((address) => identities.indexOf(address) !== -1)) { + const existingInternalAccounts = this.accountsController.listAccounts(); + if ( + newAccounts.some((address) => + existingInternalAccounts.find( + (internalAccount) => + internalAccount.address.toLowerCase() === address, + ), + ) + ) { throw new Error('Cannot import duplicate accounts'); } @@ -296,11 +297,16 @@ export default class MMIController extends EventEmitter { await this.keyringController.addNewAccountForKeyring(keyring); } - const allAccounts = await this.keyringController.getAccounts(); + const allInternalAccounts = await this.accountsController.listAccounts(); - this.preferencesController.setAddresses(allAccounts); const accountsToTrack = [ - ...new Set(oldAccounts.concat(allAccounts.map((a) => a.toLowerCase()))), + ...new Set( + oldAccounts.concat( + allInternalAccounts.map((internalAccount) => + internalAccount.address.toLowerCase(), + ), + ), + ), ]; // Create a Set of lowercased addresses from oldAccounts for efficient existence checks @@ -315,10 +321,10 @@ export default class MMIController extends EventEmitter { return acc; }, {}); - // Iterate over all accounts - allAccounts.forEach((address) => { + // Iterate over all internal accounts + allInternalAccounts.forEach((internalAccount) => { // Convert the address to lowercase for consistent comparisons - const lowercasedAddress = address.toLowerCase(); + const lowercasedAddress = internalAccount.address.toLowerCase(); // If the address is not in oldAccounts if (!oldAccountsSet.has(lowercasedAddress)) { @@ -328,7 +334,7 @@ export default class MMIController extends EventEmitter { // If the label is defined if (label) { // Set the label for the address - this.preferencesController.setAccountLabel(address, label); + this.accountsController.setAccountName(internalAccount.id, label); } } }); @@ -373,7 +379,7 @@ export default class MMIController extends EventEmitter { ) { let currentCustodyType; if (!custodianType) { - const address = this.preferencesController.getSelectedAddress(); + const address = this.accountsController.getSelectedAccount(); currentCustodyType = this.custodyController.getCustodyTypeByAddress( toChecksumHexAddress(address), ); @@ -464,12 +470,12 @@ export default class MMIController extends EventEmitter { async getCustodianJWTList(custodianName) { console.log('getCustodianJWTList', custodianName); - const { identities } = this.preferencesController.store.getState(); + const internalAccounts = this.accountsController.listAccounts(); const { mmiConfiguration } = this.mmiConfigurationController.store.getState(); - const addresses = Object.keys(identities); + const addresses = internalAccounts.map((account) => account.address); const tokenList = []; const { custodians } = mmiConfiguration; @@ -558,7 +564,15 @@ export default class MMIController extends EventEmitter { async handleMmiDashboardData() { await this.appStateController.getUnlockPromise(true); const keyringAccounts = await this.keyringController.getAccounts(); - const { identities } = this.preferencesController.store.getState(); + const internalAccounts = this.accountsController.listAccounts(); + // TODO: update handleMmiPortfolio to use internalAccounts + const identities = internalAccounts.map((internalAccount) => { + return { + address: internalAccount.address, + name: internalAccount.metadata.name, + }; + }); + const { metaMetricsId } = this.metaMetricsController.store.getState(); const getAccountDetails = (address) => this.custodyController.getAccountDetails(address); @@ -585,16 +599,14 @@ export default class MMIController extends EventEmitter { } async prepareMmiPortfolio() { - if (!process.env.IN_TEST) { - try { - const mmiDashboardData = await this.handleMmiDashboardData(); - const cookieSetUrls = - this.mmiConfigurationController.store.mmiConfiguration?.portfolio - ?.cookieSetUrls || []; - setDashboardCookie(mmiDashboardData, cookieSetUrls); - } catch (error) { - console.error(error); - } + try { + const mmiDashboardData = await this.handleMmiDashboardData(); + const cookieSetUrls = + this.mmiConfigurationController.store.mmiConfiguration?.portfolio + ?.cookieSetUrls || []; + setDashboardCookie(mmiDashboardData, cookieSetUrls); + } catch (error) { + console.error(error); } } @@ -642,9 +654,15 @@ export default class MMIController extends EventEmitter { async setAccountAndNetwork(origin, address, chainId) { await this.appStateController.getUnlockPromise(true); const addressToLowerCase = address.toLowerCase(); - const selectedAddress = this.preferencesController.getSelectedAddress(); + const { address: selectedAddress } = + this.accountsController.getSelectedAccount(); if (selectedAddress.toLowerCase() !== addressToLowerCase) { - this.preferencesController.setSelectedAddress(addressToLowerCase); + const internalAccounts = this.accountsController.listAccounts(); + const newAccount = internalAccounts.find( + (internalAccount) => + internalAccount.address.toLowerCase() === addressToLowerCase, + ); + this.accountsController.setSelectedAccount(newAccount.id); } const selectedChainId = parseInt( this.networkController.state.providerConfig.chainId, diff --git a/app/scripts/controllers/mmi-controller.test.js b/app/scripts/controllers/mmi-controller.test.js index f8c06d62981d..33500cddf9cf 100644 --- a/app/scripts/controllers/mmi-controller.test.js +++ b/app/scripts/controllers/mmi-controller.test.js @@ -3,14 +3,71 @@ import { KeyringController } from '@metamask/keyring-controller'; import { MmiConfigurationController } from '@metamask-institutional/custody-keyring'; import { TransactionUpdateController } from '@metamask-institutional/transaction-update'; import { SignatureController } from '@metamask/signature-controller'; +import { NetworkController } from '@metamask/network-controller'; +import { AccountsController } from '@metamask/accounts-controller'; import MMIController from './mmi-controller'; import TransactionController from './transactions'; import PreferencesController from './preferences'; import AppStateController from './app-state'; +import { ControllerMessenger } from '@metamask/base-controller'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; +import { + CHAIN_IDS, + NETWORK_TYPES, + TEST_NETWORK_TICKER_MAP, +} from '../../../shared/constants/network'; + +jest.mock('./permissions', () => ({ + getPermissionBackgroundApiMethods: () => ({ + addPermittedAccount: jest.fn().mockReturnValue(), + }), +})); + +const mockMetaMetrics = { + store: { + getState: jest.fn().mockReturnValue({ metaMetricsId: 'mock-metrics-id' }), + }, +}; +const mockExtension = { + runtime: { + id: 'mock-runtime-id', + }, +}; + +const mockAccount = { + address: '0x1', + id: 'mock-id', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, +}; +const mockAccount2 = { + address: '0x2', + id: 'mock-id-2', + metadata: { + name: 'Test Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, +}; describe('MMIController', function () { let mmiController; + let controllerMessenger; + let accountsController; + let networkController; + let preferencesControllerMessenger; beforeEach(function () { const mockMessenger = { @@ -22,6 +79,65 @@ describe('MMIController', function () { subscribe: jest.fn(), }; + networkController = new NetworkController({ + messenger: new ControllerMessenger().getRestricted({ + name: 'NetworkController', + allowedEvents: [ + 'NetworkController:stateChange', + 'NetworkController:networkWillChange', + 'NetworkController:networkDidChange', + 'NetworkController:infuraIsBlocked', + 'NetworkController:infuraIsUnblocked', + ], + }), + state: { + providerConfig: { + type: NETWORK_TYPES.SEPOLIA, + chainId: CHAIN_IDS.SEPOLIA, + ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.SEPOLIA], + }, + }, + infuraProjectId: 'mock-infura-project-id', + }); + + controllerMessenger = new ControllerMessenger(); + + accountsController = new AccountsController({ + messenger: controllerMessenger.getRestricted({ + name: 'AccountsController', + allowedEvents: [ + 'SnapController:stateChange', + 'KeyringController:accountRemoved', + 'KeyringController:stateChange', + 'AccountsController:selectedAccountChange', + ], + allowedActions: [ + 'AccountsController:setCurrentAccount', + 'AccountsController:setAccountName', + 'AccountsController:listAccounts', + 'AccountsController:updateAccounts', + 'KeyringController:getAccounts', + 'KeyringController:getKeyringsByType', + 'KeyringController:getKeyringForAccount', + ], + }), + state: { + internalAccounts: { + accounts: { + [mockAccount.id]: mockAccount, + [mockAccount2.id]: mockAccount2, + }, + selectedAccount: mockAccount.id, + }, + }, + keyringApiEnabled: false, + }); + + preferencesControllerMessenger = new ControllerMessenger().getRestricted({ + name: 'PreferencesController', + allowedEvents: ['AccountsController:selectedAccountChange'], + }); + mmiController = new MMIController({ mmiConfigurationController: new MmiConfigurationController(), keyringController: new KeyringController({ @@ -43,6 +159,9 @@ describe('MMIController', function () { getCurrentChainId: jest.fn(), getNetworkId: jest.fn(), onNetworkStateChange: jest.fn(), + blockTracker: { + getLatestBlock: jest.fn().mockResolvedValue({}), + }, }), signatureController: new SignatureController({ messenger: mockMessenger, @@ -55,11 +174,13 @@ describe('MMIController', function () { securityProviderRequest: jest.fn(), getCurrentChainId: jest.fn(), }), + preferencesController: new PreferencesController({ initState: {}, onAccountRemoved: jest.fn(), provider: {}, networkConfigurations: {}, + controllerMessenger: preferencesControllerMessenger, }), appStateController: new AppStateController({ addUnlockListener: jest.fn(), @@ -78,6 +199,11 @@ describe('MMIController', function () { messenger: mockMessenger, }), custodianEventHandlerFactory: jest.fn(), + accountsController, + controllerMessenger, + networkController, + metaMetricsController: mockMetaMetrics, + extension: mockExtension, }); }); @@ -89,8 +215,9 @@ describe('MMIController', function () { it('should have all required properties', function () { expect(mmiController.opts).toBeDefined(); expect(mmiController.mmiConfigurationController).toBeDefined(); - expect(mmiController.preferencesController).toBeDefined(); expect(mmiController.transactionUpdateController).toBeDefined(); + expect(mmiController.accountsController).toBeDefined(); + expect(mmiController.controllerMessenger).toBeDefined(); }); }); @@ -141,4 +268,37 @@ describe('MMIController', function () { ); }); }); + + describe('AccountsController:stateChange event', function () { + it('should call prepareMmiPortfolio', async () => { + mmiController.txController._trackTransactionMetricsEvent = jest.fn(); + jest.spyOn(mmiController, 'prepareMmiPortfolio'); + await controllerMessenger.publish('AccountsController:stateChange', []); + + expect(mmiController.prepareMmiPortfolio).toHaveBeenCalled(); + }); + }); + + describe('setAccountAndNetwork', function () { + it('should set a new selected account if the selectedAddress and the address from the arguments is different', async () => { + mmiController.txController._trackTransactionMetricsEvent = jest.fn(); + await mmiController.setAccountAndNetwork( + 'mock-origin', + mockAccount2.address, + '0x1', + ); + const selectedAccount = accountsController.getSelectedAccount(); + expect(selectedAccount.id).toBe(mockAccount2.id); + }); + + it('should set a new selected account the accounts are the same', async () => { + await mmiController.setAccountAndNetwork( + 'mock-origin', + mockAccount.address, + '0x1', + ); + const selectedAccount = accountsController.getSelectedAccount(); + expect(selectedAccount.id).toBe(mockAccount.id); + }); + }); }); diff --git a/app/scripts/controllers/permissions/specifications.js b/app/scripts/controllers/permissions/specifications.js index 42f685bfbf2e..2b5edcf0ca70 100644 --- a/app/scripts/controllers/permissions/specifications.js +++ b/app/scripts/controllers/permissions/specifications.js @@ -50,10 +50,10 @@ const CaveatFactories = Object.freeze({ * PermissionController. * * @param {{ - * getIdentities: () => Record, + * getInternalAccounts: () => Record, * }} options - Options bag. */ -export const getCaveatSpecifications = ({ getIdentities }) => { +export const getCaveatSpecifications = ({ getInternalAccounts }) => { return { [CaveatTypes.restrictReturnedAccounts]: { type: CaveatTypes.restrictReturnedAccounts, @@ -66,7 +66,7 @@ export const getCaveatSpecifications = ({ getIdentities }) => { }, validator: (caveat, _origin, _target) => - validateCaveatAccounts(caveat.value, getIdentities), + validateCaveatAccounts(caveat.value, getInternalAccounts), }, ///: BEGIN:ONLY_INCLUDE_IN(snaps) @@ -82,11 +82,11 @@ export const getCaveatSpecifications = ({ getIdentities }) => { * * @param {{ * getAllAccounts: () => Promise, - * getIdentities: () => Record, + * getInternalAccounts: () => Record, * }} options - Options bag. * @param options.getAllAccounts - A function that returns all Ethereum accounts * in the current MetaMask instance. - * @param options.getIdentities - A function that returns the + * @param options.getInternalAccounts - A function that returns the * `PreferencesController` identity objects for all Ethereum accounts in the * @param options.captureKeyringTypesWithMissingIdentities - A function that * captures extra error information about the "Missing identity for address" @@ -95,7 +95,7 @@ export const getCaveatSpecifications = ({ getIdentities }) => { */ export const getPermissionSpecifications = ({ getAllAccounts, - getIdentities, + getInternalAccounts, captureKeyringTypesWithMissingIdentities, }) => { return { @@ -130,31 +130,49 @@ export const getPermissionSpecifications = ({ methodImplementation: async (_args) => { const accounts = await getAllAccounts(); - const identities = getIdentities(); + const internalAccounts = getInternalAccounts(); return accounts.sort((firstAddress, secondAddress) => { - if (!identities[firstAddress]) { - captureKeyringTypesWithMissingIdentities(identities, accounts); + const firstAccount = internalAccounts.find( + (internalAccount) => + internalAccount.address.toLowerCase() === + firstAddress.toLowerCase(), + ); + + const secondAccount = internalAccounts.find( + (internalAccount) => + internalAccount.address.toLowerCase() === + secondAddress.toLowerCase(), + ); + + if (!firstAccount) { + captureKeyringTypesWithMissingIdentities( + internalAccounts, + accounts, + ); throw new Error(`Missing identity for address: "${firstAddress}".`); - } else if (!identities[secondAddress]) { - captureKeyringTypesWithMissingIdentities(identities, accounts); + } else if (!secondAccount) { + captureKeyringTypesWithMissingIdentities( + internalAccounts, + accounts, + ); throw new Error( `Missing identity for address: "${secondAddress}".`, ); } else if ( - identities[firstAddress].lastSelected === - identities[secondAddress].lastSelected + firstAccount.metadata?.lastSelected === + secondAccount.metadata?.lastSelected ) { return 0; - } else if (identities[firstAddress].lastSelected === undefined) { + } else if (firstAccount.metadata?.lastSelected === undefined) { return 1; - } else if (identities[secondAddress].lastSelected === undefined) { + } else if (secondAccount.metadata?.lastSelected === undefined) { return -1; } return ( - identities[secondAddress].lastSelected - - identities[firstAddress].lastSelected + secondAccount.metadata?.lastSelected - + firstAccount.metadata?.lastSelected ); }); }, @@ -181,17 +199,17 @@ export const getPermissionSpecifications = ({ * corresponds to a PreferencesController identity. * * @param {string[]} accounts - The accounts associated with the caveat. - * @param {() => Record} getIdentities - Gets all + * @param {() => Record} getInternalAccounts - Gets all * PreferencesController identities. */ -function validateCaveatAccounts(accounts, getIdentities) { +function validateCaveatAccounts(accounts, getInternalAccounts) { if (!Array.isArray(accounts) || accounts.length === 0) { throw new Error( `${PermissionNames.eth_accounts} error: Expected non-empty array of Ethereum addresses.`, ); } - const identities = getIdentities(); + const internalAccounts = getInternalAccounts(); accounts.forEach((address) => { if (!address || typeof address !== 'string') { throw new Error( @@ -199,7 +217,12 @@ function validateCaveatAccounts(accounts, getIdentities) { ); } - if (!identities[address]) { + if ( + !internalAccounts.find( + (internalAccount) => + internalAccount.address.toLowerCase() === address.toLowerCase(), + ) + ) { throw new Error( `${PermissionNames.eth_accounts} error: Received unrecognized address: "${address}".`, ); diff --git a/app/scripts/controllers/permissions/specifications.test.js b/app/scripts/controllers/permissions/specifications.test.js index 892ee3cadaec..95b0c3993fed 100644 --- a/app/scripts/controllers/permissions/specifications.test.js +++ b/app/scripts/controllers/permissions/specifications.test.js @@ -1,4 +1,5 @@ import { SnapCaveatType } from '@metamask/rpc-methods'; +import { EthMethod, EthAccountType } from '@metamask/keyring-api'; import { CaveatTypes, RestrictedMethods, @@ -47,10 +48,10 @@ describe('PermissionController specifications', () => { describe('restrictReturnedAccounts', () => { describe('decorator', () => { it('only returns array members included in the caveat value', async () => { - const getIdentities = jest.fn(); - const { decorator } = getCaveatSpecifications({ getIdentities })[ - CaveatTypes.restrictReturnedAccounts - ]; + const getInternalAccounts = jest.fn(); + const { decorator } = getCaveatSpecifications({ + getInternalAccounts, + })[CaveatTypes.restrictReturnedAccounts]; const method = async () => ['0x1', '0x2', '0x3']; const caveat = { @@ -62,10 +63,10 @@ describe('PermissionController specifications', () => { }); it('returns an empty array if no array members are included in the caveat value', async () => { - const getIdentities = jest.fn(); - const { decorator } = getCaveatSpecifications({ getIdentities })[ - CaveatTypes.restrictReturnedAccounts - ]; + const getInternalAccounts = jest.fn(); + const { decorator } = getCaveatSpecifications({ + getInternalAccounts, + })[CaveatTypes.restrictReturnedAccounts]; const method = async () => ['0x1', '0x2', '0x3']; const caveat = { @@ -77,10 +78,10 @@ describe('PermissionController specifications', () => { }); it('returns an empty array if the method result is an empty array', async () => { - const getIdentities = jest.fn(); - const { decorator } = getCaveatSpecifications({ getIdentities })[ - CaveatTypes.restrictReturnedAccounts - ]; + const getInternalAccounts = jest.fn(); + const { decorator } = getCaveatSpecifications({ + getInternalAccounts, + })[CaveatTypes.restrictReturnedAccounts]; const method = async () => []; const caveat = { @@ -94,10 +95,10 @@ describe('PermissionController specifications', () => { describe('validator', () => { it('rejects invalid array values', () => { - const getIdentities = jest.fn(); - const { validator } = getCaveatSpecifications({ getIdentities })[ - CaveatTypes.restrictReturnedAccounts - ]; + const getInternalAccounts = jest.fn(); + const { validator } = getCaveatSpecifications({ + getInternalAccounts, + })[CaveatTypes.restrictReturnedAccounts]; [null, 'foo', {}, []].forEach((invalidValue) => { expect(() => validator({ value: invalidValue })).toThrow( @@ -107,10 +108,10 @@ describe('PermissionController specifications', () => { }); it('rejects falsy or non-string addresses', () => { - const getIdentities = jest.fn(); - const { validator } = getCaveatSpecifications({ getIdentities })[ - CaveatTypes.restrictReturnedAccounts - ]; + const getInternalAccounts = jest.fn(); + const { validator } = getCaveatSpecifications({ + getInternalAccounts, + })[CaveatTypes.restrictReturnedAccounts]; [[{}], [[]], [null], ['']].forEach((invalidValue) => { expect(() => validator({ value: invalidValue })).toThrow( @@ -120,16 +121,42 @@ describe('PermissionController specifications', () => { }); it('rejects addresses that have no corresponding identity', () => { - const getIdentities = jest.fn().mockImplementationOnce(() => { - return { - '0x1': true, - '0x3': true, - }; + const getInternalAccounts = jest.fn().mockImplementationOnce(() => { + return [ + { + address: '0x1', + id: '21066553-d8c8-4cdc-af33-efc921cd3ca9', + metadata: { + name: 'Test Account 1', + lastSelected: 1, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + { + address: '0x3', + id: 'ff8fda69-d416-4d25-80a2-efb77bc7d4ad', + metadata: { + name: 'Test Account 3', + lastSelected: 3, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + ]; }); - const { validator } = getCaveatSpecifications({ getIdentities })[ - CaveatTypes.restrictReturnedAccounts - ]; + const { validator } = getCaveatSpecifications({ + getInternalAccounts, + })[CaveatTypes.restrictReturnedAccounts]; expect(() => validator({ value: ['0x1', '0x2', '0x3'] })).toThrow( /Received unrecognized address:/u, @@ -151,10 +178,10 @@ describe('PermissionController specifications', () => { describe('eth_accounts', () => { describe('factory', () => { it('constructs a valid eth_accounts permission', () => { - const getIdentities = jest.fn(); + const getInternalAccounts = jest.fn(); const getAllAccounts = jest.fn(); const { factory } = getPermissionSpecifications({ - getIdentities, + getInternalAccounts, getAllAccounts, })[RestrictedMethods.eth_accounts]; @@ -178,10 +205,10 @@ describe('PermissionController specifications', () => { }); it('throws an error if no approvedAccounts are specified', () => { - const getIdentities = jest.fn(); + const getInternalAccounts = jest.fn(); const getAllAccounts = jest.fn(); const { factory } = getPermissionSpecifications({ - getIdentities, + getInternalAccounts, getAllAccounts, })[RestrictedMethods.eth_accounts]; @@ -194,10 +221,10 @@ describe('PermissionController specifications', () => { }); it('throws an error if any caveats are specified directly', () => { - const getIdentities = jest.fn(); + const getInternalAccounts = jest.fn(); const getAllAccounts = jest.fn(); const { factory } = getPermissionSpecifications({ - getIdentities, + getInternalAccounts, getAllAccounts, })[RestrictedMethods.eth_accounts]; @@ -221,26 +248,71 @@ describe('PermissionController specifications', () => { describe('methodImplementation', () => { it('returns the keyring accounts in lastSelected order', async () => { - const getIdentities = jest.fn().mockImplementationOnce(() => { - return { - '0x1': { - lastSelected: 1, + const getInternalAccounts = jest.fn().mockImplementationOnce(() => { + return [ + { + address: '0x1', + id: '21066553-d8c8-4cdc-af33-efc921cd3ca9', + metadata: { + name: 'Test Account', + lastSelected: 1, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, - '0x2': {}, - '0x3': { - lastSelected: 3, + { + address: '0x2', + id: '0bd7348e-bdfe-4f67-875c-de831a583857', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, - '0x4': { - lastSelected: 3, + { + address: '0x3', + id: 'ff8fda69-d416-4d25-80a2-efb77bc7d4ad', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + lastSelected: 3, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, - }; + { + address: '0x4', + id: '0bd7348e-bdfe-4f67-875c-de831a583857', + metadata: { + name: 'Test Account', + lastSelected: 3, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + ]; }); const getAllAccounts = jest .fn() .mockImplementationOnce(() => ['0x1', '0x2', '0x3', '0x4']); const { methodImplementation } = getPermissionSpecifications({ - getIdentities, + getInternalAccounts, getAllAccounts, })[RestrictedMethods.eth_accounts]; @@ -253,22 +325,44 @@ describe('PermissionController specifications', () => { }); it('throws if a keyring account is missing an address (case 1)', async () => { - const getIdentities = jest.fn().mockImplementationOnce(() => { - return { - '0x2': { - lastSelected: 3, + const getInternalAccounts = jest.fn().mockImplementationOnce(() => { + return [ + { + address: '0x2', + id: '0bd7348e-bdfe-4f67-875c-de831a583857', + metadata: { + name: 'Test Account', + lastSelected: 2, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, - '0x3': { - lastSelected: 3, + { + address: '0x3', + id: 'ff8fda69-d416-4d25-80a2-efb77bc7d4ad', + metadata: { + name: 'Test Account', + lastSelected: 3, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, - }; + ]; }); const getAllAccounts = jest .fn() .mockImplementationOnce(() => ['0x1', '0x2', '0x3']); const { methodImplementation } = getPermissionSpecifications({ - getIdentities, + getInternalAccounts, getAllAccounts, captureKeyringTypesWithMissingIdentities: jest.fn(), })[RestrictedMethods.eth_accounts]; @@ -279,22 +373,44 @@ describe('PermissionController specifications', () => { }); it('throws if a keyring account is missing an address (case 2)', async () => { - const getIdentities = jest.fn().mockImplementationOnce(() => { - return { - '0x1': { - lastSelected: 1, + const getInternalAccounts = jest.fn().mockImplementationOnce(() => { + return [ + { + address: '0x1', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + lastSelected: 1, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, - '0x3': { - lastSelected: 3, + { + address: '0x3', + id: 'ff8fda69-d416-4d25-80a2-efb77bc7d4ad', + metadata: { + name: 'Test Account', + lastSelected: 3, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, - }; + ]; }); const getAllAccounts = jest .fn() .mockImplementationOnce(() => ['0x1', '0x2', '0x3']); const { methodImplementation } = getPermissionSpecifications({ - getIdentities, + getInternalAccounts, getAllAccounts, captureKeyringTypesWithMissingIdentities: jest.fn(), })[RestrictedMethods.eth_accounts]; @@ -307,10 +423,10 @@ describe('PermissionController specifications', () => { describe('validator', () => { it('accepts valid permissions', () => { - const getIdentities = jest.fn(); + const getInternalAccounts = jest.fn(); const getAllAccounts = jest.fn(); const { validator } = getPermissionSpecifications({ - getIdentities, + getInternalAccounts, getAllAccounts, })[RestrictedMethods.eth_accounts]; @@ -331,10 +447,10 @@ describe('PermissionController specifications', () => { }); it('rejects invalid caveats', () => { - const getIdentities = jest.fn(); + const getInternalAccounts = jest.fn(); const getAllAccounts = jest.fn(); const { validator } = getPermissionSpecifications({ - getIdentities, + getInternalAccounts, getAllAccounts, })[RestrictedMethods.eth_accounts]; diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index ac209b576480..70dbd23eeb05 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -1,5 +1,4 @@ import { ObservableStore } from '@metamask/obs-store'; -import { normalize as normalizeAddress } from 'eth-sig-util'; import { CHAIN_IDS, IPFS_DEFAULT_GATEWAY_URL, @@ -36,7 +35,6 @@ export default class PreferencesController { * Feature flags can be set by the global function `setPreference(feature, enabled)`, and so should not expose any sensitive behavior. * @property {object} store.knownMethodData Contains all data methods known by the user * @property {string} store.currentLocale The preferred language locale key - * @property {string} store.selectedAddress A hex string that matches the currently selected address in the app */ constructor(opts = {}) { const addedNonMainNetwork = Object.values( @@ -83,7 +81,7 @@ export default class PreferencesController { }, knownMethodData: {}, currentLocale: opts.initLangCode, - identities: {}, + selectedAddress: '', lostIdentities: {}, forgottenPassword: false, preferences: { @@ -114,6 +112,16 @@ export default class PreferencesController { this.store = new ObservableStore(initState); this.store.setMaxListeners(13); this.tokenListController = opts.tokenListController; + this.controllerMessenger = opts.controllerMessenger; + + // TODO: Not every controller is updated to use selectedAccount from the AccountsController + // This is a temporary stop gap until all other controllers stop subscribing to selectedAddress. + this.controllerMessenger.subscribe( + 'AccountsController:selectedAccountChange', + ({ address }) => { + this.store.updateState({ selectedAddress: address }); + }, + ); // subscribe to account removal opts.onAccountRemoved((address) => this.removeAddress(address)); @@ -326,161 +334,6 @@ export default class PreferencesController { return textDirection; } - /** - * Updates identities to only include specified addresses. Removes identities - * not included in addresses array - * - * @param {string[]} addresses - An array of hex addresses - */ - setAddresses(addresses) { - const oldIdentities = this.store.getState().identities; - - const identities = addresses.reduce((ids, address, index) => { - const oldId = oldIdentities[address] || {}; - ids[address] = { name: `Account ${index + 1}`, address, ...oldId }; - return ids; - }, {}); - - this.store.updateState({ identities }); - } - - /** - * Removes an address from state - * - * @param {string} address - A hex address - * @returns {string} the address that was removed - */ - removeAddress(address) { - const { identities } = this.store.getState(); - - if (!identities[address]) { - throw new Error(`${address} can't be deleted cause it was not found`); - } - delete identities[address]; - this.store.updateState({ identities }); - - // If the selected account is no longer valid, - // select an arbitrary other account: - if (address === this.getSelectedAddress()) { - const [selected] = Object.keys(identities); - this.setSelectedAddress(selected); - } - - return address; - } - - /** - * Adds addresses to the identities object without removing identities - * - * @param {string[]} addresses - An array of hex addresses - */ - addAddresses(addresses) { - const { identities } = this.store.getState(); - addresses.forEach((address) => { - // skip if already exists - if (identities[address]) { - return; - } - // add missing identity - const identityCount = Object.keys(identities).length; - - identities[address] = { name: `Account ${identityCount + 1}`, address }; - }); - this.store.updateState({ identities }); - } - - /** - * Synchronizes identity entries with known accounts. - * Removes any unknown identities, and returns the resulting selected address. - * - * @param {Array} addresses - known to the vault. - * @returns {Promise} selectedAddress the selected address. - */ - syncAddresses(addresses) { - if (!Array.isArray(addresses) || addresses.length === 0) { - throw new Error('Expected non-empty array of addresses. Error #11201'); - } - - const { identities, lostIdentities } = this.store.getState(); - - const newlyLost = {}; - Object.keys(identities).forEach((identity) => { - if (!addresses.includes(identity)) { - newlyLost[identity] = identities[identity]; - delete identities[identity]; - } - }); - - // Identities are no longer present. - if (Object.keys(newlyLost).length > 0) { - // store lost accounts - Object.keys(newlyLost).forEach((key) => { - lostIdentities[key] = newlyLost[key]; - }); - } - - this.store.updateState({ identities, lostIdentities }); - this.addAddresses(addresses); - - // If the selected account is no longer valid, - // select an arbitrary other account: - let selected = this.getSelectedAddress(); - if (!addresses.includes(selected)) { - [selected] = addresses; - this.setSelectedAddress(selected); - } - - return selected; - } - - /** - * Setter for the `selectedAddress` property - * - * @param {string} _address - A new hex address for an account - */ - setSelectedAddress(_address) { - const address = normalizeAddress(_address); - - const { identities } = this.store.getState(); - const selectedIdentity = identities[address]; - if (!selectedIdentity) { - throw new Error(`Identity for '${address} not found`); - } - - selectedIdentity.lastSelected = Date.now(); - this.store.updateState({ identities, selectedAddress: address }); - } - - /** - * Getter for the `selectedAddress` property - * - * @returns {string} The hex address for the currently selected account - */ - getSelectedAddress() { - return this.store.getState().selectedAddress; - } - - /** - * Sets a custom label for an account - * - * @param {string} account - the account to set a label for - * @param {string} label - the custom label for the account - * @returns {Promise} - */ - async setAccountLabel(account, label) { - if (!account) { - throw new Error( - `setAccountLabel requires a valid address, got ${String(account)}`, - ); - } - const address = normalizeAddress(account); - const { identities } = this.store.getState(); - identities[address] = identities[address] || {}; - identities[address].name = label; - this.store.updateState({ identities }); - return label; - } - /** * Updates the `featureFlags` property, which is an object. One property within that object will be set to a boolean. * diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index 106871aa5d71..c1ff8baa1fea 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -24,6 +24,7 @@ const NETWORK_CONFIGURATION_DATA = { describe('preferences controller', () => { let preferencesController; let tokenListController; + let preferencesControllerMessenger; beforeEach(() => { const tokenListMessenger = new ControllerMessenger().getRestricted({ @@ -37,11 +38,17 @@ describe('preferences controller', () => { messenger: tokenListMessenger, }); + preferencesControllerMessenger = new ControllerMessenger().getRestricted({ + name: 'PreferencesController', + allowedEvents: ['AccountsController:selectedAccountChange'], + }); + preferencesController = new PreferencesController({ initLangCode: 'en_US', tokenListController, onAccountRemoved: jest.fn(), networkConfigurations: NETWORK_CONFIGURATION_DATA, + controllerMessenger: preferencesControllerMessenger, }); }); @@ -73,139 +80,6 @@ describe('preferences controller', () => { }); }); - describe('setAddresses', () => { - it('should keep a map of addresses to names and addresses in the store', () => { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); - - const { identities } = preferencesController.store.getState(); - expect(identities).toStrictEqual({ - '0xda22le': { - name: 'Account 1', - address: '0xda22le', - }, - '0x7e57e2': { - name: 'Account 2', - address: '0x7e57e2', - }, - }); - }); - - it('should replace its list of addresses', () => { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); - preferencesController.setAddresses(['0xda22le77', '0x7e57e277']); - - const { identities } = preferencesController.store.getState(); - expect(identities).toStrictEqual({ - '0xda22le77': { - name: 'Account 1', - address: '0xda22le77', - }, - '0x7e57e277': { - name: 'Account 2', - address: '0x7e57e277', - }, - }); - }); - }); - - describe('onAccountRemoved', () => { - it('should remove an address from state', () => { - const testAddress = '0xda22le'; - let accountRemovedListener; - const onAccountRemoved = (callback) => { - accountRemovedListener = callback; - }; - preferencesController = new PreferencesController({ - initLangCode: 'en_US', - tokenListController, - initState: { - identities: { - [testAddress]: { - name: 'Account 1', - address: testAddress, - }, - }, - }, - onAccountRemoved, - networkConfigurations: NETWORK_CONFIGURATION_DATA, - }); - - accountRemovedListener(testAddress); - - expect( - preferencesController.store.getState().identities['0xda22le'], - ).toStrictEqual(undefined); - }); - - it('should throw an error if address not found', () => { - const testAddress = '0xda22le'; - let accountRemovedListener; - const onAccountRemoved = (callback) => { - accountRemovedListener = callback; - }; - preferencesController = new PreferencesController({ - initLangCode: 'en_US', - tokenListController, - initState: { - identities: { - '0x7e57e2': { - name: 'Account 1', - address: '0x7e57e2', - }, - }, - }, - onAccountRemoved, - networkConfigurations: NETWORK_CONFIGURATION_DATA, - }); - expect(() => { - accountRemovedListener(testAddress); - }).toThrow(`${testAddress} can't be deleted cause it was not found`); - }); - }); - - describe('removeAddress', () => { - it('should remove an address from state', () => { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); - - preferencesController.removeAddress('0xda22le'); - - expect( - preferencesController.store.getState().identities['0xda22le'], - ).toStrictEqual(undefined); - }); - - it('should switch accounts if the selected address is removed', () => { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); - - preferencesController.setSelectedAddress('0x7e57e2'); - preferencesController.removeAddress('0x7e57e2'); - expect(preferencesController.getSelectedAddress()).toStrictEqual( - '0xda22le', - ); - }); - }); - - describe('setAccountLabel', () => { - it('should update a label for the given account', () => { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); - - expect( - preferencesController.store.getState().identities['0xda22le'], - ).toStrictEqual({ - name: 'Account 1', - address: '0xda22le', - }); - - preferencesController.setAccountLabel('0xda22le', 'Dazzle'); - expect( - preferencesController.store.getState().identities['0xda22le'], - ).toStrictEqual({ - name: 'Dazzle', - address: '0xda22le', - }); - }); - }); - describe('setPasswordForgotten', () => { it('should default to false', () => { expect( diff --git a/app/scripts/controllers/transactions/IncomingTransactionHelper.test.ts b/app/scripts/controllers/transactions/IncomingTransactionHelper.test.ts index f20675215885..2b6aa7611dbe 100644 --- a/app/scripts/controllers/transactions/IncomingTransactionHelper.test.ts +++ b/app/scripts/controllers/transactions/IncomingTransactionHelper.test.ts @@ -1,6 +1,11 @@ import { NetworkType } from '@metamask/controller-utils'; import type { BlockTracker, NetworkState } from '@metamask/network-controller'; +import { + EthAccountType, + EthMethod, + InternalAccount, +} from '@metamask/keyring-api'; import { TransactionMeta, TransactionStatus, @@ -22,7 +27,19 @@ const NETWORK_STATE_MOCK: NetworkState = { networkId: '1', } as unknown as NetworkState; -const ADDERSS_MOCK = '0x1'; +const INTERNAL_ACCOUNT_MOCK: InternalAccount = { + address: '0x1', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, +}; const FROM_BLOCK_HEX_MOCK = '0x20'; const FROM_BLOCK_DECIMAL_MOCK = 32; @@ -34,7 +51,7 @@ const BLOCK_TRACKER_MOCK = { const CONTROLLER_ARGS_MOCK = { blockTracker: BLOCK_TRACKER_MOCK, - getCurrentAccount: () => ADDERSS_MOCK, + getCurrentAccount: () => INTERNAL_ACCOUNT_MOCK, getNetworkState: () => NETWORK_STATE_MOCK, remoteTransactionSource: {} as RemoteTransactionSource, transactionLimit: 1, @@ -137,7 +154,7 @@ describe('IncomingTransactionHelper', () => { ); expect(remoteTransactionSource.fetchTransactions).toHaveBeenCalledWith({ - address: ADDERSS_MOCK, + address: INTERNAL_ACCOUNT_MOCK.address, currentChainId: NETWORK_STATE_MOCK.providerConfig.chainId, currentNetworkId: NETWORK_STATE_MOCK.networkId, fromBlock: expect.any(Number), @@ -173,7 +190,7 @@ describe('IncomingTransactionHelper', () => { ...CONTROLLER_ARGS_MOCK, remoteTransactionSource, lastFetchedBlockNumbers: { - [`${NETWORK_STATE_MOCK.providerConfig.chainId}#${ADDERSS_MOCK}`]: + [`${NETWORK_STATE_MOCK.providerConfig.chainId}#${INTERNAL_ACCOUNT_MOCK.address}`]: FROM_BLOCK_DECIMAL_MOCK, }, }); @@ -414,7 +431,7 @@ describe('IncomingTransactionHelper', () => { ); expect(lastFetchedBlockNumbers).toStrictEqual({ - [`${NETWORK_STATE_MOCK.providerConfig.chainId}#${ADDERSS_MOCK}`]: + [`${NETWORK_STATE_MOCK.providerConfig.chainId}#${INTERNAL_ACCOUNT_MOCK.address}`]: parseInt(TRANSACTION_MOCK_2.blockNumber as string, 10), }); }); @@ -472,7 +489,7 @@ describe('IncomingTransactionHelper', () => { TRANSACTION_MOCK_2, ]), lastFetchedBlockNumbers: { - [`${NETWORK_STATE_MOCK.providerConfig.chainId}#${ADDERSS_MOCK}`]: + [`${NETWORK_STATE_MOCK.providerConfig.chainId}#${INTERNAL_ACCOUNT_MOCK.address}`]: parseInt(TRANSACTION_MOCK_2.blockNumber as string, 10), }, }); diff --git a/app/scripts/controllers/transactions/IncomingTransactionHelper.ts b/app/scripts/controllers/transactions/IncomingTransactionHelper.ts index 3acb6372637f..f90d0220a157 100644 --- a/app/scripts/controllers/transactions/IncomingTransactionHelper.ts +++ b/app/scripts/controllers/transactions/IncomingTransactionHelper.ts @@ -3,6 +3,7 @@ import type { BlockTracker, NetworkState } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; import log from 'loglevel'; +import { InternalAccount } from '@metamask/keyring-api'; import { TransactionMeta } from '../../../../shared/constants/transaction'; import { RemoteTransactionSource } from './types'; @@ -15,7 +16,7 @@ export class IncomingTransactionHelper { #blockTracker: BlockTracker; - #getCurrentAccount: () => string; + #getCurrentAccount: () => InternalAccount; #getLocalTransactions: () => TransactionMeta[]; @@ -49,7 +50,7 @@ export class IncomingTransactionHelper { updateTransactions, }: { blockTracker: BlockTracker; - getCurrentAccount: () => string; + getCurrentAccount: () => InternalAccount; getNetworkState: () => NetworkState; getLocalTransactions?: () => TransactionMeta[]; isEnabled?: () => boolean; @@ -115,7 +116,7 @@ export class IncomingTransactionHelper { ); const fromBlock = this.#getFromBlock(latestBlockNumber); - const address = this.#getCurrentAccount(); + const { address } = this.#getCurrentAccount(); const currentChainId = this.#getCurrentChainId(); const currentNetworkId = this.#getCurrentNetworkId(); @@ -256,7 +257,7 @@ export class IncomingTransactionHelper { } #getBlockNumberKey(): string { - return `${this.#getCurrentChainId()}#${this.#getCurrentAccount().toLowerCase()}`; + return `${this.#getCurrentChainId()}#${this.#getCurrentAccount().address.toLowerCase()}`; } #canStart(): boolean { diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 5aee2965ea6c..373c608474ca 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -154,6 +154,7 @@ export default class TransactionController extends EventEmitter { this._getCurrentAccountEIP1559Compatibility = opts.getCurrentAccountEIP1559Compatibility; this.preferencesStore = opts.preferencesStore || new ObservableStore({}); + this.getCurrentAccount = opts.getCurrentAccount; this.provider = opts.provider; this.getPermittedAccounts = opts.getPermittedAccounts; this.blockTracker = opts.blockTracker; @@ -224,7 +225,7 @@ export default class TransactionController extends EventEmitter { this.incomingTransactionHelper = new IncomingTransactionHelper({ blockTracker: this.blockTracker, - getCurrentAccount: () => this.getSelectedAddress(), + getCurrentAccount: () => this.getCurrentAccount(), getNetworkState: () => this._getNetworkState(), isEnabled: () => Boolean( @@ -1585,13 +1586,13 @@ export default class TransactionController extends EventEmitter { if (origin === ORIGIN_METAMASK) { // Assert the from address is the selected address - if (normalizedTxParams.from !== this.getSelectedAddress()) { + if (normalizedTxParams.from !== this.getSelectedAccountAddress()) { throw ethErrors.rpc.internal({ message: `Internally initiated transaction is using invalid account.`, data: { origin, fromAddress: normalizedTxParams.from, - selectedAddress: this.getSelectedAddress(), + selectedAddress: this.getSelectedAccountAddress(), }, }); } @@ -1916,8 +1917,7 @@ export default class TransactionController extends EventEmitter { this.getState = () => this.memStore.getState(); /** @returns {string} the user selected address */ - this.getSelectedAddress = () => - this.preferencesStore.getState().selectedAddress; + this.getSelectedAccountAddress = () => this.getCurrentAccount().address; /** @returns {Array} transactions whos status is unapproved */ this.getUnapprovedTxCount = () => @@ -2496,8 +2496,8 @@ export default class TransactionController extends EventEmitter { eip_1559_version: eip1559Version, gas_edit_type: 'none', gas_edit_attempted: 'none', - account_type: await this.getAccountType(this.getSelectedAddress()), - device_model: await this.getDeviceModel(this.getSelectedAddress()), + account_type: await this.getAccountType(this.getSelectedAccountAddress()), + device_model: await this.getDeviceModel(this.getSelectedAccountAddress()), asset_type: assetType, token_standard: tokenStandard, transaction_type: transactionType, diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js index 046ed4bae3ad..7056f1766842 100644 --- a/app/scripts/controllers/transactions/index.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -6,6 +6,7 @@ import { TransactionFactory } from '@ethereumjs/tx'; import { ObservableStore } from '@metamask/obs-store'; import { ApprovalType } from '@metamask/controller-utils'; import sinon from 'sinon'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { errorCodes, ethErrors } from 'eth-rpc-errors'; import { @@ -62,6 +63,20 @@ const TRANSACTION_META_MOCK = { }, }; +const MOCK_INTERNAL_ACCOUNT = { + id: '2d47e693-26c2-47cb-b374-6151199bbe3f', + address: '0x88bb7F89eB5e5b30D3e15a57C68DBe03C6aCCB21', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, +}; + async function flushPromises() { await new Promise((resolve) => setImmediate(resolve)); } @@ -75,6 +90,7 @@ describe('Transaction Controller', function () { networkStatusStore, preferencesStore, getCurrentChainId, + getCurrentAccount, messengerMock, resultCallbacksMock, updateSpy, @@ -107,6 +123,8 @@ describe('Transaction Controller', function () { getCurrentChainId = sinon.stub().callsFake(() => currentChainId); + getCurrentAccount = sinon.stub().callsFake(() => MOCK_INTERNAL_ACCOUNT); + resultCallbacksMock = { success: sinon.spy(), error: sinon.spy(), @@ -146,6 +164,7 @@ describe('Transaction Controller', function () { getProviderConfig: () => providerConfig, getPermittedAccounts: () => undefined, getCurrentChainId, + getCurrentAccount, getParticipateInMetrics: () => false, trackMetaMetricsEvent: () => undefined, createEventFragment: () => undefined, @@ -352,13 +371,26 @@ describe('Transaction Controller', function () { describe('#addTransaction', function () { const selectedAddress = '0xc684832530fcbddae4b4230a47e991ddcec2831d'; + const selectedInternalAccount = { + address: selectedAddress, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }; const recipientAddress = '0xc684832530fcbddae4b4230a47e991ddcec2831d'; let txMeta, txParams, getPermittedAccounts, signStub, - getSelectedAddress, + getSelectedAccountAddress, getDefaultGasFees; beforeEach(function () { @@ -381,9 +413,9 @@ describe('Transaction Controller', function () { .stub(txController, 'getPermittedAccounts') .returns([txParams.from]); - getSelectedAddress = sinon - .stub(txController, 'getSelectedAddress') - .returns(selectedAddress); + getSelectedAccountAddress = sinon + .stub(txController, 'getSelectedAccountAddress') + .returns(selectedInternalAccount.address); getDefaultGasFees = sinon .stub(txController, '_getDefaultGasFees') @@ -394,7 +426,7 @@ describe('Transaction Controller', function () { txController.txStateManager._addTransactionsToState([]); getPermittedAccounts.restore(); signStub?.restore(); - getSelectedAddress.restore(); + getSelectedAccountAddress.restore(); getDefaultGasFees.restore(); }); @@ -1130,9 +1162,22 @@ describe('Transaction Controller', function () { describe('#createCancelTransaction', function () { const selectedAddress = '0x1678a085c290ebd122dc42cba69373b5953b831d'; + const selectedInternalAccount = { + address: selectedAddress, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }; const recipientAddress = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'; - let getSelectedAddress, + let getSelectedAccountAddress, getPermittedAccounts, getDefaultGasFees, getDefaultGasLimit; @@ -1141,9 +1186,9 @@ describe('Transaction Controller', function () { '0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8'; providerResultStub.eth_sendRawTransaction = hash; - getSelectedAddress = sinon - .stub(txController, 'getSelectedAddress') - .returns(selectedAddress); + getSelectedAccountAddress = sinon + .stub(txController, 'getSelectedAccountAddress') + .returns(selectedInternalAccount.address); getDefaultGasFees = sinon .stub(txController, '_getDefaultGasFees') .returns({}); @@ -1156,7 +1201,7 @@ describe('Transaction Controller', function () { }); afterEach(function () { - getSelectedAddress.restore(); + getSelectedAccountAddress.restore(); getPermittedAccounts.restore(); getDefaultGasFees.restore(); getDefaultGasLimit.restore(); @@ -1596,9 +1641,22 @@ describe('Transaction Controller', function () { let txParams; let expectedTxParams; const selectedAddress = '0x1678a085c290ebd122dc42cba69373b5953b831d'; + const selectedInternalAccount = { + address: selectedAddress, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }; const recipientAddress = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'; - let getSelectedAddress, + let getSelectedAccountAddress, getPermittedAccounts, getDefaultGasFees, getDefaultGasLimit; @@ -1611,9 +1669,9 @@ describe('Transaction Controller', function () { '0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8'; providerResultStub.eth_sendRawTransaction = hash; - getSelectedAddress = sinon - .stub(txController, 'getSelectedAddress') - .returns(selectedAddress); + getSelectedAccountAddress = sinon + .stub(txController, 'getSelectedAccountAddress') + .returns(selectedInternalAccount.address); getDefaultGasFees = sinon .stub(txController, '_getDefaultGasFees') .returns({}); @@ -1649,7 +1707,7 @@ describe('Transaction Controller', function () { afterEach(function () { addTransactionSpy.restore(); approveTransactionSpy.restore(); - getSelectedAddress.restore(); + getSelectedAccountAddress.restore(); getPermittedAccounts.restore(); getDefaultGasFees.restore(); getDefaultGasLimit.restore(); diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index 787db1fa86d1..17b4b4ab298d 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -83,6 +83,7 @@ export default class AccountTracker { this.getNetworkIdentifier = opts.getNetworkIdentifier; this.preferencesController = opts.preferencesController; this.onboardingController = opts.onboardingController; + this.accountsController = opts.accountsController; // subscribe to account removal opts.onAccountRemoved((address) => this.removeAccount([address])); @@ -97,20 +98,24 @@ export default class AccountTracker { }, this.onboardingController.store.getState()), ); - this.preferencesController.store.subscribe( - previousValueComparator(async (prevState, currState) => { - const { selectedAddress: prevSelectedAddress } = prevState; - const { - selectedAddress: currSelectedAddress, - useMultiAccountBalanceChecker, - } = currState; + this.selectedAccount = this.accountsController.getSelectedAccount(); + + this.controllerMessenger = opts.controllerMessenger; + + this.controllerMessenger.subscribe( + 'AccountsController:selectedAccountChange', + (newAccount) => { + const { useMultiAccountBalanceChecker } = + this.preferencesController.store.getState(); + if ( - prevSelectedAddress !== currSelectedAddress && + this.selectedAccount.id !== newAccount.id && !useMultiAccountBalanceChecker ) { + this.selectedAccount = newAccount; this._updateAccounts(); } - }, this.onboardingController.store.getState()), + }, ); this.ethersProvider = new Web3Provider(this._provider); } @@ -251,7 +256,8 @@ export default class AccountTracker { addresses = Object.keys(accounts); } else { - const selectedAddress = this.preferencesController.getSelectedAddress(); + const selectedAddress = + this.accountsController.getSelectedAccount().address; addresses = [selectedAddress]; } diff --git a/app/scripts/lib/account-tracker.test.js b/app/scripts/lib/account-tracker.test.js index 0edfe96bdf01..4ba333852485 100644 --- a/app/scripts/lib/account-tracker.test.js +++ b/app/scripts/lib/account-tracker.test.js @@ -1,4 +1,6 @@ import EventEmitter from 'events'; +import { ControllerMessenger } from '@metamask/base-controller'; +import { AccountsController } from '@metamask/accounts-controller'; import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts'; @@ -36,7 +38,9 @@ describe('Account Tracker', () => { blockTrackerStub, providerResultStub, useMultiAccountBalanceChecker, - accountTracker; + accountTracker, + controllerMessenger, + accountsController; beforeEach(() => { providerResultStub = { @@ -53,6 +57,22 @@ describe('Account Tracker', () => { blockTrackerStub.getCurrentBlock = noop; blockTrackerStub.getLatestBlock = noop; + controllerMessenger = new ControllerMessenger(); + + const accountsControllerMessenger = controllerMessenger.getRestricted({ + name: 'AccountsController', + allowedEvents: [ + 'SnapController:stateChange', + 'KeyringController:accountRemoved', + 'KeyringController:stateChange', + ], + }); + + accountsController = new AccountsController({ + state: {}, + messenger: accountsControllerMessenger, + }); + accountTracker = new AccountTracker({ provider, blockTracker: blockTrackerStub, @@ -70,6 +90,8 @@ describe('Account Tracker', () => { getState: noop, }, }, + controllerMessenger, + accountsController, onAccountRemoved: jest.fn(), }); }); @@ -164,6 +186,8 @@ describe('Account Tracker', () => { getState: noop, }, }, + controllerMessenger, + accountsController, onAccountRemoved, }); accountRemovedListener(VALID_ADDRESS); diff --git a/app/scripts/lib/backup.js b/app/scripts/lib/backup.js index 01c49e0b14cb..bf5367cc787f 100644 --- a/app/scripts/lib/backup.js +++ b/app/scripts/lib/backup.js @@ -5,11 +5,13 @@ export default class Backup { const { preferencesController, addressBookController, + accountsController, networkController, trackMetaMetricsEvent, } = opts; this.preferencesController = preferencesController; + this.accountsController = accountsController; this.addressBookController = addressBookController; this.networkController = networkController; this._trackMetaMetricsEvent = trackMetaMetricsEvent; @@ -17,11 +19,10 @@ export default class Backup { async restoreUserData(jsonString) { const existingPreferences = this.preferencesController.store.getState(); - const { preferences, addressBook, network } = JSON.parse(jsonString); + const { preferences, addressBook, network, internalAccounts } = + JSON.parse(jsonString); if (preferences) { - preferences.identities = existingPreferences.identities; preferences.lostIdentities = existingPreferences.lostIdentities; - preferences.selectedAddress = existingPreferences.selectedAddress; this.preferencesController.store.updateState(preferences); } @@ -34,7 +35,11 @@ export default class Backup { this.networkController.loadBackup(network); } - if (preferences || addressBook || network) { + if (internalAccounts) { + this.accountsController.loadBackup(internalAccounts); + } + + if (preferences || addressBook || network || internalAccounts) { this._trackMetaMetricsEvent({ event: 'User Data Imported', category: 'Backup', @@ -45,6 +50,9 @@ export default class Backup { async backupUserData() { const userData = { preferences: { ...this.preferencesController.store.getState() }, + internalAccounts: { + internalAccounts: this.accountsController.state.internalAccounts, + }, addressBook: { ...this.addressBookController.state }, network: { networkConfigurations: @@ -55,9 +63,7 @@ export default class Backup { /** * We can remove these properties since we will won't be restoring identities from backup */ - delete userData.preferences.identities; delete userData.preferences.lostIdentities; - delete userData.preferences.selectedAddress; const result = JSON.stringify(userData); diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js index 8097e9030f4b..3125648da02c 100644 --- a/app/scripts/lib/backup.test.js +++ b/app/scripts/lib/backup.test.js @@ -1,19 +1,11 @@ /** * @jest-environment node */ +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import Backup from './backup'; function getMockPreferencesController() { const mcState = { - getSelectedAddress: jest.fn().mockReturnValue('0x01'), - selectedAddress: '0x01', - identities: { - '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B': { - address: '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B', - lastSelected: 1655380342907, - name: 'Account 3', - }, - }, lostIdentities: { '0xfd59bbe569376e3d3e4430297c3c69ea93f77435': { address: '0xfd59bbe569376e3d3e4430297c3c69ea93f77435', @@ -69,6 +61,25 @@ function getMockNetworkController() { return { state, loadBackup }; } +function getMockAccountsController() { + const state = { + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, + }; + + const loadBackup = (internalAccounts) => { + Object.assign(state, { internalAccounts }); + }; + + return { + state, + loadBackup, + getSelectedAccount: () => 'mock-id', + }; +} + const jsonData = JSON.stringify({ addressBook: { addressBook: { @@ -148,6 +159,25 @@ const jsonData = JSON.stringify({ customNetworkListEnabled: false, textDirection: 'auto', }, + internalAccounts: { + accounts: { + 'fcbcdca4-cc47-4bc8-b455-b14421e9277e': { + address: '0x129af01f4b770b30615f049790e1e206ebaa7b10', + id: 'fcbcdca4-cc47-4bc8-b455-b14421e9277e', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + lastSelected: 1693289751176, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'fcbcdca4-cc47-4bc8-b455-b14421e9277e', + }, }); describe('Backup', function () { @@ -156,6 +186,7 @@ describe('Backup', function () { preferencesController: getMockPreferencesController(), addressBookController: getMockAddressBookController(), networkController: getMockNetworkController(), + accountsController: getMockAccountsController(), trackMetaMetricsEvent: jest.fn(), }); }; @@ -163,8 +194,8 @@ describe('Backup', function () { describe('constructor', function () { it('should setup correctly', async function () { const backup = getBackup(); - const selectedAddress = backup.preferencesController.getSelectedAddress(); - expect(selectedAddress).toStrictEqual('0x01'); + const selectedAccount = backup.accountsController.getSelectedAccount(); + expect(selectedAccount).toStrictEqual('mock-id'); }); it('should restore backup', async function () { @@ -191,18 +222,6 @@ describe('Backup', function () { 'network-configuration-id-4' ].chainId, ).toStrictEqual('0x89'); - // make sure identities are not lost after restore - expect( - backup.preferencesController.store.identities[ - '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B' - ].lastSelected, - ).toStrictEqual(1655380342907); - - expect( - backup.preferencesController.store.identities[ - '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B' - ].name, - ).toStrictEqual('Account 3'); expect( backup.preferencesController.store.lostIdentities[ @@ -215,10 +234,6 @@ describe('Backup', function () { '0xfd59bbe569376e3d3e4430297c3c69ea93f77435' ].name, ).toStrictEqual('Ledger 1'); - // make sure selected address is not lost after restore - expect(backup.preferencesController.store.selectedAddress).toStrictEqual( - '0x01', - ); // check address book backup expect( @@ -238,6 +253,36 @@ describe('Backup', function () { '0x42EB768f2244C8811C63729A21A3569731535f06' ].isEns, ).toBeFalsy(); + + // make sure the internal accounts are restored + expect( + backup.accountsController.state.internalAccounts.accounts[ + 'fcbcdca4-cc47-4bc8-b455-b14421e9277e' + ], + ).toStrictEqual({ + address: '0x129af01f4b770b30615f049790e1e206ebaa7b10', + id: 'fcbcdca4-cc47-4bc8-b455-b14421e9277e', + metadata: { + keyring: { type: 'HD Key Tree' }, + lastSelected: 1693289751176, + name: 'Account 1', + }, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + options: {}, + type: 'eip155:eoa', + }); + + // make sure selected account is restored + expect( + backup.accountsController.state.internalAccounts.selectedAccount, + ).toBe('fcbcdca4-cc47-4bc8-b455-b14421e9277e'); }); }); }); diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 8a1be42a182c..eb6be705ca71 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -32,6 +32,12 @@ export const SENTRY_BACKGROUND_STATE = { accounts: false, currentBlockGasLimit: true, }, + AccountsController: { + internalAccounts: { + accounts: false, + selectedAccount: false, + }, + }, AddressBookController: { addressBook: false, }, @@ -171,13 +177,13 @@ export const SENTRY_BACKGROUND_STATE = { dismissSeedBackUpReminder: true, featureFlags: true, forgottenPassword: true, - identities: false, incomingTransactionsPreferences: true, ipfsGateway: false, isLineaMainnetReleased: true, knownMethodData: false, ledgerTransportType: true, lostIdentities: false, + selectedAddress: false, openSeaEnabled: true, preferences: { autoLockTimeLimit: true, @@ -186,7 +192,6 @@ export const SENTRY_BACKGROUND_STATE = { showTestNetworks: true, useNativeCurrencyAsPrimaryCurrency: true, }, - selectedAddress: false, snapRegistryList: false, theme: true, transactionSecurityCheckEnabled: true, diff --git a/app/scripts/metamask-controller.actions.test.js b/app/scripts/metamask-controller.actions.test.js index a0dba444b2df..dfe2a88c5c87 100644 --- a/app/scripts/metamask-controller.actions.test.js +++ b/app/scripts/metamask-controller.actions.test.js @@ -139,27 +139,46 @@ describe('MetaMaskController', function () { }); describe('#addNewAccount', function () { + const getAccountsAfterRemovingLastSelected = (internalAccounts) => { + internalAccounts.map((account) => { + return { + ...account, + lastSelected: undefined, + }; + }); + }; it('two parallel calls with same accountCount give same result', async function () { await metamaskController.createNewVaultAndKeychain('test@123'); const [addNewAccountResult1, addNewAccountResult2] = await Promise.all([ metamaskController.addNewAccount(1), metamaskController.addNewAccount(1), ]); - assert.equal(addNewAccountResult1, addNewAccountResult2); + + // Ignore the lastSelected property + assert.deepEqual( + getAccountsAfterRemovingLastSelected(addNewAccountResult1.accounts), + getAccountsAfterRemovingLastSelected(addNewAccountResult2.accounts), + ); }); it('two successive calls with same accountCount give same result', async function () { await metamaskController.createNewVaultAndKeychain('test@123'); const addNewAccountResult1 = await metamaskController.addNewAccount(1); const addNewAccountResult2 = await metamaskController.addNewAccount(1); - assert.equal(addNewAccountResult1, addNewAccountResult2); + + // Ignore the lastSelected property + assert.deepEqual( + getAccountsAfterRemovingLastSelected(addNewAccountResult1.accounts), + getAccountsAfterRemovingLastSelected(addNewAccountResult2.accounts), + ); }); it('two successive calls with different accountCount give different results', async function () { await metamaskController.createNewVaultAndKeychain('test@123'); const addNewAccountResult1 = await metamaskController.addNewAccount(1); const addNewAccountResult2 = await metamaskController.addNewAccount(2); - assert.notEqual(addNewAccountResult1, addNewAccountResult2); + console.log(addNewAccountResult1, addNewAccountResult2); + assert.notDeepEqual(addNewAccountResult1, addNewAccountResult2); }); }); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index ed70842d596b..ed1e8a7668b3 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -101,6 +101,7 @@ import { } from '@metamask/controller-utils'; import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; +import { AccountsController } from '@metamask/accounts-controller'; ///: BEGIN:ONLY_INCLUDE_IN(petnames) import { NameController, @@ -437,6 +438,12 @@ export default class MetamaskController extends EventEmitter { state: initState.TokenListController, }); + this.preferencesControllerMessenger = + this.controllerMessenger.getRestricted({ + name: 'PreferencesController', + allowedEvents: ['AccountsController:selectedAccountChange'], + }); + this.preferencesController = new PreferencesController({ initState: initState.PreferencesController, initLangCode: opts.initLangCode, @@ -447,24 +454,39 @@ export default class MetamaskController extends EventEmitter { tokenListController: this.tokenListController, provider: this.provider, networkConfigurations: this.networkController.state.networkConfigurations, + controllerMessenger: this.preferencesControllerMessenger, }); const tokensControllerMessenger = this.controllerMessenger.getRestricted({ name: 'TokensController', allowedActions: ['ApprovalController:addRequest'], - allowedEvents: ['NetworkController:stateChange'], + allowedEvents: [ + 'NetworkController:stateChange', + 'AccountsController:selectedAccountChange', + ], }); this.tokensController = new TokensController({ messenger: tokensControllerMessenger, chainId: this.networkController.state.providerConfig.chainId, - onPreferencesStateChange: this.preferencesController.store.subscribe.bind( - this.preferencesController.store, - ), + // TODO: The tokens controller currently does not support internalAccounts. This is done to match the behavior of the previous tokens controller subscription. + onPreferencesStateChange: (listener) => + this.controllerMessenger.subscribe( + `AccountsController:selectedAccountChange`, + (newlySelectedInternalAccount) => { + listener({ selectedAddress: newlySelectedInternalAccount.address }); + }, + ), onNetworkStateChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, 'NetworkController:stateChange', ), - config: { provider: this.provider }, + config: { + provider: this.provider, + selectedAddress: + initState?.AccountsController?.internalAccounts?.accounts[ + initState?.AccountsController?.internalAccounts?.selectedAccount + ]?.address ?? '', + }, state: initState.TokensController, }); @@ -770,58 +792,6 @@ export default class MetamaskController extends EventEmitter { initState: initState.OnboardingController, }); - // account tracker watches balances, nonces, and any code at their address - this.accountTracker = new AccountTracker({ - provider: this.provider, - blockTracker: this.blockTracker, - getCurrentChainId: () => - this.networkController.state.providerConfig.chainId, - getNetworkIdentifier: () => { - const { type, rpcUrl } = this.networkController.state.providerConfig; - return type === NETWORK_TYPES.RPC ? rpcUrl : type; - }, - preferencesController: this.preferencesController, - onboardingController: this.onboardingController, - initState: - isManifestV3 && - isFirstMetaMaskControllerSetup === false && - initState.AccountTracker?.accounts - ? { accounts: initState.AccountTracker.accounts } - : { accounts: {} }, - onAccountRemoved: this.controllerMessenger.subscribe.bind( - this.controllerMessenger, - 'KeyringController:accountRemoved', - ), - }); - - // start and stop polling for balances based on activeControllerConnections - this.on('controllerConnectionChanged', (activeControllerConnections) => { - const { completedOnboarding } = - this.onboardingController.store.getState(); - if (activeControllerConnections > 0 && completedOnboarding) { - this.triggerNetworkrequests(); - } else { - this.stopNetworkRequests(); - } - }); - - this.onboardingController.store.subscribe( - previousValueComparator(async (prevState, currState) => { - const { completedOnboarding: prevCompletedOnboarding } = prevState; - const { completedOnboarding: currCompletedOnboarding } = currState; - if (!prevCompletedOnboarding && currCompletedOnboarding) { - this.triggerNetworkrequests(); - } - }, this.onboardingController.store.getState()), - ); - - this.cachedBalancesController = new CachedBalancesController({ - accountTracker: this.accountTracker, - getCurrentChainId: () => - this.networkController.state.providerConfig.chainId, - initState: initState.CachedBalancesController, - }); - let additionalKeyrings = [keyringBuilderFactory(QRHardwareKeyring)]; if (this.canUseHardwareWallets()) { @@ -861,6 +831,10 @@ export default class MetamaskController extends EventEmitter { removeAccount: async (address) => { await this.removeAccount(address); }, + addressExists: async (address) => { + const addresses = await this.coreKeyringController.getAccounts(); + return addresses.includes(address.toLowerCase()); + }, }); builder.type = SnapKeyring.type; return builder; @@ -896,21 +870,39 @@ export default class MetamaskController extends EventEmitter { encryptor: opts.encryptor || undefined, cacheEncryptionKey: isManifestV3, messenger: keyringControllerMessenger, - removeIdentity: this.preferencesController.removeAddress.bind( - this.preferencesController, - ), - setAccountLabel: this.preferencesController.setAccountLabel.bind( - this.preferencesController, - ), - setSelectedAddress: this.preferencesController.setSelectedAddress.bind( - this.preferencesController, - ), - syncIdentities: this.preferencesController.syncAddresses.bind( - this.preferencesController, - ), - updateIdentities: this.preferencesController.setAddresses.bind( - this.preferencesController, - ), + // TO BE REMOVED START // + // These call backs will be removed when the keyring controller is updated to use internal accounts + removeIdentity: async () => + await this.accountsController.updateAccounts(), + setAccountLabel: (address, label) => { + const accountToBeNamed = this.accountsController + .listAccounts() + .find( + (account) => + account.address.toLowerCase() === address.toLowerCase(), + ); + if (accountToBeNamed) { + this.accountsController.setAccountName(accountToBeNamed.id, label); + } + }, + setSelectedAddress: (address) => { + const accountToBeSet = this.accountsController + .listAccounts() + .find( + (account) => + account.address.toLowerCase() === address.toLowerCase(), + ); + if (accountToBeSet) { + this.accountsController.setSelectedAccount(accountToBeSet); + } + }, + syncIdentities: async () => { + await this.accountsController.updateAccounts(); + }, + // TODO: This will be removed, the accounts controller listens to the keyring controller state changes. + // eslint-disable-next-line no-empty-function + updateIdentities: () => {}, + // TO BE REMOVED END // }); this.controllerMessenger.subscribe('KeyringController:unlock', () => @@ -929,8 +921,30 @@ export default class MetamaskController extends EventEmitter { this.keyringController = this.coreKeyringController.getEthKeyringController(); - const getIdentities = () => - this.preferencesController.store.getState().identities; + const accountsControllerMessenger = this.controllerMessenger.getRestricted({ + name: 'AccountsController', + allowedEvents: [ + 'SnapController:stateChange', + 'KeyringController:accountRemoved', + 'KeyringController:stateChange', + 'AccountsController:selectedAccountChange', + ], + allowedActions: [ + 'AccountsController:setCurrentAccount', + 'AccountsController:setAccountName', + 'AccountsController:listAccounts', + 'AccountsController:updateAccounts', + 'KeyringController:getAccounts', + 'KeyringController:getKeyringsByType', + 'KeyringController:getKeyringForAccount', + ], + }); + + this.accountsController = new AccountsController({ + messenger: accountsControllerMessenger, + state: initState.AccountsController, + keyringApiEnabled: process.env.KEYRING_API_ENABLED ?? false, + }); this.permissionController = new PermissionController({ messenger: this.controllerMessenger.getRestricted({ @@ -946,26 +960,36 @@ export default class MetamaskController extends EventEmitter { ], }), state: initState.PermissionController, - caveatSpecifications: getCaveatSpecifications({ getIdentities }), + caveatSpecifications: getCaveatSpecifications({ + getInternalAccounts: this.accountsController.listAccounts.bind( + this.accountsController, + ), + }), permissionSpecifications: { ...getPermissionSpecifications({ - getIdentities, + getInternalAccounts: this.accountsController.listAccounts.bind( + this.accountsController, + ), getAllAccounts: this.keyringController.getAccounts.bind( this.keyringController, ), captureKeyringTypesWithMissingIdentities: ( - identities = {}, + internalAccounts = [], accounts = [], ) => { const accountsMissingIdentities = accounts.filter( - (address) => !identities[address], + (address) => + !internalAccounts.find( + (account) => + account.address.toLowerCase() === address.toLowerCase(), + ), ); const keyringTypesWithMissingIdentities = accountsMissingIdentities.map((address) => this.coreKeyringController.getAccountKeyringType(address), ); - const identitiesCount = Object.keys(identities || {}).length; + const internalAccountCount = internalAccounts.length; const accountTrackerCount = Object.keys( this.accountTracker.store.getState().accounts || {}, @@ -973,7 +997,7 @@ export default class MetamaskController extends EventEmitter { captureException( new Error( - `Attempt to get permission specifications failed because their were ${accounts.length} accounts, but ${identitiesCount} identities, and the ${keyringTypesWithMissingIdentities} keyrings included accounts with missing identities. Meanwhile, there are ${accountTrackerCount} accounts in the account tracker.`, + `Attempt to get permission specifications failed because their 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.`, ), ); }, @@ -1155,6 +1179,60 @@ export default class MetamaskController extends EventEmitter { ///: END:ONLY_INCLUDE_IN + // account tracker watches balances, nonces, and any code at their address + this.accountTracker = new AccountTracker({ + provider: this.provider, + blockTracker: this.blockTracker, + getCurrentChainId: () => + this.networkController.state.providerConfig.chainId, + getNetworkIdentifier: () => { + const { type, rpcUrl } = this.networkController.state.providerConfig; + return type === NETWORK_TYPES.RPC ? rpcUrl : type; + }, + preferencesController: this.preferencesController, + accountsController: this.accountsController, + controllerMessenger: this.controllerMessenger, + onboardingController: this.onboardingController, + initState: + isManifestV3 && + isFirstMetaMaskControllerSetup === false && + initState.AccountTracker?.accounts + ? { accounts: initState.AccountTracker.accounts } + : { accounts: {} }, + onAccountRemoved: this.controllerMessenger.subscribe.bind( + this.controllerMessenger, + 'KeyringController:accountRemoved', + ), + }); + + // start and stop polling for balances based on activeControllerConnections + this.on('controllerConnectionChanged', (activeControllerConnections) => { + const { completedOnboarding } = + this.onboardingController.store.getState(); + if (activeControllerConnections > 0 && completedOnboarding) { + this.triggerNetworkrequests(); + } else { + this.stopNetworkRequests(); + } + }); + + this.onboardingController.store.subscribe( + previousValueComparator(async (prevState, currState) => { + const { completedOnboarding: prevCompletedOnboarding } = prevState; + const { completedOnboarding: currCompletedOnboarding } = currState; + if (!prevCompletedOnboarding && currCompletedOnboarding) { + this.triggerNetworkrequests(); + } + }, this.onboardingController.store.getState()), + ); + + this.cachedBalancesController = new CachedBalancesController({ + accountTracker: this.accountTracker, + getCurrentChainId: () => + this.networkController.state.providerConfig.chainId, + initState: initState.CachedBalancesController, + }); + ///: BEGIN:ONLY_INCLUDE_IN(desktop) this.desktopController = new DesktopController({ initState: initState.DesktopController, @@ -1169,12 +1247,18 @@ export default class MetamaskController extends EventEmitter { 'NetworkController:stateChange', 'KeyringController:lock', 'KeyringController:unlock', + 'AccountsController:selectedAccountChange', ], }); this.detectTokensController = new DetectTokensController({ messenger: detectTokensControllerMessenger, preferences: this.preferencesController, tokensController: this.tokensController, + getCurrentSelectedAccount: + this.accountsController.getSelectedAccount.bind( + this.accountsController, + ), + controllerMessenger: this.controllerMessenger, assetsContractController: this.assetsContractController, network: this.networkController, tokenList: this.tokenListController, @@ -1190,7 +1274,11 @@ export default class MetamaskController extends EventEmitter { this.alertController = new AlertController({ initState: initState.AlertController, - preferencesStore: this.preferencesController.store, + controllerMessenger: this.controllerMessenger, + getCurrentSelectedAccount: + this.accountsController.getSelectedAccount.bind( + this.accountsController, + ), }); ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) @@ -1212,6 +1300,7 @@ export default class MetamaskController extends EventEmitter { this.backup = new Backup({ preferencesController: this.preferencesController, addressBookController: this.addressBookController, + accountsController: this.accountsController, networkController: this.networkController, trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, @@ -1247,6 +1336,7 @@ export default class MetamaskController extends EventEmitter { getCurrentChainId: () => this.networkController.state.providerConfig.chainId, preferencesStore: this.preferencesController.store, + getCurrentAccount: () => this.accountsController.getSelectedAccount(), txHistoryLimit: 60, signTransaction: this.keyringController.signTransaction.bind( this.keyringController, @@ -1479,6 +1569,8 @@ export default class MetamaskController extends EventEmitter { networkController: this.networkController, permissionController: this.permissionController, signatureController: this.signatureController, + accountsController: this.accountsController, + controllerMessenger: this.controllerMessenger, platform: this.platform, extension: this.extension, }); @@ -1614,7 +1706,7 @@ export default class MetamaskController extends EventEmitter { ) => { if (innerOrigin === ORIGIN_METAMASK) { const selectedAddress = - this.preferencesController.getSelectedAddress(); + this.accountsController.getSelectedAccount().address; return selectedAddress ? [selectedAddress] : []; } else if (this.isUnlocked()) { return await this.getPermittedAccounts(innerOrigin, { @@ -1718,6 +1810,7 @@ export default class MetamaskController extends EventEmitter { this.store.updateStructure({ AppStateController: this.appStateController.store, + AccountsController: this.accountsController, AppMetadataController: this.appMetadataController.store, TransactionController: this.txController.store, KeyringController: this.keyringController.store, @@ -1769,6 +1862,7 @@ export default class MetamaskController extends EventEmitter { this.memStore = new ComposableObservableStore({ config: { AppStateController: this.appStateController.store, + AccountsController: this.accountsController, AppMetadataController: this.appMetadataController.store, NetworkController: this.networkController, CachedBalancesController: this.cachedBalancesController.store, @@ -2075,7 +2169,8 @@ export default class MetamaskController extends EventEmitter { let lastSelectedAddress; this.preferencesController.store.subscribe(async (state) => { - const { selectedAddress, currentLocale } = state; + const { currentLocale } = state; + const { chainId } = this.networkController.state.providerConfig; await updateCurrentLocale(currentLocale); @@ -2084,13 +2179,18 @@ export default class MetamaskController extends EventEmitter { } else { this.txController.stopIncomingTransactionPolling(); } - - if (selectedAddress && selectedAddress !== lastSelectedAddress) { - lastSelectedAddress = selectedAddress; - await this._onAccountChange(selectedAddress); - } }); + this.controllerMessenger.subscribe( + `${this.accountsController.name}:selectedAccountChange`, + async (account) => { + if (account.address && account.address !== lastSelectedAddress) { + lastSelectedAddress = account.address; + await this._onAccountChange(account.address); + } + }, + ); + // This handles account changes every time relevant permission state // changes, for any reason. this.controllerMessenger.subscribe( @@ -2310,6 +2410,7 @@ export default class MetamaskController extends EventEmitter { */ getApi() { const { + accountsController, addressBookController, alertController, appStateController, @@ -2470,14 +2571,9 @@ export default class MetamaskController extends EventEmitter { this.networkController, ), // PreferencesController - setSelectedAddress: preferencesController.setSelectedAddress.bind( - preferencesController, - ), addToken: tokensController.addToken.bind(tokensController), updateTokenType: tokensController.updateTokenType.bind(tokensController), - setAccountLabel: preferencesController.setAccountLabel.bind( - preferencesController, - ), + setFeatureFlag: preferencesController.setFeatureFlag.bind( preferencesController, ), @@ -2515,6 +2611,16 @@ export default class MetamaskController extends EventEmitter { ), ///: END:ONLY_INCLUDE_IN + // AccountsController + setSelectedInternalAccount: + accountsController.setSelectedAccount.bind(accountsController), + + setAccountName: + accountsController.setAccountName.bind(accountsController), + + setAccountLabel: + accountsController.setAccountName.bind(accountsController), + // AssetsContractController getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this), @@ -3034,9 +3140,10 @@ export default class MetamaskController extends EventEmitter { vault = await this.keyringController.createNewVaultAndKeychain( password, ); - const addresses = await this.keyringController.getAccounts(); - this.preferencesController.setAddresses(addresses); - this.selectFirstIdentity(); + await this.accountsController.updateAccounts(); + this.accountsController.setSelectedAccount( + this.accountsController.listAccounts()[0].id, + ); } return vault; @@ -3059,10 +3166,6 @@ export default class MetamaskController extends EventEmitter { const seedPhraseAsBuffer = Buffer.from(encodedSeedPhrase); - // clear known identities - this.preferencesController.setAddresses([]); - - // clear permissions this.permissionController.clearState(); ///: BEGIN:ONLY_INCLUDE_IN(snaps) @@ -3089,6 +3192,8 @@ export default class MetamaskController extends EventEmitter { const ethQuery = new EthQuery(this.provider); accounts = await this.coreKeyringController.getAccounts(); + const firstAccount = accounts[0]; + lastBalance = await this.getBalance( accounts[accounts.length - 1], ethQuery, @@ -3102,10 +3207,18 @@ export default class MetamaskController extends EventEmitter { lastBalance = await this.getBalance(addedAccountAddress, ethQuery); } + await this.accountsController.updateAccounts(); + const internalAccounts = this.accountsController.listAccounts(); + // remove extra zero balance account potentially created from seeking ahead if (accounts.length > 1 && lastBalance === '0x0') { - await this.removeAccount(accounts[accounts.length - 1]); - accounts = await this.coreKeyringController.getAccounts(); + await this.removeAccount( + internalAccounts.find( + (account) => + account.address.toLowerCase() === + accounts[accounts.length - 1].toLowerCase(), + ).id, + ); } // This must be set as soon as possible to communicate to the @@ -3116,7 +3229,14 @@ export default class MetamaskController extends EventEmitter { this.preferencesController.getLedgerTransportPreference(); this.setLedgerTransportPreference(transportPreference); - this.selectFirstIdentity(); + await this.accountsController.updateAccounts(); + + // set first hd account as the selected account by default + this.accountsController.setSelectedAccount( + this.accountsController + .listAccounts() + .find((account) => account.address === firstAccount).id, + ); return vault; } finally { @@ -3205,6 +3325,10 @@ export default class MetamaskController extends EventEmitter { this.preferencesController.getLedgerTransportPreference(); this.setLedgerTransportPreference(transportPreference); + + await this.accountsController.updateAccounts(); + + return this.keyringController.fullUpdate(); } async _loginUser() { @@ -3291,10 +3415,20 @@ export default class MetamaskController extends EventEmitter { /** * Sets the first address in the state to the selected address */ - selectFirstIdentity() { - const { identities } = this.preferencesController.store.getState(); - const [address] = Object.keys(identities); - this.preferencesController.setSelectedAddress(address); + selectFirstAccount() { + const [account] = this.accountsController.listAccounts(); + this.accountsController.setSelectedAccount(account.id); + } + + setAccountName(accountId, accountName) { + const accounts = this.accountsController.listAccounts(); + if (accounts.some((account) => account.metadata.name === accountName)) { + throw new Error( + `An account with the name ${accountName} already exists.`, + ); + } + + this.accountsController.setAccountName(accountId, accountName); } /** @@ -3528,28 +3662,20 @@ export default class MetamaskController extends EventEmitter { const keyring = await this.getKeyringForDevice(deviceName, hdPath); keyring.setAccountToUnlock(index); - const oldAccounts = await this.keyringController.getAccounts(); const keyState = await this.keyringController.addNewAccount(keyring); - const newAccounts = await this.keyringController.getAccounts(); - this.preferencesController.setAddresses(newAccounts); - newAccounts.forEach((address) => { - if (!oldAccounts.includes(address)) { - const label = this.getAccountLabel( - deviceName === HardwareDeviceNames.qr - ? keyring.getName() - : deviceName, - index, - hdPathDescription, - ); - // Set the account label to Trezor 1 / Ledger 1 / QR Hardware 1, etc - this.preferencesController.setAccountLabel(address, label); - // Select the account - this.preferencesController.setSelectedAddress(address); - } - }); + await this.accountsController.updateAccounts(); + const newlyAddedAccount = this.accountsController.getSelectedAccount(); + await this.accountsController.setAccountName( + newlyAddedAccount.id, + this.getAccountLabel( + deviceName === HardwareDeviceNames.qr ? keyring.getName() : deviceName, + index, + hdPathDescription, + ), + ); - const { identities } = this.preferencesController.store.getState(); - return { ...keyState, identities }; + const accounts = this.accountsController.listAccounts(); + return { ...keyState, accounts }; } // @@ -3560,9 +3686,10 @@ export default class MetamaskController extends EventEmitter { * Adds a new account to the default (first) HD seed phrase Keyring. * * @param accountCount - * @returns {Promise} The address of the newly-created account. + * @param accountName + * @returns {Promise<{accounts: InternalAccounts[]}>} A list of all the accounts after creating a new one. */ - async addNewAccount(accountCount) { + async addNewAccount(accountCount, accountName) { const isActionMetricsQueueE2ETest = this.appStateController.store.getState()[ACTION_QUEUE_METRICS_E2E_TEST]; @@ -3570,16 +3697,25 @@ export default class MetamaskController extends EventEmitter { await new Promise((resolve) => setTimeout(resolve, 5_000)); } - const oldAccounts = await this.coreKeyringController.getAccounts(); - const { addedAccountAddress } = await this.coreKeyringController.addNewAccount(accountCount); - if (!oldAccounts.includes(addedAccountAddress)) { - this.preferencesController.setSelectedAddress(addedAccountAddress); + await this.accountsController.updateAccounts(); + const currentAccounts = this.accountsController.listAccounts(); + + const newAccount = currentAccounts.find( + (account) => + account.address.toLowerCase() === addedAccountAddress.toLowerCase(), + ); + + if (accountName) { + this.accountsController.setAccountName(newAccount.id, accountName); } + this.accountsController.setSelectedAccount(newAccount.id); - return addedAccountAddress; + return { + accounts: currentAccounts, + }; } /** @@ -3606,7 +3742,8 @@ export default class MetamaskController extends EventEmitter { * @returns {Promise} The current selected address. */ async resetAccount() { - const selectedAddress = this.preferencesController.getSelectedAddress(); + const selectedAddress = + this.accountsController.getSelectedAccount().address; this.txController.wipeTransactions(selectedAddress); this.networkController.resetConnection(); @@ -3665,12 +3802,17 @@ export default class MetamaskController extends EventEmitter { /** * Removes an account from state / storage. * - * @param {string[]} address - A hex address + * @param {string[]} accountId - A uuid of the account to remove. */ - async removeAccount(address) { + async removeAccount(accountId) { + const { address } = this.accountsController.getAccountExpect(accountId); + // Remove all associated permissions this.removeAllAccountPermissions(address); + // Remove account from the account tracker controller + this.accountTracker.removeAccount([address]); + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) this.custodyController.removeAccount(address); ///: END:ONLY_INCLUDE_IN(build-mmi) @@ -3702,8 +3844,16 @@ export default class MetamaskController extends EventEmitter { strategy, args, ); - // set new account as selected - this.preferencesController.setSelectedAddress(importedAccountAddress); + await this.accountsController.updateAccounts(); + const importedAccount = await this.accountsController + .listAccounts() + .find( + (account) => + account.address.toLowerCase() === + importedAccountAddress.toLowerCase(), + ); + + this.accountsController.setSelectedAccount(importedAccount.id); } // --------------------------------------------------------------------------- @@ -4559,8 +4709,7 @@ export default class MetamaskController extends EventEmitter { return; } - // Ensure preferences + identities controller know about all addresses - this.preferencesController.syncAddresses(addresses); + // Ensure account tracker controller know about all addresses this.accountTracker.syncWithAddresses(addresses); } diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index c8e6c070e14d..55576a1cfd48 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -14,6 +14,9 @@ import { METAMASK_STALELIST_FILE, METAMASK_HOTLIST_DIFF_FILE, } from '@metamask/phishing-controller'; +import { EthMethod, EthAccountType } from '@metamask/keyring-api'; +import { v4 as uuid } from 'uuid'; +import { sha256FromString } from 'ethereumjs-util'; import { NetworkType } from '@metamask/controller-utils'; import { ControllerMessenger } from '@metamask/base-controller'; import { TransactionStatus } from '../../shared/constants/transaction'; @@ -113,6 +116,20 @@ const TEST_ADDRESS_3 = '0xeb9e64b93097bc15f01f13eae97015c57ab64823'; const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle'; const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'; +const TEST_INTERNAL_ACCOUNT = { + id: '2d47e693-26c2-47cb-b374-6151199bbe3f', + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + lastSelected: 0, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, +}; const NOTIFICATION_ID = 'NHL8f2eSSTn9TKBamRLiU'; @@ -132,6 +149,16 @@ const MAINNET_CHAIN_ID = '0x1'; const firstTimeState = { config: {}, + AccountsController: { + internalAccounts: { + accounts: { + '': { + address: '', + }, + }, + selectedAccount: '', + }, + }, NetworkController: { providerConfig: { type: NETWORK_TYPES.RPC, @@ -375,22 +402,22 @@ describe('MetaMaskController', () => { const password = 'password'; await metamaskController.createNewVaultAndKeychain(password); - const fakeAddress = '0xbad0'; - metamaskController.preferencesController.addAddresses([fakeAddress]); await metamaskController.submitPassword(password); - const identities = Object.keys( - metamaskController.preferencesController.store.getState().identities, - ); const addresses = await metamaskController.coreKeyringController.getAccounts(); - identities.forEach((identity) => { - expect(addresses).toContain(identity); + const internalAccounts = + metamaskController.accountsController.listAccounts(); + + internalAccounts.forEach((account) => { + expect(addresses.includes(account.address) === true).toBe(true); }); addresses.forEach((address) => { - expect(identities).toContain(address); + expect( + internalAccounts.find((account) => account.address === address), + ).toBeDefined(); }); }); }); @@ -412,8 +439,6 @@ describe('MetaMaskController', () => { describe('#createNewVaultAndKeychain', () => { it('can only create new vault on keyringController once', async () => { - jest.spyOn(metamaskController, 'selectFirstIdentity').mockReturnValue(); - const password = 'a-fake-password'; await metamaskController.createNewVaultAndKeychain(password); @@ -450,29 +475,57 @@ describe('MetaMaskController', () => { ); let endTime = Date.now(); - const firstVaultIdentities = cloneDeep( - metamaskController.getState().identities, + const originalVaultAccounts = + metamaskController.getState().internalAccounts; + + const testAccount = cloneDeep( + Object.values(originalVaultAccounts.accounts).find( + (account) => account.address === TEST_ADDRESS, + ), ); + expect( - firstVaultIdentities[TEST_ADDRESS].lastSelected >= startTime && - firstVaultIdentities[TEST_ADDRESS].lastSelected <= endTime, - ).toStrictEqual(true); - delete firstVaultIdentities[TEST_ADDRESS].lastSelected; - expect(firstVaultIdentities).toStrictEqual({ - [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, + testAccount.metadata.lastSelected >= startTime && + testAccount.metadata.lastSelected <= endTime, + ).toBe(true); + + delete testAccount.metadata.lastSelected; + + expect(testAccount).toStrictEqual({ + id: 'e26b5b50-739e-4d6a-a9d1-a9163f480a52', + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + metadata: { + name: DEFAULT_LABEL, + keyring: { type: 'HD Key Tree' }, + }, }); - await metamaskController.preferencesController.setAccountLabel( - TEST_ADDRESS, - 'Account Foo', - ); + metamaskController.setAccountName(testAccount.id, 'Account Foo'); + + const labeledVaultAccounts = + metamaskController.getState().internalAccounts; - const labelledFirstVaultIdentities = cloneDeep( - metamaskController.getState().identities, + const labeledAccount = cloneDeep( + Object.values(labeledVaultAccounts.accounts).find( + (account) => account.address === TEST_ADDRESS, + ), ); - delete labelledFirstVaultIdentities[TEST_ADDRESS].lastSelected; - expect(labelledFirstVaultIdentities).toStrictEqual({ - [TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' }, + + delete labeledAccount.metadata.lastSelected; + + expect(labeledAccount).toStrictEqual({ + id: 'e26b5b50-739e-4d6a-a9d1-a9163f480a52', + address: TEST_ADDRESS, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + metadata: { + name: 'Account Foo', + keyring: { type: 'HD Key Tree' }, + }, }); startTime = Date.now(); @@ -482,18 +535,31 @@ describe('MetaMaskController', () => { ); endTime = Date.now(); - const secondVaultIdentities = cloneDeep( - metamaskController.getState().identities, + const secondVaultAccounts = + metamaskController.getState().internalAccounts; + + const secondTestAccount = cloneDeep( + Object.values(secondVaultAccounts.accounts).find( + (account) => account.address === TEST_ADDRESS_ALT, + ), ); + expect( - secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected >= startTime && - secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected <= endTime, - ).toStrictEqual(true); - delete secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected; - expect(secondVaultIdentities).toStrictEqual({ - [TEST_ADDRESS_ALT]: { - address: TEST_ADDRESS_ALT, + secondTestAccount.metadata.lastSelected >= startTime && + secondTestAccount.metadata.lastSelected <= endTime, + ).toBe(true); + + delete secondTestAccount.metadata.lastSelected; + + expect(secondTestAccount).toStrictEqual({ + id: 'f73954ab-4605-459c-b0ff-23978b190709', + address: TEST_ADDRESS_ALT, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + metadata: { name: DEFAULT_LABEL, + keyring: { type: 'HD Key Tree' }, }, }); }); @@ -521,14 +587,32 @@ describe('MetaMaskController', () => { TEST_SEED, ); - const identities = cloneDeep(metamaskController.getState().identities); + const restoredVaultAccounts = + metamaskController.getState().internalAccounts; + + const testAccount = cloneDeep( + Object.values(restoredVaultAccounts.accounts).find( + (account) => account.address === TEST_ADDRESS, + ), + ); + expect( - identities[TEST_ADDRESS].lastSelected >= startTime && - identities[TEST_ADDRESS].lastSelected <= Date.now(), - ).toStrictEqual(true); - delete identities[TEST_ADDRESS].lastSelected; - expect(identities).toStrictEqual({ - [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, + testAccount.metadata.lastSelected >= startTime && + testAccount.metadata.lastSelected <= Date.now(), + ).toBe(true); + + delete testAccount.metadata.lastSelected; + + expect(testAccount).toStrictEqual({ + id: 'e26b5b50-739e-4d6a-a9d1-a9163f480a52', + address: TEST_ADDRESS, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + metadata: { + name: DEFAULT_LABEL, + keyring: { type: 'HD Key Tree' }, + }, }); }); }); @@ -573,38 +657,33 @@ describe('MetaMaskController', () => { }); }); - describe('#selectFirstIdentity', () => { - let identities, address; - - beforeEach(() => { - address = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; - identities = { - '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { - address, - name: 'Account 1', - }, - '0xc42edfcc21ed14dda456aa0756c153f7985d8813': { - address: '0xc42edfcc21ed14dda456aa0756c153f7985d8813', - name: 'Account 2', - }, - }; - metamaskController.preferencesController.store.updateState({ - identities, - }); - metamaskController.selectFirstIdentity(); + describe('#selectFirstAccount', function () { + const expectedId = uuid({ + random: sha256FromString(TEST_ADDRESS).slice(0, 16), }); - it('changes preferences controller select address', () => { - const preferenceControllerState = - metamaskController.preferencesController.store.getState(); - expect(preferenceControllerState.selectedAddress).toStrictEqual( - address, + beforeEach(async function () { + await metamaskController.createNewVaultAndRestore( + 'password', + TEST_SEED, ); + await metamaskController.addNewAccount(1); + await metamaskController.addNewAccount(2); + + metamaskController.selectFirstAccount(); + }); + it('changes accounts controller select account', function () { + const selectedAccount = + metamaskController.accountsController.getSelectedAccount(); + expect(selectedAccount.id).toStrictEqual(expectedId); }); - it('changes metamask controller selected address', () => { + it('changes metamask controller selected account', function () { const metamaskState = metamaskController.getState(); - expect(metamaskState.selectedAddress).toStrictEqual(address); + console.log(metamaskState); + expect(metamaskState.internalAccounts.selectedAccount).toStrictEqual( + expectedId, + ); }); }); @@ -728,6 +807,30 @@ describe('MetaMaskController', () => { describe('unlockHardwareWalletAccount', () => { const accountToUnlock = 10; + + const generateInternalAccountMock = (index) => { + return { + address: `0x${index}`, + id: `mock-id-${index}`, + metadata: { + name: `Trezor ${index}`, + keyring: { + type: 'Trezor Hardware', + }, + }, + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + }; + }; beforeEach(async () => { jest.spyOn(window, 'open').mockReturnValue(); jest @@ -739,15 +842,22 @@ describe('MetaMaskController', () => { .mockResolvedValueOnce(['0x1']) .mockResolvedValueOnce(['0x2']) .mockResolvedValueOnce(['0x3']); + jest - .spyOn(metamaskController.preferencesController, 'setAddresses') - .mockReturnValue(); + .spyOn(metamaskController.accountsController, 'getAccountExpect') + .mockReturnValue(generateInternalAccountMock(11)); jest - .spyOn(metamaskController.preferencesController, 'setSelectedAddress') - .mockReturnValue(); + .spyOn(metamaskController.accountsController, 'listAccounts') + .mockReturnValue([ + generateInternalAccountMock(0), + generateInternalAccountMock(1), + generateInternalAccountMock(2), + generateInternalAccountMock(3), + ]); jest - .spyOn(metamaskController.preferencesController, 'setAccountLabel') - .mockReturnValue(); + .spyOn(metamaskController.accountsController, 'updateAccounts') + .mockResolvedValue(); + jest.spyOn(metamaskController.accountsController, 'setAccountName'); await metamaskController .connectHardware(HardwareDeviceNames.trezor, 0, `m/44'/1'/0'/0`) @@ -773,28 +883,20 @@ describe('MetaMaskController', () => { ).toHaveBeenCalledTimes(1); }); - it('should call keyringController.getAccounts', async () => { + it('should call accountsController.updateAccounts', async function () { expect( - metamaskController.keyringController.getAccounts, - ).toHaveBeenCalledTimes(3); - }); - - it('should call preferencesController.setAddresses', async () => { - expect( - metamaskController.preferencesController.setAddresses, + metamaskController.accountsController.updateAccounts, ).toHaveBeenCalledTimes(1); }); - it('should call preferencesController.setSelectedAddress', async () => { + it('should set the name of the account', async function () { expect( - metamaskController.preferencesController.setSelectedAddress, + metamaskController.accountsController.setAccountName, ).toHaveBeenCalledTimes(1); - }); - it('should call preferencesController.setAccountLabel', async () => { expect( - metamaskController.preferencesController.setAccountLabel, - ).toHaveBeenCalledTimes(1); + metamaskController.accountsController.setAccountName, + ).toHaveBeenCalledWith('mock-id-11', 'Trezor 11'); }); }); @@ -825,8 +927,8 @@ describe('MetaMaskController', () => { describe('#resetAccount', () => { it('wipes transactions from only the correct network id and with the selected address', async () => { jest - .spyOn(metamaskController.preferencesController, 'getSelectedAddress') - .mockReturnValue('0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'); + .spyOn(metamaskController.accountsController, 'getSelectedAccount') + .mockReturnValue(TEST_INTERNAL_ACCOUNT); jest .spyOn(metamaskController.txController.txStateManager, 'getNetworkId') .mockReturnValue(42); @@ -867,14 +969,22 @@ describe('MetaMaskController', () => { describe('#removeAccount', () => { let ret; - const addressToRemove = '0x1'; let mockKeyring; + const accountIdToBeRemoved = 'mock-id'; + const accountAddressTobeRemoved = '0x1'; beforeEach(async () => { mockKeyring = { getAccounts: jest.fn().mockResolvedValue([]), destroy: jest.fn(), }; + jest + .spyOn(metamaskController.accountsController, 'getAccountExpect') + .mockReturnValue({ + id: accountIdToBeRemoved, + address: accountAddressTobeRemoved, + }); + jest .spyOn(metamaskController.keyringController, 'removeAccount') .mockReturnValue(); @@ -889,18 +999,18 @@ describe('MetaMaskController', () => { ) .mockResolvedValue(mockKeyring); - ret = await metamaskController.removeAccount(addressToRemove); + ret = await metamaskController.removeAccount(accountIdToBeRemoved); }); it('should call keyringController.removeAccount', async () => { expect( metamaskController.keyringController.removeAccount, - ).toHaveBeenCalledWith(addressToRemove); + ).toHaveBeenCalledWith(accountAddressTobeRemoved); }); it('should call metamaskController.removeAllAccountPermissions', async () => { expect( metamaskController.removeAllAccountPermissions, - ).toHaveBeenCalledWith(addressToRemove); + ).toHaveBeenCalledWith(accountAddressTobeRemoved); }); it('should return address', async () => { expect(ret).toStrictEqual('0x1'); @@ -908,7 +1018,7 @@ describe('MetaMaskController', () => { it('should call coreKeyringController.getKeyringForAccount', async () => { expect( metamaskController.coreKeyringController.getKeyringForAccount, - ).toHaveBeenCalledWith(addressToRemove); + ).toHaveBeenCalledWith(accountAddressTobeRemoved); }); it('should call keyring.destroy', async () => { expect(mockKeyring.destroy).toHaveBeenCalledTimes(1); @@ -1089,9 +1199,6 @@ describe('MetaMaskController', () => { describe('#_onKeyringControllerUpdate', () => { it('should do nothing if there are no keyrings in state', async () => { - jest - .spyOn(metamaskController.preferencesController, 'syncAddresses') - .mockReturnValue(); jest .spyOn(metamaskController.accountTracker, 'syncWithAddresses') .mockReturnValue(); @@ -1099,9 +1206,6 @@ describe('MetaMaskController', () => { const oldState = metamaskController.getState(); await metamaskController._onKeyringControllerUpdate({ keyrings: [] }); - expect( - metamaskController.preferencesController.syncAddresses, - ).not.toHaveBeenCalled(); expect( metamaskController.accountTracker.syncWithAddresses, ).not.toHaveBeenCalled(); @@ -1109,9 +1213,6 @@ describe('MetaMaskController', () => { }); it('should sync addresses if there are keyrings in state', async () => { - jest - .spyOn(metamaskController.preferencesController, 'syncAddresses') - .mockReturnValue(); jest .spyOn(metamaskController.accountTracker, 'syncWithAddresses') .mockReturnValue(); @@ -1125,9 +1226,6 @@ describe('MetaMaskController', () => { ], }); - expect( - metamaskController.preferencesController.syncAddresses, - ).toHaveBeenCalledWith(['0x1', '0x2']); expect( metamaskController.accountTracker.syncWithAddresses, ).toHaveBeenCalledWith(['0x1', '0x2']); @@ -1135,9 +1233,6 @@ describe('MetaMaskController', () => { }); it('should NOT update selected address if already unlocked', async () => { - jest - .spyOn(metamaskController.preferencesController, 'syncAddresses') - .mockReturnValue(); jest .spyOn(metamaskController.accountTracker, 'syncWithAddresses') .mockReturnValue(); @@ -1152,9 +1247,6 @@ describe('MetaMaskController', () => { ], }); - expect( - metamaskController.preferencesController.syncAddresses, - ).toHaveBeenCalledWith(['0x1', '0x2']); expect( metamaskController.accountTracker.syncWithAddresses, ).toHaveBeenCalledWith(['0x1', '0x2']); @@ -1559,10 +1651,9 @@ describe('MetaMaskController', () => { TransactionController.prototype.updateIncomingTransactions, ).not.toHaveBeenCalled(); - await metamaskController.preferencesController.store.subscribe.mock.lastCall[0]( - { - selectedAddress: 'foo', - }, + metamaskController.controllerMessenger.publish( + 'AccountsController:selectedAccountChange', + TEST_INTERNAL_ACCOUNT, ); expect( diff --git a/app/scripts/migrations/100.test.ts b/app/scripts/migrations/100.test.ts new file mode 100644 index 000000000000..bc797df569db --- /dev/null +++ b/app/scripts/migrations/100.test.ts @@ -0,0 +1,274 @@ +import { v4 as uuid } from 'uuid'; +import { sha256FromString } from 'ethereumjs-util'; +import { EthMethod, InternalAccount } from '@metamask/keyring-api'; +import { migrate } from './100'; + +const MOCK_ADDRESS = '0x0'; +const MOCK_ADDRESS_2 = '0x1'; + +function addressToUUID(address: string): string { + return uuid({ + random: sha256FromString(address).slice(0, 16), + }); +} + +interface Identity { + name: string; + address: string; + lastSelected?: number; +} + +interface Identities { + [key: string]: Identity; +} + +function createMockPreferenceControllerState( + identities: Identity[] = [{ name: 'Account 1', address: MOCK_ADDRESS }], + selectedAddress: string = MOCK_ADDRESS, +): { + identities: Identities; + selectedAddress: string; +} { + const state: { + identities: Identities; + selectedAddress: string; + } = { + identities: {}, + selectedAddress, + }; + + identities.forEach(({ address, name, lastSelected }) => { + state.identities[address] = { + address, + name, + lastSelected, + }; + }); + + return state; +} + +function expectedInternalAccount( + address: string, + nickname: string, + lastSelected?: number, +): InternalAccount { + return { + address, + id: addressToUUID(address), + metadata: { + name: nickname, + keyring: { + type: 'HD Key Tree', + }, + lastSelected: lastSelected ? expect.any(Number) : undefined, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: 'eip155:eoa', + }; +} + +function createMockState( + preferenceState: { + identities: Identities; + selectedAddress: string; + } = createMockPreferenceControllerState(), +) { + return { + PreferencesController: { + ...preferenceState, + }, + }; +} + +describe('migration #100', () => { + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: 99 }, + data: createMockState(), + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.meta).toStrictEqual({ version: 100 }); + }); + + describe('createDefaultAccountsController', () => { + it('creates default state for accounts controller', async () => { + const oldData = createMockState(); + + const oldStorage = { + meta: { version: 99 }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + + const expectedUUID = addressToUUID(MOCK_ADDRESS); + const resultInternalAccount = expectedInternalAccount( + MOCK_ADDRESS, + 'Account 1', + ); + + expect(newStorage.data.AccountsController).toStrictEqual({ + internalAccounts: { + accounts: { + [expectedUUID]: resultInternalAccount, + }, + selectedAccount: expectedUUID, + }, + }); + }); + }); + + describe('moveIdentitiesToAccountsController', () => { + const expectedUUID = addressToUUID(MOCK_ADDRESS); + const expectedUUID2 = addressToUUID(MOCK_ADDRESS_2); + + it('should move the identities into AccountsController as internal accounts', async () => { + const oldData = createMockState(); + + const oldStorage = { + meta: { version: 99 }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + AccountsController: { + internalAccounts: { + accounts: { + [expectedUUID]: expectedInternalAccount( + MOCK_ADDRESS, + `Account 1`, + ), + }, + selectedAccount: expectedUUID, + }, + }, + PreferencesController: expect.any(Object), + }); + }); + + it('should keep the same name from the identities', async () => { + const oldData = createMockState( + createMockPreferenceControllerState([ + { name: 'a random name', address: MOCK_ADDRESS }, + ]), + ); + const oldStorage = { + meta: { version: 99 }, + data: oldData, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual({ + PreferencesController: expect.any(Object), + AccountsController: { + internalAccounts: { + accounts: { + [expectedUUID]: expectedInternalAccount( + MOCK_ADDRESS, + `a random name`, + ), + }, + selectedAccount: expectedUUID, + }, + }, + }); + }); + + it('should be able to handle multiple identities', async () => { + const oldData = createMockState({ + identities: { + [MOCK_ADDRESS]: { name: 'Account 1', address: MOCK_ADDRESS }, + [MOCK_ADDRESS_2]: { name: 'Account 2', address: MOCK_ADDRESS_2 }, + }, + selectedAddress: MOCK_ADDRESS, + }); + + const oldStorage = { + meta: { version: 99 }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + AccountsController: { + internalAccounts: { + accounts: { + [expectedUUID]: expectedInternalAccount( + MOCK_ADDRESS, + `Account 1`, + ), + [expectedUUID2]: expectedInternalAccount( + MOCK_ADDRESS_2, + `Account 2`, + ), + }, + selectedAccount: expectedUUID, + }, + }, + PreferencesController: expect.any(Object), + }); + }); + }); + + describe('moveSelectedAddressToAccountsController', () => { + it('should select the same account as the selected address', async () => { + const oldData = createMockState(); + const oldStorage = { + meta: { version: 99 }, + data: oldData, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual({ + PreferencesController: expect.any(Object), + AccountsController: { + internalAccounts: { + accounts: expect.any(Object), + selectedAccount: addressToUUID(MOCK_ADDRESS), + }, + }, + }); + }); + + it("should leave selectedAccount as empty is there aren't any selectedAddress", async () => { + const oldData = createMockState(); + const oldStorage = { + meta: { version: 99 }, + data: oldData, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual({ + PreferencesController: expect.any(Object), + AccountsController: { + internalAccounts: { + accounts: expect.any(Object), + selectedAccount: addressToUUID(MOCK_ADDRESS), + }, + }, + }); + }); + }); + + describe('removeIdentitiesAndSelectedAddressFromPreferencesController', () => { + it('removes identities and selectedAddress in PreferenceController state', async () => { + const oldData = createMockState(); + + const oldStorage = { + meta: { version: 99 }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + AccountsController: expect.any(Object), + PreferencesController: {}, + }); + }); + }); +}); diff --git a/app/scripts/migrations/100.ts b/app/scripts/migrations/100.ts new file mode 100644 index 000000000000..2d4b0e10612b --- /dev/null +++ b/app/scripts/migrations/100.ts @@ -0,0 +1,119 @@ +import { + EthAccountType, + InternalAccount, + EthMethod, +} from '@metamask/keyring-api'; +import { sha256FromString } from 'ethereumjs-util'; +import { v4 as uuid } from 'uuid'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +interface Identity { + name: string; + address: string; + lastSelected?: number; +} + +export const version = 100; + +/** + * This migration does the following: + * + * - Creates a default state for AccountsController. + * - Moves identites and selectedAddress from the PreferencesController to the AccountsController state. + * - Removes identites and selectedAddress from the PreferencesController + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + migrateData(versionedData.data); + return versionedData; +} + +function migrateData(state: Record): void { + createDefaultAccountsController(state); + moveIdentitiesToAccountsController(state); + moveSelectedAddressToAccountsController(state); + removeIdentitiesAndSelectedAddressFromPreferencesController(state); +} + +function createDefaultAccountsController(state: Record) { + state.AccountsController = { + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, + }; +} + +function moveIdentitiesToAccountsController(state: Record) { + const identities: { + [key: string]: Identity; + } = state.PreferencesController?.identities || {}; + + if (Object.keys(identities).length === 0) { + return; + } + + const accounts: Record = {}; + + Object.values(identities).forEach((identity) => { + const expectedId = uuid({ + random: sha256FromString(identity.address).slice(0, 16), + }); + + accounts[expectedId] = { + address: identity.address, + id: expectedId, + options: {}, + metadata: { + name: identity.name, + lastSelected: identity.lastSelected ?? undefined, + keyring: { + // This is default HD Key Tree type because the keyring is encrypted during migration, the type will get updated when the during the initial updateAccounts call. + type: 'HD Key Tree', + }, + }, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }; + }); + + state.AccountsController.internalAccounts.accounts = accounts; +} + +function moveSelectedAddressToAccountsController(state: Record) { + const selectedAddress = state.PreferencesController?.selectedAddress; + + const selectedAccount = Object.values( + state.AccountsController.internalAccounts.accounts, + ).find((account: InternalAccount) => { + return account.address.toLowerCase() === selectedAddress.toLowerCase(); + }) as InternalAccount; + + if (selectedAccount) { + state.AccountsController.internalAccounts = { + ...state.AccountsController.internalAccounts, + selectedAccount: selectedAccount.id ?? '', + }; + } +} + +function removeIdentitiesAndSelectedAddressFromPreferencesController( + state: Record, +) { + delete state.PreferencesController.identities; + delete state.PreferencesController.selectedAddress; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index e70a1aa6a4e9..c163d18c03c4 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -106,6 +106,7 @@ import * as m096 from './096'; import * as m097 from './097'; import * as m098 from './098'; import * as m099 from './099'; +import * as m100 from './100'; const migrations = [ m002, @@ -209,5 +210,6 @@ const migrations = [ m097, m098, m099, + m100, ]; export default migrations; diff --git a/builds.yml b/builds.yml index c096b51509ec..f2484b4503e5 100644 --- a/builds.yml +++ b/builds.yml @@ -25,6 +25,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_PROD_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true + - KEYRING_API_ENABLED: false - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.consensys.io/2.0.1/index.html # Main build uses the default browser manifest manifestOverrides: false @@ -37,6 +38,7 @@ buildTypes: - SEGMENT_BETA_WRITE_KEY - INFURA_ENV_KEY_REF: INFURA_BETA_PROJECT_ID - SEGMENT_WRITE_KEY_REF: SEGMENT_BETA_WRITE_KEY + - KEYRING_API_ENABLED: false # Modifies how the version is displayed. # eg. instead of 10.25.0 -> 10.25.0-beta.2 isPrerelease: true @@ -63,6 +65,7 @@ buildTypes: - SUPPORT_REQUEST_LINK: https://metamask-flask.zendesk.com/hc/en-us/requests/new - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID - SEGMENT_WRITE_KEY_REF: SEGMENT_FLASK_WRITE_KEY + - KEYRING_API_ENABLED: true isPrerelease: true manifestOverrides: ./app/build-types/flask/manifest/ @@ -82,6 +85,7 @@ buildTypes: - SUPPORT_REQUEST_LINK: https://metamask-flask.zendesk.com/hc/en-us/requests/new - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID - SEGMENT_WRITE_KEY_REF: SEGMENT_FLASK_WRITE_KEY + - KEYRING_API_ENABLED: false isPrerelease: true manifestOverrides: ./app/build-types/desktop/manifest/ @@ -100,6 +104,7 @@ buildTypes: - MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v2/configuration/default - SUPPORT_LINK: https://mmi-support.zendesk.com/hc/en-us - SUPPORT_REQUEST_LINK: https://mmi-support.zendesk.com/hc/en-us/requests/new + - KEYRING_API_ENABLED: false # For some reason, MMI uses this type of versioning # Leaving it on for backwards compatibility isPrerelease: true @@ -118,6 +123,7 @@ features: # Whether to verify that a snap can be installed using an allow list - REQUIRE_SNAPS_ALLOWLIST - IFRAME_EXECUTION_ENVIRONMENT_URL + - KEYRING_API_ENABLED assets: - ./{app,shared,ui}/**/snaps/** desktop: diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 5f198c10ec85..15f6750bb3c9 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -715,6 +715,15 @@ "@babel/runtime": true } }, + "@metamask/accounts-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/eth-snap-keyring": true, + "@metamask/keyring-api": true, + "ethereumjs-util": true, + "uuid": true + } + }, "@metamask/address-book-controller": { "packages": { "@metamask/base-controller": true, @@ -920,11 +929,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-json-rpc-middleware>clone": { @@ -995,11 +1004,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": { @@ -1069,9 +1078,9 @@ "@metamask/eth-ledger-bridge-keyring>eth-sig-util": { "packages": { "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "browserify>buffer": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true, "ethereumjs-abi": true } }, @@ -1082,7 +1091,7 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, + "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, "ethereumjs-util>rlp": true, "koa>content-disposition>safe-buffer": true @@ -1115,6 +1124,51 @@ "koa>content-disposition>safe-buffer": true } }, + "@metamask/eth-snap-keyring": { + "globals": { + "console.error": true, + "console.log": true + }, + "packages": { + "@ethereumjs/tx": true, + "@metamask/eth-snap-keyring>@metamask/eth-sig-util": true, + "@metamask/eth-snap-keyring>@metamask/utils": true, + "@metamask/eth-snap-keyring>uuid": true, + "@metamask/keyring-api": true, + "superstruct": true, + "webpack>events": true + } + }, + "@metamask/eth-snap-keyring>@metamask/eth-sig-util": { + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/eth-snap-keyring>@metamask/utils": true, + "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, + "browserify>buffer": true, + "ethereumjs-abi>ethereumjs-util>ethjs-util": true + } + }, + "@metamask/eth-snap-keyring>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, + "@metamask/eth-snap-keyring>uuid": { + "globals": { + "crypto": true + } + }, "@metamask/eth-token-tracker": { "globals": { "console.warn": true @@ -1248,11 +1302,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-trezor-keyring>@metamask/utils": { @@ -1556,6 +1610,31 @@ "TextEncoder": true } }, + "@metamask/keyring-api": { + "packages": { + "@metamask/keyring-api>@metamask/utils": true, + "@metamask/keyring-api>uuid": true, + "superstruct": true + } + }, + "@metamask/keyring-api>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, + "@metamask/keyring-api>uuid": { + "globals": { + "crypto": true + } + }, "@metamask/keyring-controller": { "packages": { "@metamask/base-controller": true, @@ -1584,11 +1663,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/keyring-controller>@metamask/eth-keyring-controller>@metamask/utils": { @@ -1710,10 +1789,10 @@ "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": true, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/utils": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": { @@ -1748,6 +1827,25 @@ "superstruct": true } }, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": { + "globals": { + "crypto": true, + "msCrypto": true, + "nacl": "write" + }, + "packages": { + "browserify>browser-resolve": true + } + }, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": { + "globals": { + "atob": true, + "btoa": true + }, + "packages": { + "browserify>browser-resolve": true + } + }, "@metamask/message-manager>@metamask/utils": { "globals": { "TextDecoder": true, @@ -3675,62 +3773,6 @@ "eth-rpc-errors>fast-safe-stringify": true } }, - "eth-sig-util": { - "packages": { - "browserify>buffer": true, - "eth-sig-util>ethereumjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true, - "ethereumjs-abi": true - } - }, - "eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/ppom-validator>elliptic": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethereum-cryptography": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>rlp": true, - "koa>content-disposition>safe-buffer": true - } - }, - "eth-sig-util>ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "mocha>serialize-javascript>randombytes": true - } - }, - "eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, - "eth-sig-util>tweetnacl": { - "globals": { - "crypto": true, - "msCrypto": true, - "nacl": "write" - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-sig-util>tweetnacl-util": { - "globals": { - "atob": true, - "btoa": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, "ethereumjs-abi": { "packages": { "bn.js": true, @@ -3744,8 +3786,8 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, "ethereumjs-abi>ethereumjs-util>ethereum-cryptography": true, + "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, "ethereumjs-util>rlp": true } @@ -3758,6 +3800,13 @@ "mocha>serialize-javascript>randombytes": true } }, + "ethereumjs-abi>ethereumjs-util>ethjs-util": { + "packages": { + "browserify>buffer": true, + "ethjs>ethjs-util>is-hex-prefixed": true, + "ethjs>ethjs-util>strip-hex-prefix": true + } + }, "ethereumjs-util": { "packages": { "bn.js": true, diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index a8272600411e..de063131e565 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -715,6 +715,15 @@ "@babel/runtime": true } }, + "@metamask/accounts-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/eth-snap-keyring": true, + "@metamask/keyring-api": true, + "ethereumjs-util": true, + "uuid": true + } + }, "@metamask/address-book-controller": { "packages": { "@metamask/base-controller": true, @@ -991,11 +1000,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-json-rpc-middleware>clone": { @@ -1066,11 +1075,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": { @@ -1140,9 +1149,9 @@ "@metamask/eth-ledger-bridge-keyring>eth-sig-util": { "packages": { "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "browserify>buffer": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true, "ethereumjs-abi": true } }, @@ -1153,7 +1162,7 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, + "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, "ethereumjs-util>rlp": true, "koa>content-disposition>safe-buffer": true @@ -1188,14 +1197,15 @@ }, "@metamask/eth-snap-keyring": { "globals": { - "console.error": true + "console.error": true, + "console.log": true }, "packages": { "@ethereumjs/tx": true, "@metamask/eth-snap-keyring>@metamask/eth-sig-util": true, - "@metamask/eth-snap-keyring>@metamask/keyring-api": true, "@metamask/eth-snap-keyring>@metamask/utils": true, "@metamask/eth-snap-keyring>uuid": true, + "@metamask/keyring-api": true, "superstruct": true, "webpack>events": true } @@ -1206,35 +1216,10 @@ "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/eth-snap-keyring>@metamask/utils": true, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true - } - }, - "@metamask/eth-snap-keyring>@metamask/keyring-api": { - "packages": { - "@metamask/eth-snap-keyring>@metamask/keyring-api>@metamask/utils": true, - "@metamask/eth-snap-keyring>@metamask/keyring-api>uuid": true, - "superstruct": true - } - }, - "@metamask/eth-snap-keyring>@metamask/keyring-api>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/key-tree>@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true, - "superstruct": true - } - }, - "@metamask/eth-snap-keyring>@metamask/keyring-api>uuid": { - "globals": { - "crypto": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-snap-keyring>@metamask/utils": { @@ -1388,11 +1373,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-trezor-keyring>@metamask/utils": { @@ -1696,6 +1681,31 @@ "TextEncoder": true } }, + "@metamask/keyring-api": { + "packages": { + "@metamask/keyring-api>@metamask/utils": true, + "@metamask/keyring-api>uuid": true, + "superstruct": true + } + }, + "@metamask/keyring-api>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, + "@metamask/keyring-api>uuid": { + "globals": { + "crypto": true + } + }, "@metamask/keyring-controller": { "packages": { "@metamask/base-controller": true, @@ -1724,11 +1734,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/keyring-controller>@metamask/eth-keyring-controller>@metamask/utils": { @@ -1850,10 +1860,10 @@ "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": true, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/utils": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": { @@ -1888,6 +1898,25 @@ "superstruct": true } }, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": { + "globals": { + "crypto": true, + "msCrypto": true, + "nacl": "write" + }, + "packages": { + "browserify>browser-resolve": true + } + }, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": { + "globals": { + "atob": true, + "btoa": true + }, + "packages": { + "browserify>browser-resolve": true + } + }, "@metamask/message-manager>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2248,7 +2277,6 @@ "@metamask/providers>@metamask/object-multiplex": true, "@metamask/rpc-methods": true, "@metamask/snaps-controllers>@metamask/post-message-stream": true, - "@metamask/snaps-controllers>@metamask/snaps-registry": true, "@metamask/snaps-controllers>@metamask/utils": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, @@ -2257,6 +2285,7 @@ "@metamask/snaps-controllers>readable-web-to-node-stream": true, "@metamask/snaps-controllers>tar-stream": true, "@metamask/snaps-utils": true, + "@metamask/snaps-utils>@metamask/snaps-registry": true, "eth-rpc-errors": true, "json-rpc-engine": true, "json-rpc-middleware-stream": true, @@ -2296,13 +2325,6 @@ "superstruct": true } }, - "@metamask/snaps-controllers>@metamask/snaps-registry": { - "packages": { - "@metamask/key-tree>@noble/secp256k1": true, - "@metamask/snaps-controllers>@metamask/utils": true, - "superstruct": true - } - }, "@metamask/snaps-controllers>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2511,6 +2533,26 @@ "superstruct": true } }, + "@metamask/snaps-utils>@metamask/snaps-registry": { + "packages": { + "@metamask/key-tree>@noble/secp256k1": true, + "@metamask/snaps-utils>@metamask/snaps-registry>@metamask/utils": true, + "superstruct": true + } + }, + "@metamask/snaps-utils>@metamask/snaps-registry>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, "@metamask/snaps-utils>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4063,62 +4105,6 @@ "eth-rpc-errors>fast-safe-stringify": true } }, - "eth-sig-util": { - "packages": { - "browserify>buffer": true, - "eth-sig-util>ethereumjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true, - "ethereumjs-abi": true - } - }, - "eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/ppom-validator>elliptic": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethereum-cryptography": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>rlp": true, - "koa>content-disposition>safe-buffer": true - } - }, - "eth-sig-util>ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "mocha>serialize-javascript>randombytes": true - } - }, - "eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, - "eth-sig-util>tweetnacl": { - "globals": { - "crypto": true, - "msCrypto": true, - "nacl": "write" - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-sig-util>tweetnacl-util": { - "globals": { - "atob": true, - "btoa": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, "ethereumjs-abi": { "packages": { "bn.js": true, @@ -4132,8 +4118,8 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, "ethereumjs-abi>ethereumjs-util>ethereum-cryptography": true, + "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, "ethereumjs-util>rlp": true } @@ -4146,6 +4132,13 @@ "mocha>serialize-javascript>randombytes": true } }, + "ethereumjs-abi>ethereumjs-util>ethjs-util": { + "packages": { + "browserify>buffer": true, + "ethjs>ethjs-util>is-hex-prefixed": true, + "ethjs>ethjs-util>strip-hex-prefix": true + } + }, "ethereumjs-util": { "packages": { "bn.js": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index d7d38f217ceb..1123a9b03621 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -715,6 +715,15 @@ "@babel/runtime": true } }, + "@metamask/accounts-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/eth-snap-keyring": true, + "@metamask/keyring-api": true, + "ethereumjs-util": true, + "uuid": true + } + }, "@metamask/address-book-controller": { "packages": { "@metamask/base-controller": true, @@ -991,11 +1000,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-json-rpc-middleware>clone": { @@ -1066,11 +1075,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": { @@ -1140,9 +1149,9 @@ "@metamask/eth-ledger-bridge-keyring>eth-sig-util": { "packages": { "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "browserify>buffer": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true, "ethereumjs-abi": true } }, @@ -1153,7 +1162,7 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, + "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, "ethereumjs-util>rlp": true, "koa>content-disposition>safe-buffer": true @@ -1188,14 +1197,15 @@ }, "@metamask/eth-snap-keyring": { "globals": { - "console.error": true + "console.error": true, + "console.log": true }, "packages": { "@ethereumjs/tx": true, "@metamask/eth-snap-keyring>@metamask/eth-sig-util": true, - "@metamask/eth-snap-keyring>@metamask/keyring-api": true, "@metamask/eth-snap-keyring>@metamask/utils": true, "@metamask/eth-snap-keyring>uuid": true, + "@metamask/keyring-api": true, "superstruct": true, "webpack>events": true } @@ -1206,35 +1216,10 @@ "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/eth-snap-keyring>@metamask/utils": true, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true - } - }, - "@metamask/eth-snap-keyring>@metamask/keyring-api": { - "packages": { - "@metamask/eth-snap-keyring>@metamask/keyring-api>@metamask/utils": true, - "@metamask/eth-snap-keyring>@metamask/keyring-api>uuid": true, - "superstruct": true - } - }, - "@metamask/eth-snap-keyring>@metamask/keyring-api>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/key-tree>@noble/hashes": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true, - "superstruct": true - } - }, - "@metamask/eth-snap-keyring>@metamask/keyring-api>uuid": { - "globals": { - "crypto": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-snap-keyring>@metamask/utils": { @@ -1388,11 +1373,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-trezor-keyring>@metamask/utils": { @@ -1696,6 +1681,31 @@ "TextEncoder": true } }, + "@metamask/keyring-api": { + "packages": { + "@metamask/keyring-api>@metamask/utils": true, + "@metamask/keyring-api>uuid": true, + "superstruct": true + } + }, + "@metamask/keyring-api>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, + "@metamask/keyring-api>uuid": { + "globals": { + "crypto": true + } + }, "@metamask/keyring-controller": { "packages": { "@metamask/base-controller": true, @@ -1724,11 +1734,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/keyring-controller>@metamask/eth-keyring-controller>@metamask/utils": { @@ -1850,10 +1860,10 @@ "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": true, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/utils": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": { @@ -1888,6 +1898,25 @@ "superstruct": true } }, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": { + "globals": { + "crypto": true, + "msCrypto": true, + "nacl": "write" + }, + "packages": { + "browserify>browser-resolve": true + } + }, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": { + "globals": { + "atob": true, + "btoa": true + }, + "packages": { + "browserify>browser-resolve": true + } + }, "@metamask/message-manager>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2264,7 +2293,6 @@ "@metamask/providers>@metamask/object-multiplex": true, "@metamask/rpc-methods": true, "@metamask/snaps-controllers>@metamask/post-message-stream": true, - "@metamask/snaps-controllers>@metamask/snaps-registry": true, "@metamask/snaps-controllers>@metamask/utils": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, @@ -2273,6 +2301,7 @@ "@metamask/snaps-controllers>readable-web-to-node-stream": true, "@metamask/snaps-controllers>tar-stream": true, "@metamask/snaps-utils": true, + "@metamask/snaps-utils>@metamask/snaps-registry": true, "eth-rpc-errors": true, "json-rpc-engine": true, "json-rpc-middleware-stream": true, @@ -2312,13 +2341,6 @@ "superstruct": true } }, - "@metamask/snaps-controllers>@metamask/snaps-registry": { - "packages": { - "@metamask/key-tree>@noble/secp256k1": true, - "@metamask/snaps-controllers>@metamask/utils": true, - "superstruct": true - } - }, "@metamask/snaps-controllers>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2527,6 +2549,26 @@ "superstruct": true } }, + "@metamask/snaps-utils>@metamask/snaps-registry": { + "packages": { + "@metamask/key-tree>@noble/secp256k1": true, + "@metamask/snaps-utils>@metamask/snaps-registry>@metamask/utils": true, + "superstruct": true + } + }, + "@metamask/snaps-utils>@metamask/snaps-registry>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, "@metamask/snaps-utils>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4079,62 +4121,6 @@ "eth-rpc-errors>fast-safe-stringify": true } }, - "eth-sig-util": { - "packages": { - "browserify>buffer": true, - "eth-sig-util>ethereumjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true, - "ethereumjs-abi": true - } - }, - "eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/ppom-validator>elliptic": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethereum-cryptography": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>rlp": true, - "koa>content-disposition>safe-buffer": true - } - }, - "eth-sig-util>ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "mocha>serialize-javascript>randombytes": true - } - }, - "eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, - "eth-sig-util>tweetnacl": { - "globals": { - "crypto": true, - "msCrypto": true, - "nacl": "write" - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-sig-util>tweetnacl-util": { - "globals": { - "atob": true, - "btoa": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, "ethereumjs-abi": { "packages": { "bn.js": true, @@ -4148,8 +4134,8 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, "ethereumjs-abi>ethereumjs-util>ethereum-cryptography": true, + "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, "ethereumjs-util>rlp": true } @@ -4162,6 +4148,13 @@ "mocha>serialize-javascript>randombytes": true } }, + "ethereumjs-abi>ethereumjs-util>ethjs-util": { + "packages": { + "browserify>buffer": true, + "ethjs>ethjs-util>is-hex-prefixed": true, + "ethjs>ethjs-util>strip-hex-prefix": true + } + }, "ethereumjs-util": { "packages": { "bn.js": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 50bafcc3fa88..7a3a3d3ca015 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -715,6 +715,15 @@ "@babel/runtime": true } }, + "@metamask/accounts-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/eth-snap-keyring": true, + "@metamask/keyring-api": true, + "ethereumjs-util": true, + "uuid": true + } + }, "@metamask/address-book-controller": { "packages": { "@metamask/base-controller": true, @@ -920,11 +929,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-json-rpc-middleware>clone": { @@ -995,11 +1004,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": { @@ -1069,9 +1078,9 @@ "@metamask/eth-ledger-bridge-keyring>eth-sig-util": { "packages": { "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "browserify>buffer": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true, "ethereumjs-abi": true } }, @@ -1082,7 +1091,7 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, + "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, "ethereumjs-util>rlp": true, "koa>content-disposition>safe-buffer": true @@ -1115,6 +1124,51 @@ "koa>content-disposition>safe-buffer": true } }, + "@metamask/eth-snap-keyring": { + "globals": { + "console.error": true, + "console.log": true + }, + "packages": { + "@ethereumjs/tx": true, + "@metamask/eth-snap-keyring>@metamask/eth-sig-util": true, + "@metamask/eth-snap-keyring>@metamask/utils": true, + "@metamask/eth-snap-keyring>uuid": true, + "@metamask/keyring-api": true, + "superstruct": true, + "webpack>events": true + } + }, + "@metamask/eth-snap-keyring>@metamask/eth-sig-util": { + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/eth-snap-keyring>@metamask/utils": true, + "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, + "browserify>buffer": true, + "ethereumjs-abi>ethereumjs-util>ethjs-util": true + } + }, + "@metamask/eth-snap-keyring>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, + "@metamask/eth-snap-keyring>uuid": { + "globals": { + "crypto": true + } + }, "@metamask/eth-token-tracker": { "globals": { "console.warn": true @@ -1248,11 +1302,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-trezor-keyring>@metamask/utils": { @@ -1556,6 +1610,31 @@ "TextEncoder": true } }, + "@metamask/keyring-api": { + "packages": { + "@metamask/keyring-api>@metamask/utils": true, + "@metamask/keyring-api>uuid": true, + "superstruct": true + } + }, + "@metamask/keyring-api>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, + "@metamask/keyring-api>uuid": { + "globals": { + "crypto": true + } + }, "@metamask/keyring-controller": { "packages": { "@metamask/base-controller": true, @@ -1584,11 +1663,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/keyring-controller>@metamask/eth-keyring-controller>@metamask/utils": { @@ -1710,10 +1789,10 @@ "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": true, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/utils": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": { @@ -1748,6 +1827,25 @@ "superstruct": true } }, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": { + "globals": { + "crypto": true, + "msCrypto": true, + "nacl": "write" + }, + "packages": { + "browserify>browser-resolve": true + } + }, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": { + "globals": { + "atob": true, + "btoa": true + }, + "packages": { + "browserify>browser-resolve": true + } + }, "@metamask/message-manager>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2108,7 +2206,6 @@ "@metamask/providers>@metamask/object-multiplex": true, "@metamask/rpc-methods": true, "@metamask/snaps-controllers>@metamask/post-message-stream": true, - "@metamask/snaps-controllers>@metamask/snaps-registry": true, "@metamask/snaps-controllers>@metamask/utils": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, @@ -2117,6 +2214,7 @@ "@metamask/snaps-controllers>readable-web-to-node-stream": true, "@metamask/snaps-controllers>tar-stream": true, "@metamask/snaps-utils": true, + "@metamask/snaps-utils>@metamask/snaps-registry": true, "eth-rpc-errors": true, "json-rpc-engine": true, "json-rpc-middleware-stream": true, @@ -2156,13 +2254,6 @@ "superstruct": true } }, - "@metamask/snaps-controllers>@metamask/snaps-registry": { - "packages": { - "@metamask/key-tree>@noble/secp256k1": true, - "@metamask/snaps-controllers>@metamask/utils": true, - "superstruct": true - } - }, "@metamask/snaps-controllers>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2371,6 +2462,26 @@ "superstruct": true } }, + "@metamask/snaps-utils>@metamask/snaps-registry": { + "packages": { + "@metamask/key-tree>@noble/secp256k1": true, + "@metamask/snaps-utils>@metamask/snaps-registry>@metamask/utils": true, + "superstruct": true + } + }, + "@metamask/snaps-utils>@metamask/snaps-registry>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, "@metamask/snaps-utils>@metamask/utils": { "globals": { "TextDecoder": true, @@ -3923,62 +4034,6 @@ "eth-rpc-errors>fast-safe-stringify": true } }, - "eth-sig-util": { - "packages": { - "browserify>buffer": true, - "eth-sig-util>ethereumjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true, - "ethereumjs-abi": true - } - }, - "eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/ppom-validator>elliptic": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethereum-cryptography": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>rlp": true, - "koa>content-disposition>safe-buffer": true - } - }, - "eth-sig-util>ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "mocha>serialize-javascript>randombytes": true - } - }, - "eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, - "eth-sig-util>tweetnacl": { - "globals": { - "crypto": true, - "msCrypto": true, - "nacl": "write" - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-sig-util>tweetnacl-util": { - "globals": { - "atob": true, - "btoa": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, "ethereumjs-abi": { "packages": { "bn.js": true, @@ -3992,8 +4047,8 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, "ethereumjs-abi>ethereumjs-util>ethereum-cryptography": true, + "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, "ethereumjs-util>rlp": true } @@ -4006,6 +4061,13 @@ "mocha>serialize-javascript>randombytes": true } }, + "ethereumjs-abi>ethereumjs-util>ethjs-util": { + "packages": { + "browserify>buffer": true, + "ethjs>ethjs-util>is-hex-prefixed": true, + "ethjs>ethjs-util>strip-hex-prefix": true + } + }, "ethereumjs-util": { "packages": { "bn.js": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index cf531b5ffbf9..c4f26660c57c 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -855,6 +855,15 @@ "webpack>events": true } }, + "@metamask/accounts-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/eth-snap-keyring": true, + "@metamask/keyring-api": true, + "ethereumjs-util": true, + "uuid": true + } + }, "@metamask/address-book-controller": { "packages": { "@metamask/base-controller": true, @@ -1060,11 +1069,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-json-rpc-middleware>clone": { @@ -1135,11 +1144,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": { @@ -1209,9 +1218,9 @@ "@metamask/eth-ledger-bridge-keyring>eth-sig-util": { "packages": { "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "browserify>buffer": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true, "ethereumjs-abi": true } }, @@ -1222,7 +1231,7 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, + "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, "ethereumjs-util>rlp": true, "koa>content-disposition>safe-buffer": true @@ -1255,6 +1264,51 @@ "koa>content-disposition>safe-buffer": true } }, + "@metamask/eth-snap-keyring": { + "globals": { + "console.error": true, + "console.log": true + }, + "packages": { + "@ethereumjs/tx": true, + "@metamask/eth-snap-keyring>@metamask/eth-sig-util": true, + "@metamask/eth-snap-keyring>@metamask/utils": true, + "@metamask/eth-snap-keyring>uuid": true, + "@metamask/keyring-api": true, + "superstruct": true, + "webpack>events": true + } + }, + "@metamask/eth-snap-keyring>@metamask/eth-sig-util": { + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/eth-snap-keyring>@metamask/utils": true, + "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, + "browserify>buffer": true, + "ethereumjs-abi>ethereumjs-util>ethjs-util": true + } + }, + "@metamask/eth-snap-keyring>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, + "@metamask/eth-snap-keyring>uuid": { + "globals": { + "crypto": true + } + }, "@metamask/eth-token-tracker": { "globals": { "console.warn": true @@ -1388,11 +1442,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/eth-trezor-keyring>@metamask/utils": { @@ -1696,6 +1750,31 @@ "TextEncoder": true } }, + "@metamask/keyring-api": { + "packages": { + "@metamask/keyring-api>@metamask/utils": true, + "@metamask/keyring-api>uuid": true, + "superstruct": true + } + }, + "@metamask/keyring-api>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, + "@metamask/keyring-api>uuid": { + "globals": { + "crypto": true + } + }, "@metamask/keyring-controller": { "packages": { "@metamask/base-controller": true, @@ -1724,11 +1803,11 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "bn.js": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/keyring-controller>@metamask/eth-keyring-controller>@metamask/utils": { @@ -1850,10 +1929,10 @@ "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": true, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/utils": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": true, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true + "ethereumjs-abi>ethereumjs-util>ethjs-util": true } }, "@metamask/message-manager>@metamask/eth-sig-util>@metamask/abi-utils": { @@ -1888,6 +1967,25 @@ "superstruct": true } }, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl": { + "globals": { + "crypto": true, + "msCrypto": true, + "nacl": "write" + }, + "packages": { + "browserify>browser-resolve": true + } + }, + "@metamask/message-manager>@metamask/eth-sig-util>tweetnacl-util": { + "globals": { + "atob": true, + "btoa": true + }, + "packages": { + "browserify>browser-resolve": true + } + }, "@metamask/message-manager>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2248,7 +2346,6 @@ "@metamask/providers>@metamask/object-multiplex": true, "@metamask/rpc-methods": true, "@metamask/snaps-controllers>@metamask/post-message-stream": true, - "@metamask/snaps-controllers>@metamask/snaps-registry": true, "@metamask/snaps-controllers>@metamask/utils": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, @@ -2257,6 +2354,7 @@ "@metamask/snaps-controllers>readable-web-to-node-stream": true, "@metamask/snaps-controllers>tar-stream": true, "@metamask/snaps-utils": true, + "@metamask/snaps-utils>@metamask/snaps-registry": true, "eth-rpc-errors": true, "json-rpc-engine": true, "json-rpc-middleware-stream": true, @@ -2296,13 +2394,6 @@ "superstruct": true } }, - "@metamask/snaps-controllers>@metamask/snaps-registry": { - "packages": { - "@metamask/key-tree>@noble/secp256k1": true, - "@metamask/snaps-controllers>@metamask/utils": true, - "superstruct": true - } - }, "@metamask/snaps-controllers>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2511,6 +2602,26 @@ "superstruct": true } }, + "@metamask/snaps-utils>@metamask/snaps-registry": { + "packages": { + "@metamask/key-tree>@noble/secp256k1": true, + "@metamask/snaps-utils>@metamask/snaps-registry>@metamask/utils": true, + "superstruct": true + } + }, + "@metamask/snaps-utils>@metamask/snaps-registry>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, "@metamask/snaps-utils>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4063,62 +4174,6 @@ "eth-rpc-errors>fast-safe-stringify": true } }, - "eth-sig-util": { - "packages": { - "browserify>buffer": true, - "eth-sig-util>ethereumjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true, - "ethereumjs-abi": true - } - }, - "eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/ppom-validator>elliptic": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethereum-cryptography": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>rlp": true, - "koa>content-disposition>safe-buffer": true - } - }, - "eth-sig-util>ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "mocha>serialize-javascript>randombytes": true - } - }, - "eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, - "eth-sig-util>tweetnacl": { - "globals": { - "crypto": true, - "msCrypto": true, - "nacl": "write" - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-sig-util>tweetnacl-util": { - "globals": { - "atob": true, - "btoa": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, "ethereumjs-abi": { "packages": { "bn.js": true, @@ -4132,8 +4187,8 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, "ethereumjs-abi>ethereumjs-util>ethereum-cryptography": true, + "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, "ethereumjs-util>rlp": true } @@ -4146,6 +4201,13 @@ "mocha>serialize-javascript>randombytes": true } }, + "ethereumjs-abi>ethereumjs-util>ethjs-util": { + "packages": { + "browserify>buffer": true, + "ethjs>ethjs-util>is-hex-prefixed": true, + "ethjs>ethjs-util>strip-hex-prefix": true + } + }, "ethereumjs-util": { "packages": { "bn.js": true, diff --git a/package.json b/package.json index b59c0c349892..1b2bafee2845 100644 --- a/package.json +++ b/package.json @@ -235,6 +235,7 @@ "@metamask-institutional/rpc-allowlist": "^1.0.0", "@metamask-institutional/sdk": "^0.1.18", "@metamask-institutional/transaction-update": "^0.1.27", + "@metamask/accounts-controller": "^1.0.0", "@metamask/address-book-controller": "^3.0.0", "@metamask/announcement-controller": "^4.0.0", "@metamask/approval-controller": "^3.4.0", @@ -248,7 +249,7 @@ "@metamask/eth-json-rpc-middleware": "^11.0.0", "@metamask/eth-keyring-controller": "^10.0.1", "@metamask/eth-ledger-bridge-keyring": "^0.15.0", - "@metamask/eth-snap-keyring": "^0.2.2", + "@metamask/eth-snap-keyring": "^0.2.3", "@metamask/eth-token-tracker": "^4.0.0", "@metamask/eth-trezor-keyring": "^1.1.0", "@metamask/etherscan-link": "^2.2.0", @@ -256,6 +257,7 @@ "@metamask/gas-fee-controller": "^6.0.1", "@metamask/jazzicon": "^2.0.0", "@metamask/key-tree": "^9.0.0", + "@metamask/keyring-api": "^0.2.7", "@metamask/keyring-controller": "^8.0.0", "@metamask/logging-controller": "^1.0.1", "@metamask/logo": "^3.1.1", @@ -313,7 +315,6 @@ "eth-method-registry": "^2.0.0", "eth-query": "^2.1.2", "eth-rpc-errors": "^4.0.2", - "eth-sig-util": "^3.0.0", "ethereum-ens-network-map": "^1.0.2", "ethereumjs-abi": "^0.6.4", "ethereumjs-util": "^7.0.10", diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index b17c67e63038..802b145a079a 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -313,7 +313,7 @@ export type MetaMetricsUserTraits = { */ nft_autodetection_enabled?: number; /** - * A number representing the number of identities (accounts) added to the + * A number representing the number of InternalAccounts (accounts) added to the * user's wallet. */ number_of_accounts?: number; @@ -400,7 +400,7 @@ export enum MetaMetricsUserTrait { */ NftAutodetectionEnabled = 'nft_autodetection_enabled', /** - * Identified when identities change. + * Identified when InternalAccounts change. */ NumberOfAccounts = 'number_of_accounts', /** diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index 14d4967d70f9..446ef2af49b6 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -118,6 +118,99 @@ "type": "rpc", "chainId": "0x5" }, + "internalAccounts": { + "accounts": { + "cf8dace4-9439-4bd4-b3a8-88c821c8fcb3": { + "address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", + "id": "cf8dace4-9439-4bd4-b3a8-88c821c8fcb3", + "metadata": { + "keyring": { + "type": "HD Key Tree" + } + }, + "name": "Test Account", + "options": {}, + "methods": [ + "personal_sign", + "eth_sendTransaction", + "eth_sign", + "eth_signTransaction", + "eth_signTypedData_v1", + "eth_signTypedData_v2", + "eth_signTypedData_v3", + "eth_signTypedData_v4" + ], + "type": "eip155:eoa" + }, + "07c2cfec-36c9-46c4-8115-3836d3ac9047": { + "address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b", + "id": "07c2cfec-36c9-46c4-8115-3836d3ac9047", + "metadata": { + "keyring": { + "type": "HD Key Tree" + } + }, + "name": "Test Account 2", + "options": {}, + "methods": [ + "personal_sign", + "eth_sendTransaction", + "eth_sign", + "eth_signTransaction", + "eth_signTypedData_v1", + "eth_signTypedData_v2", + "eth_signTypedData_v3", + "eth_signTypedData_v4" + ], + "type": "eip155:eoa" + }, + "15e69915-2a1a-4019-93b3-916e11fd432f": { + "address": "0xc42edfcc21ed14dda456aa0756c153f7985d8813", + "id": "15e69915-2a1a-4019-93b3-916e11fd432f", + "metadata": { + "keyring": { + "type": "Ledger Hardware" + } + }, + "name": "Ledger Hardware 2", + "options": {}, + "methods": [ + "personal_sign", + "eth_sendTransaction", + "eth_sign", + "eth_signTransaction", + "eth_signTypedData_v1", + "eth_signTypedData_v2", + "eth_signTypedData_v3", + "eth_signTypedData_v4" + ], + "type": "eip155:eoa" + }, + "784225f4-d30b-4e77-a900-c8bbce735b88": { + "address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b", + "id": "784225f4-d30b-4e77-a900-c8bbce735b88", + "metadata": { + "keyring": { + "type": "HD Key Tree" + } + }, + "name": "Test Account 3", + "options": {}, + "methods": [ + "personal_sign", + "eth_sendTransaction", + "eth_sign", + "eth_signTransaction", + "eth_signTypedData_v1", + "eth_signTypedData_v2", + "eth_signTypedData_v3", + "eth_signTypedData_v4" + ], + "type": "eip155:eoa" + } + }, + "selectedAccount": "cf8dace4-9439-4bd4-b3a8-88c821c8fcb3" + }, "keyrings": [ { "type": "HD Key Tree", @@ -135,24 +228,6 @@ "accounts": ["0xeb9e64b93097bc15f01f13eae97015c57ab64823"] } ], - "identities": { - "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": { - "address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", - "name": "Test Account" - }, - "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": { - "address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b", - "name": "Test Account 2" - }, - "0xc42edfcc21ed14dda456aa0756c153f7985d8813": { - "address": "0xc42edfcc21ed14dda456aa0756c153f7985d8813", - "name": "Test Ledger 1" - }, - "0xeb9e64b93097bc15f01f13eae97015c57ab64823": { - "name": "Test Account 3", - "address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823" - } - }, "selectedNetworkClientId": "goerli", "networksMetadata": { "goerli": { diff --git a/test/data/mock-state.json b/test/data/mock-state.json index be994991cb49..a466417532a8 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -133,6 +133,91 @@ "id": "chain5" } }, + "internalAccounts": { + "accounts": { + "cf8dace4-9439-4bd4-b3a8-88c821c8fcb3": { + "address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", + "id": "cf8dace4-9439-4bd4-b3a8-88c821c8fcb3", + "metadata": { + "name": "Test Account", + "keyring": { + "type": "HD Key Tree" + } + }, + "options": {}, + "methods": [ + "personal_sign", + "eth_sign", + "eth_signTransaction", + "eth_signTypedData_v1", + "eth_signTypedData_v3", + "eth_signTypedData_v4" + ], + "type": "eip155:eoa" + }, + "07c2cfec-36c9-46c4-8115-3836d3ac9047": { + "address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b", + "id": "07c2cfec-36c9-46c4-8115-3836d3ac9047", + "metadata": { + "name": "Test Account 2", + "keyring": { + "type": "HD Key Tree" + } + }, + "options": {}, + "methods": [ + "personal_sign", + "eth_sign", + "eth_signTransaction", + "eth_signTypedData_v1", + "eth_signTypedData_v3", + "eth_signTypedData_v4" + ], + "type": "eip155:eoa" + }, + "15e69915-2a1a-4019-93b3-916e11fd432f": { + "address": "0xc42edfcc21ed14dda456aa0756c153f7985d8813", + "id": "15e69915-2a1a-4019-93b3-916e11fd432f", + "metadata": { + "name": "Ledger Hardware 2", + "keyring": { + "type": "Ledger Hardware" + } + }, + "options": {}, + "methods": [ + "personal_sign", + "eth_sign", + "eth_signTransaction", + "eth_signTypedData_v1", + "eth_signTypedData_v3", + "eth_signTypedData_v4" + ], + "type": "eip155:eoa" + }, + "784225f4-d30b-4e77-a900-c8bbce735b88": { + "address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823", + "id": "784225f4-d30b-4e77-a900-c8bbce735b88", + "metadata": { + "name": "Test Account 3", + "keyring": { + "type": "HD Key Tree" + } + }, + "options": {}, + "methods": [ + "personal_sign", + "eth_sign", + "eth_signTransaction", + "eth_signTypedData_v1", + "eth_signTypedData_v3", + "eth_signTypedData_v4" + ], + "type": "eip155:eoa" + } + }, + "selectedAccount": "cf8dace4-9439-4bd4-b3a8-88c821c8fcb3" + }, "keyrings": [ { "type": "HD Key Tree", @@ -154,24 +239,6 @@ "accounts": ["0xb552685e3d2790efd64a175b00d51f02cdafee5d"] } ], - "identities": { - "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": { - "address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", - "name": "Test Account" - }, - "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": { - "address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b", - "name": "Test Account 2" - }, - "0xc42edfcc21ed14dda456aa0756c153f7985d8813": { - "address": "0xc42edfcc21ed14dda456aa0756c153f7985d8813", - "name": "Test Ledger 1" - }, - "0xeb9e64b93097bc15f01f13eae97015c57ab64823": { - "name": "Test Account 3", - "address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823" - } - }, "selectedNetworkClientId": "goerli", "networksMetadata": { "goerli": { @@ -228,7 +295,59 @@ "0xaa36a7": true, "0xe704": true }, - "selectedAddress": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", + + "unapprovedTxs": { + "8393540981007587": { + "chainId": "0x5", + "id": 8393540981007587, + "time": 1536268017676, + "status": "unapproved", + "metamaskNetworkId": "4", + "loadingDefaults": false, + "txParams": { + "data": "0xa9059cbb000000000000000000000000b19ac54efa18cc3a14a5b821bfec73d284bf0c5e0000000000000000000000000000000000000000000000003782dace9d900000", + "from": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", + "to": "0xc42edfcc21ed14dda456aa0756c153f7985d8813", + "value": "0x0", + "gas": "0x5208", + "gasPrice": "0x3b9aca00" + }, + "history": [ + { + "id": 8393540981007587, + "time": 1536268017676, + "status": "unapproved", + "metamaskNetworkId": "4", + "loadingDefaults": true, + "txParams": { + "from": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", + "to": "0xc42edfcc21ed14dda456aa0756c153f7985d8813", + "value": "0x0", + "gas": "0x5208", + "gasPrice": "0x3b9aca00" + } + }, + [ + { + "op": "replace", + "path": "/loadingDefaults", + "value": false, + "timestamp": 1536268017685 + } + ], + [ + { + "op": "add", + "path": "/origin", + "value": "MetaMask", + "note": "#newUnapprovedTransaction - adding the origin", + "timestamp": 1536268017686 + } + ] + ], + "origin": "metamask" + } + }, "accounts": { "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": { "balance": "0x346ba7725f412cbfdb", diff --git a/test/e2e/accounts/test-snap-accounts.spec.js b/test/e2e/accounts/test-snap-accounts.spec.js index 436ca1c6b97d..b6394f9fc351 100644 --- a/test/e2e/accounts/test-snap-accounts.spec.js +++ b/test/e2e/accounts/test-snap-accounts.spec.js @@ -361,7 +361,10 @@ describe('Test Snap Account', function () { // click on Accounts await driver.clickElement('[data-testid="account-menu-icon"]'); - const label = await driver.findElement({ css: '.mm-tag', text: 'Snaps' }); + const label = await driver.findElement({ + css: '.mm-tag', + text: 'MetaMask Simple Snap Keyring', + }); label.click(); diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 7f59264e6e84..beef95508e9e 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -15,6 +15,34 @@ const { DAPP_URL } = require('./helpers'); function defaultFixture() { return { data: { + AccountsController: { + internalAccounts: { + selectedAccount: 'd5e45e4a-3b04-4a09-a5e1-39762e5c6be4', + accounts: { + 'd5e45e4a-3b04-4a09-a5e1-39762e5c6be4': { + id: 'd5e45e4a-3b04-4a09-a5e1-39762e5c6be4', + address: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + metadata: { + name: 'Account 1', + lastSelected: 1665507600000, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + }, + }, + }, + }, AlertController: { alertEnabledness: { unconnectedAccount: true, @@ -252,13 +280,6 @@ function defaultFixture() { dismissSeedBackUpReminder: true, featureFlags: {}, forgottenPassword: false, - identities: { - '0x5cfe73b6021e818b776b421b1c4db2474086a7e1': { - address: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', - lastSelected: 1665507600000, - name: 'Account 1', - }, - }, ipfsGateway: 'dweb.link', knownMethodData: {}, ledgerTransportType: 'webhid', @@ -270,7 +291,6 @@ function defaultFixture() { showTestNetworks: false, useNativeCurrencyAsPrimaryCurrency: true, }, - selectedAddress: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', theme: 'light', useBlockie: false, useNftDetection: false, @@ -659,41 +679,131 @@ class FixtureBuilder { return this; } - withPreferencesControllerAdditionalAccountIdentities() { - return this.withPreferencesController({ - identites: { - '0x5cfe73b6021e818b776b421b1c4db2474086a7e1': { - address: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', - lastSelected: 1665507600000, - name: 'Account 1', - }, - '0x09781764c08de8ca82e156bbf156a3ca217c7950': { - address: '0x09781764c08de8ca82e156bbf156a3ca217c7950', - lastSelected: 1665507500000, - name: 'Account 2', + withAccountsController(data) { + merge(this.fixture.data.AccountsController, data); + return this; + } + + withAccountsControllerImportedAccount() { + return this.withAccountsController({ + internalAccounts: { + selectedAccount: '2fdb2de6-80c7-4d2f-9f95-cb6895389843', + accounts: { + '2fdb2de6-80c7-4d2f-9f95-cb6895389843': { + id: '2fdb2de6-80c7-4d2f-9f95-cb6895389843', + address: '0x0cc5261ab8ce458dc977078a3623e2badd27afd3', + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + metadata: { + name: 'Account 1', + lastSelected: 1665507600000, + keyring: { + type: 'HD Key Tree', + }, + }, + }, + '58093703-57e9-4ea9-8545-49e8a75cb084': { + id: '58093703-57e9-4ea9-8545-49e8a75cb084', + address: '0x3ed0ee22e0685ebbf07b2360a8331693c413cc59', + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + }, + 'dd658aab-abf2-4f53-b735-c8a57151d447': { + id: 'dd658aab-abf2-4f53-b735-c8a57151d447', + address: '0xd38d853771fb546bd8b18b2f3638491bc0b0e906', + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + metadata: { + name: 'Account 3', + keyring: { + type: 'HD Key Tree', + }, + }, + }, }, }, }); } - withPreferencesControllerImportedAccountIdentities() { - return this.withPreferencesController({ - identities: { - '0x0cc5261ab8ce458dc977078a3623e2badd27afd3': { - name: 'Account 1', - address: '0x0cc5261ab8ce458dc977078a3623e2badd27afd3', - lastSelected: 1665507600000, - }, - '0x3ed0ee22e0685ebbf07b2360a8331693c413cc59': { - name: 'Account 2', - address: '0x3ed0ee22e0685ebbf07b2360a8331693c413cc59', - }, - '0xd38d853771fb546bd8b18b2f3638491bc0b0e906': { - name: 'Account 3', - address: '0xd38d853771fb546bd8b18b2f3638491bc0b0e906', + withAccountsControllerAdditionalAccountIdentities() { + return this.withAccountsController({ + internalAccounts: { + selectedAccount: '2fdb2de6-80c7-4d2f-9f95-cb6895389843', + accounts: { + '2fdb2de6-80c7-4d2f-9f95-cb6895389843': { + id: '2fdb2de6-80c7-4d2f-9f95-cb6895389843', + address: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + metadata: { + name: 'Account 1', + lastSelected: 1665507600000, + keyring: { + type: 'HD Key Tree', + }, + }, + }, + 'dd658aab-abf2-4f53-b735-c8a57151d447': { + id: 'dd658aab-abf2-4f53-b735-c8a57151d447', + address: '0x09781764c08de8ca82e156bbf156a3ca217c7950', + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + metadata: { + name: 'Account 2', + lastSelected: 1665507600000, + keyring: { + type: 'HD Key Tree', + }, + }, + }, }, }, - selectedAddress: '0x0cc5261ab8ce458dc977078a3623e2badd27afd3', }); } diff --git a/test/e2e/json-rpc/eth_accounts.spec.js b/test/e2e/json-rpc/eth_accounts.spec.js index ecc15a5d0d24..a6f008e1b347 100644 --- a/test/e2e/json-rpc/eth_accounts.spec.js +++ b/test/e2e/json-rpc/eth_accounts.spec.js @@ -9,7 +9,7 @@ describe('eth_accounts', function () { dapp: true, fixtures: new FixtureBuilder() .withKeyringControllerAdditionalAccountVault() - .withPreferencesControllerAdditionalAccountIdentities() + .withAccountsControllerAdditionalAccountIdentities() .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, diff --git a/test/e2e/nft/erc721-interaction.spec.js b/test/e2e/nft/erc721-interaction.spec.js index 9d169e6ebce2..fb31b345c747 100644 --- a/test/e2e/nft/erc721-interaction.spec.js +++ b/test/e2e/nft/erc721-interaction.spec.js @@ -121,6 +121,12 @@ describe('ERC721 NFTs testdapp interaction', function () { windowHandles, ); + // avoid race condition + await driver.waitForSelector({ + css: '.confirm-add-suggested-nft__nft-tokenId', + text: '#6', + }); + // confirm watchNFT await driver.waitForSelector({ css: '.mm-text--heading-lg', diff --git a/test/e2e/snaps/keyring-api/test-snap-keyring-sign.spec.ts b/test/e2e/snaps/keyring-api/test-snap-keyring-sign.spec.ts new file mode 100644 index 000000000000..b81e7b6ef742 --- /dev/null +++ b/test/e2e/snaps/keyring-api/test-snap-keyring-sign.spec.ts @@ -0,0 +1,4 @@ +import * as tests from '../../tests/signature-request.spec'; +import { installTestKeyringSnap } from './helper'; + +tests.signatureTests('Snap Simple Keyring', installTestKeyringSnap); diff --git a/test/e2e/snaps/test-snap-update.spec.js b/test/e2e/snaps/test-snap-update.spec.js index ea8dcbc11962..6c05e0248fd3 100644 --- a/test/e2e/snaps/test-snap-update.spec.js +++ b/test/e2e/snaps/test-snap-update.spec.js @@ -107,7 +107,7 @@ describe('Test Snap update', function () { await driver.waitForSelector({ text: 'Update' }); - await driver.clickElement('[data-testid="snap-update-scroll"]'); + await driver.clickElementSafe('[data-testid="snap-update-scroll"]'); await driver.clickElement({ text: 'Update', diff --git a/test/e2e/tests/import-flow.spec.js b/test/e2e/tests/import-flow.spec.js index 3991350a6407..9d9d594d8293 100644 --- a/test/e2e/tests/import-flow.spec.js +++ b/test/e2e/tests/import-flow.spec.js @@ -21,7 +21,6 @@ const ganacheOptions = { }, ], }; - describe('Import flow', function () { it('Import wallet using Secret Recovery Phrase', async function () { const testPassword = 'correct horse battery staple'; @@ -185,7 +184,7 @@ describe('Import flow', function () { { fixtures: new FixtureBuilder() .withKeyringControllerImportedAccountVault() - .withPreferencesControllerImportedAccountIdentities() + .withAccountsControllerImportedAccount() .build(), ganacheOptions, title: this.test.title, @@ -249,7 +248,7 @@ describe('Import flow', function () { await driver.clickElement({ text: 'Remove', tag: 'button' }); await driver.findElement({ css: '[data-testid="account-menu-icon"]', - text: 'Account 1', + text: 'Account 4', }); await driver.clickElement('[data-testid="account-menu-icon"]'); const accountListItemsAfterRemoval = await driver.findElements( @@ -265,7 +264,7 @@ describe('Import flow', function () { { fixtures: new FixtureBuilder() .withKeyringControllerImportedAccountVault() - .withPreferencesControllerImportedAccountIdentities() + .withAccountsControllerImportedAccount() .build(), ganacheOptions, title: this.test.title, @@ -328,7 +327,7 @@ describe('Import flow', function () { { fixtures: new FixtureBuilder() .withKeyringControllerImportedAccountVault() - .withPreferencesControllerImportedAccountIdentities() + .withAccountsControllerImportedAccount() .build(), ganacheOptions, title: this.test.title, diff --git a/test/e2e/tests/state-logs.spec.js b/test/e2e/tests/state-logs.spec.js index 8cccbcf911bf..c096d6c70607 100644 --- a/test/e2e/tests/state-logs.spec.js +++ b/test/e2e/tests/state-logs.spec.js @@ -69,8 +69,8 @@ describe('State logs', function () { assert.notEqual(info, null); // Verify Json assert.equal( - info?.metamask?.identities[ - '0x5cfe73b6021e818b776b421b1c4db2474086a7e1' + info?.metamask?.internalAccounts.accounts[ + info?.metamask?.internalAccounts.selectedAccount ].address, '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', ); diff --git a/test/e2e/tests/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/state-snapshots/errors-after-init-opt-in-background-state.json index d41af246394d..f446693369bf 100644 --- a/test/e2e/tests/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/state-snapshots/errors-after-init-opt-in-background-state.json @@ -1,5 +1,8 @@ { "AccountTracker": { "accounts": "object" }, + "AccountsController": { + "internalAccounts": { "accounts": "object", "selectedAccount": "string" } + }, "AddressBookController": { "addressBook": "object" }, "AlertController": { "alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true }, @@ -136,7 +139,7 @@ "incomingTransactionsPreferences": {}, "knownMethodData": "object", "currentLocale": "en", - "identities": "object", + "selectedAddress": "string", "lostIdentities": "object", "forgottenPassword": false, "preferences": { diff --git a/test/e2e/tests/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/state-snapshots/errors-after-init-opt-in-ui-state.json index 98a7ecf58f11..34a21d75978f 100644 --- a/test/e2e/tests/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -12,7 +12,7 @@ "isUnlocked": false, "isAccountMenuOpen": false, "isNetworkMenuOpen": false, - "identities": "object", + "internalAccounts": { "accounts": "object", "selectedAccount": "string" }, "transactions": "object", "networkConfigurations": "object", "addressBook": "object", @@ -94,6 +94,7 @@ "openSeaEnabled": false, "advancedGasFee": {}, "incomingTransactionsPreferences": {}, + "selectedAddress": "string", "lostIdentities": "object", "forgottenPassword": false, "ipfsGateway": "string", @@ -103,7 +104,6 @@ "transactionSecurityCheckEnabled": false, "theme": "light", "isLineaMainnetReleased": true, - "selectedAddress": "string", "metaMetricsId": "fake-metrics-id", "eventsBeforeMetricsOptIn": "object", "traits": "object", diff --git a/test/e2e/tests/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/state-snapshots/errors-before-init-opt-in-background-state.json index 75bd62d27908..6a960eedea95 100644 --- a/test/e2e/tests/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/state-snapshots/errors-before-init-opt-in-background-state.json @@ -1,5 +1,8 @@ { "data": { + "AccountsController": { + "internalAccounts": { "selectedAccount": "string", "accounts": "object" } + }, "AlertController": { "alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true }, "unconnectedAccountAlertShownOrigins": "object", @@ -79,7 +82,6 @@ "dismissSeedBackUpReminder": true, "featureFlags": {}, "forgottenPassword": false, - "identities": "object", "ipfsGateway": "string", "knownMethodData": "object", "ledgerTransportType": "webhid", @@ -91,7 +93,6 @@ "showTestNetworks": false, "useNativeCurrencyAsPrimaryCurrency": true }, - "selectedAddress": "string", "theme": "light", "useBlockie": false, "useNftDetection": false, diff --git a/test/e2e/tests/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/state-snapshots/errors-before-init-opt-in-ui-state.json index 5fa13c429985..5bc44d0976d7 100644 --- a/test/e2e/tests/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -1,5 +1,8 @@ { "data": { + "AccountsController": { + "internalAccounts": { "selectedAccount": "string", "accounts": "object" } + }, "AlertController": { "alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true }, "unconnectedAccountAlertShownOrigins": "object", @@ -79,7 +82,6 @@ "dismissSeedBackUpReminder": true, "featureFlags": {}, "forgottenPassword": false, - "identities": "object", "ipfsGateway": "string", "knownMethodData": "object", "ledgerTransportType": "webhid", @@ -91,7 +93,6 @@ "showTestNetworks": false, "useNativeCurrencyAsPrimaryCurrency": true }, - "selectedAddress": "string", "theme": "light", "useBlockie": false, "useNftDetection": false, diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 4ceaded9cee0..8644f222e887 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -1,5 +1,6 @@ import { NetworkType } from '@metamask/controller-utils'; import { NetworkStatus } from '@metamask/network-controller'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { CHAIN_IDS } from '../../shared/constants/network'; import { KeyringType } from '../../shared/constants/keyring'; @@ -230,23 +231,62 @@ export const createSwapsMockStore = () => { '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 2, '0x1111111111111111111111111111111111111111': 0.1, }, - identities: { - '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { - address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', - name: 'Send Account 1', - }, - '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { - address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - name: 'Send Account 2', - }, - '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { - address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', - name: 'Send Account 3', - }, - '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - name: 'Send Account 4', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '07c2cfec-36c9-46c4-8115-3836d3ac9047': { + address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Test Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '15e69915-2a1a-4019-93b3-916e11fd432f': { + address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', + id: '15e69915-2a1a-4019-93b3-916e11fd432f', + metadata: { + name: 'Ledger Hardware 2', + keyring: { + type: 'Ledger Hardware', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '784225f4-d30b-4e77-a900-c8bbce735b88': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: '784225f4-d30b-4e77-a900-c8bbce735b88', + metadata: { + name: 'Test Account 3', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, accounts: { '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { @@ -258,7 +298,6 @@ export const createSwapsMockStore = () => { balance: '0x0', }, }, - selectedAddress: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', currentLocale: 'en', keyringTypes: [KeyringType.imported, KeyringType.hdKeyTree], keyrings: [ diff --git a/ui/components/app/account-list-item/account-list-item-component.test.js b/ui/components/app/account-list-item/account-list-item-component.test.js index a2b607d5d778..4618e9b26c1e 100644 --- a/ui/components/app/account-list-item/account-list-item-component.test.js +++ b/ui/components/app/account-list-item/account-list-item-component.test.js @@ -1,6 +1,7 @@ import React from 'react'; import configureStore from 'redux-mock-store'; import { fireEvent } from '@testing-library/react'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; import mockState from '../../../../test/data/mock-state.json'; import AccountListItem from './account-list-item'; @@ -12,8 +13,17 @@ describe('AccountListItem Component', () => { const props = { account: { address: 'mockAddress', - name: 'mockName', balance: 'mockBalance', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'mockName', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, className: 'mockClassName', displayAddress: false, @@ -58,6 +68,17 @@ describe('AccountListItem Component', () => { ...props, account: { address: 'addressButNoName', + balance: 'mockBalance', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: '', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, }; diff --git a/ui/components/app/account-list-item/account-list-item.js b/ui/components/app/account-list-item/account-list-item.js index 25671dafab82..83bcfc6f23db 100644 --- a/ui/components/app/account-list-item/account-list-item.js +++ b/ui/components/app/account-list-item/account-list-item.js @@ -14,7 +14,11 @@ export default function AccountListItem({ hideDefaultMismatchWarning = false, ///: END:ONLY_INCLUDE_IN }) { - const { name, address, balance } = account || {}; + const { + address, + balance, + metadata: { name }, + } = account || {}; let showDefaultMismatchWarning = true; @@ -61,10 +65,21 @@ AccountListItem.propTypes = { * An account object that has name, address, and balance data */ account: PropTypes.shape({ + id: PropTypes.string.isRequired, address: PropTypes.string.isRequired, - balance: PropTypes.string, - name: PropTypes.string, - }), + balance: PropTypes.string.isRequired, + metadata: PropTypes.shape({ + name: PropTypes.string.isRequired, + snap: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string, + enabled: PropTypes.bool, + }), + keyring: PropTypes.shape({ + type: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + }).isRequired, /** * Additional className to add to the root div element of AccountListItem */ diff --git a/ui/components/app/account-list-item/account-list-item.stories.js b/ui/components/app/account-list-item/account-list-item.stories.js index 5412fadbcf87..5dc128a0edc9 100644 --- a/ui/components/app/account-list-item/account-list-item.stories.js +++ b/ui/components/app/account-list-item/account-list-item.stories.js @@ -1,4 +1,5 @@ import React from 'react'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import README from './README.mdx'; import AccountListItem from './account-list-item'; @@ -22,9 +23,17 @@ export default { }; const account = { - name: 'Account 2', address: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', balance: '0x2d3142f5000', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }; export const DefaultStory = (args) => { diff --git a/ui/components/app/advanced-gas-controls/advanced-gas-controls.test.js b/ui/components/app/advanced-gas-controls/advanced-gas-controls.test.js index 6a15f9c6b4a2..7ce522dd6325 100644 --- a/ui/components/app/advanced-gas-controls/advanced-gas-controls.test.js +++ b/ui/components/app/advanced-gas-controls/advanced-gas-controls.test.js @@ -7,7 +7,7 @@ import AdvancedGasControls from './advanced-gas-controls.component'; const renderComponent = (props) => { const store = configureMockStore([])({ - metamask: { identities: [], providerConfig: {} }, + metamask: { providerConfig: {} }, }); return renderWithProvider(, store); }; diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.stories.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.stories.js index 5769f235b881..c8e62d9969c0 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.stories.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.stories.js @@ -8,13 +8,18 @@ import { GasFeeContextProvider } from '../../../../contexts/gasFee'; import { GasEstimateTypes } from '../../../../../shared/constants/gas'; import AdvancedGasFeeDefaults from './advanced-gas-fee-defaults'; +const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; + const store = configureStore({ ...mockState, metamask: { ...mockState.metamask, accounts: { - [mockState.metamask.selectedAddress]: { - address: mockState.metamask.selectedAddress, + [selectedAddress]: { + address: selectedAddress, balance: '0x1F4', }, }, diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js index e93c1c44ae55..bd3b25769f35 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js @@ -33,13 +33,17 @@ jest.mock('../../../../store/actions', () => ({ })); const render = (defaultGasParams, contextParams) => { + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; const store = configureStore({ metamask: { ...mockState.metamask, ...defaultGasParams, accounts: { - [mockState.metamask.selectedAddress]: { - address: mockState.metamask.selectedAddress, + [selectedAddress]: { + address: selectedAddress, balance: '0x1F4', }, }, diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-gas-limit/advanced-gas-fee-gas-limit.test.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-gas-limit/advanced-gas-fee-gas-limit.test.js index 7d1495883fb5..917c7d9e9d54 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-gas-limit/advanced-gas-fee-gas-limit.test.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-gas-limit/advanced-gas-fee-gas-limit.test.js @@ -22,12 +22,16 @@ jest.mock('../../../../store/actions', () => ({ })); const render = (contextProps) => { + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; const store = configureStore({ metamask: { ...mockState.metamask, accounts: { - [mockState.metamask.selectedAddress]: { - address: mockState.metamask.selectedAddress, + [selectedAddress]: { + address: selectedAddress, balance: '0x1F4', }, }, diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.test.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.test.js index c6f088164802..c8928e82504c 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.test.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.test.js @@ -25,12 +25,16 @@ jest.mock('../../../../../store/actions', () => ({ })); const render = (txProps, contextProps) => { + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; const store = configureStore({ metamask: { ...mockState.metamask, accounts: { - [mockState.metamask.selectedAddress]: { - address: mockState.metamask.selectedAddress, + [selectedAddress]: { + address: selectedAddress, balance: '0x1F4', }, }, diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js index eab4d43ac939..681ad9dc8c16 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js @@ -26,12 +26,16 @@ jest.mock('../../../../../store/actions', () => ({ })); const render = (txProps, contextProps) => { + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; const store = configureStore({ metamask: { ...mockState.metamask, accounts: { - [mockState.metamask.selectedAddress]: { - address: mockState.metamask.selectedAddress, + [selectedAddress]: { + address: selectedAddress, balance: '0x1F4', }, }, diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-popover.test.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-popover.test.js index 3b60b3bab288..166f3bd59c06 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-popover.test.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-popover.test.js @@ -29,12 +29,16 @@ jest.mock('../../../contexts/transaction-modal', () => ({ })); const render = () => { + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; const store = configureStore({ metamask: { ...mockState.metamask, accounts: { - [mockState.metamask.selectedAddress]: { - address: mockState.metamask.selectedAddress, + [selectedAddress]: { + address: selectedAddress, balance: '0x1F4', }, }, diff --git a/ui/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js b/ui/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js index 1a64b0aa0bbb..618534ec9b94 100644 --- a/ui/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js +++ b/ui/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js @@ -12,8 +12,7 @@ import { import { getOriginOfCurrentTab, getOrderedConnectedAccountsForActiveTab, - getSelectedAddress, - getSelectedIdentity, + getSelectedInternalAccount, } from '../../../../selectors'; import { isExtensionUrl, getURLHost } from '../../../../helpers/utils/util'; import Popover from '../../../ui/popover'; @@ -34,8 +33,8 @@ const UnconnectedAccountAlert = () => { getOrderedConnectedAccountsForActiveTab, ); const origin = useSelector(getOriginOfCurrentTab); - const selectedIdentity = useSelector(getSelectedIdentity); - const selectedAddress = useSelector(getSelectedAddress); + const account = useSelector(getSelectedInternalAccount); + const { address: selectedAddress } = account; const [dontShowThisAgain, setDontShowThisAgain] = useState(false); const onClose = async () => { @@ -98,11 +97,11 @@ const UnconnectedAccountAlert = () => { footer={footer} > dispatch(connectAccount(selectedAddress))} connectedAccounts={connectedAccounts} selectedAddress={selectedAddress} - setSelectedAddress={(address) => dispatch(switchToAccount(address))} + setSelectedAccount={(accountId) => dispatch(switchToAccount(accountId))} shouldRenderListOptions={false} /> diff --git a/ui/components/app/alerts/unconnected-account-alert/unconnected-account-alert.test.js b/ui/components/app/alerts/unconnected-account-alert/unconnected-account-alert.test.js index 10c514743429..8b59bb1517d7 100644 --- a/ui/components/app/alerts/unconnected-account-alert/unconnected-account-alert.test.js +++ b/ui/components/app/alerts/unconnected-account-alert/unconnected-account-alert.test.js @@ -5,6 +5,7 @@ import thunk from 'redux-thunk'; import { fireEvent } from '@testing-library/react'; import configureMockStore from 'redux-mock-store'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { tick } from '../../../../../test/lib/tick'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; @@ -16,17 +17,36 @@ import { KeyringType } from '../../../../../shared/constants/keyring'; import UnconnectedAccountAlert from '.'; describe('Unconnected Account Alert', () => { - const selectedAddress = '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b'; - - const identities = { - '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - name: 'Account 1', - }, - '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b': { - address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b', - name: 'Account 2', + const internalAccounts = { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '07c2cfec-36c9-46c4-8115-3836d3ac9047': { + address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b', + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }; const accounts = { @@ -59,8 +79,7 @@ describe('Unconnected Account Alert', () => { const mockState = { metamask: { - selectedAddress, - identities, + internalAccounts, accounts, cachedBalances, keyrings, diff --git a/ui/components/app/asset-list/asset-list.test.js b/ui/components/app/asset-list/asset-list.test.js index cb12394ac4f3..fc5089634c00 100644 --- a/ui/components/app/asset-list/asset-list.test.js +++ b/ui/components/app/asset-list/asset-list.test.js @@ -50,6 +50,10 @@ jest.mock('../../../hooks/useTokenTracker', () => { }); const render = () => { + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; const state = { ...mockState, metamask: { @@ -58,7 +62,7 @@ const render = () => { conversionRate: CONVERSION_RATE, cachedBalances: { [CHAIN_IDS.MAINNET]: { - [mockState.metamask.selectedAddress]: ETH_BALANCE, + [selectedAddress]: ETH_BALANCE, }, }, contractExchangeRates: { diff --git a/ui/components/app/cancel-button/cancel-button.js b/ui/components/app/cancel-button/cancel-button.js index c751fa668847..bedae6c0dff9 100644 --- a/ui/components/app/cancel-button/cancel-button.js +++ b/ui/components/app/cancel-button/cancel-button.js @@ -9,7 +9,7 @@ import { getConversionRate } from '../../../ducks/metamask/metamask'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { useIncrementedGasFees } from '../../../hooks/useIncrementedGasFees'; import { isBalanceSufficient } from '../../../pages/send/send.utils'; -import { getSelectedAccount } from '../../../selectors'; +import { getSelectedInternalAccountWithBalance } from '../../../selectors'; export default function CancelButton({ cancelTransaction, @@ -20,7 +20,7 @@ export default function CancelButton({ const customCancelGasSettings = useIncrementedGasFees(transaction); - const selectedAccount = useSelector(getSelectedAccount); + const selectedAccount = useSelector(getSelectedInternalAccountWithBalance); const conversionRate = useSelector(getConversionRate); const hasEnoughCancelGas = isBalanceSufficient({ diff --git a/ui/components/app/cancel-speedup-popover/cancel-speedup-popover.stories.js b/ui/components/app/cancel-speedup-popover/cancel-speedup-popover.stories.js index 0b2d541d771e..5f8f1967feea 100644 --- a/ui/components/app/cancel-speedup-popover/cancel-speedup-popover.stories.js +++ b/ui/components/app/cancel-speedup-popover/cancel-speedup-popover.stories.js @@ -13,12 +13,16 @@ import { decGWEIToHexWEI } from '../../../../shared/modules/conversion.utils'; import { GasFeeContextProvider } from '../../../contexts/gasFee'; import CancelSpeedupPopover from './cancel-speedup-popover'; +const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; const store = configureStore({ metamask: { ...mockState.metamask, accounts: { - [mockState.metamask.selectedAddress]: { - address: mockState.metamask.selectedAddress, + [selectedAddress]: { + address: selectedAddress, balance: '0x1F4', }, }, diff --git a/ui/components/app/cancel-speedup-popover/cancel-speedup-popover.test.js b/ui/components/app/cancel-speedup-popover/cancel-speedup-popover.test.js index 1512b8992a09..4dc20d5f515d 100644 --- a/ui/components/app/cancel-speedup-popover/cancel-speedup-popover.test.js +++ b/ui/components/app/cancel-speedup-popover/cancel-speedup-popover.test.js @@ -73,12 +73,16 @@ const render = ( props, maxFeePerGas = MOCK_SUGGESTED_MEDIUM_MAXFEEPERGAS_HEX_WEI, ) => { + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; const store = configureStore({ metamask: { ...mockState.metamask, accounts: { - [mockState.metamask.selectedAddress]: { - address: mockState.metamask.selectedAddress, + [selectedAddress]: { + address: selectedAddress, balance: '0x1F4', }, }, diff --git a/ui/components/app/confirm-gas-display/confirm-gas-display.test.js b/ui/components/app/confirm-gas-display/confirm-gas-display.test.js index 2851e84f795b..744978ba9052 100644 --- a/ui/components/app/confirm-gas-display/confirm-gas-display.test.js +++ b/ui/components/app/confirm-gas-display/confirm-gas-display.test.js @@ -22,14 +22,18 @@ jest.mock('../../../store/actions', () => ({ })); const render = ({ transactionProp = {}, contextProps = {} } = {}) => { + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; const store = configureStore({ ...mockState, ...contextProps, metamask: { ...mockState.metamask, accounts: { - [mockState.metamask.selectedAddress]: { - address: mockState.metamask.selectedAddress, + [selectedAddress]: { + address: selectedAddress, balance: '0x1F4', }, }, diff --git a/ui/components/app/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js b/ui/components/app/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js index 6a96b3b436ec..0a3edc55dcc5 100644 --- a/ui/components/app/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js +++ b/ui/components/app/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js @@ -8,13 +8,17 @@ import configureStore from '../../../../store/store'; import ConfirmLegacyGasDisplay from './confirm-legacy-gas-display'; +const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; const mmState = { ...mockState, metamask: { ...mockState.metamask, accounts: { - [mockState.metamask.selectedAddress]: { - address: mockState.metamask.selectedAddress, + [selectedAddress]: { + address: selectedAddress, balance: '0x1F4', }, }, diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.test.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.test.js index f81301c195ec..be473eec602e 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.test.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.test.js @@ -1,6 +1,7 @@ import { fireEvent } from '@testing-library/react'; import React from 'react'; import configureMockStore from 'redux-mock-store'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { TransactionType } from '../../../../../shared/constants/transaction'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import { @@ -25,6 +26,63 @@ describe('Confirm Page Container Content', () => { }, }, }, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '07c2cfec-36c9-46c4-8115-3836d3ac9047': { + address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b', + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Test Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '15e69915-2a1a-4019-93b3-916e11fd432f': { + address: '0xc42edfcc21ed14dda456aa0756c153f7985d8813', + id: '15e69915-2a1a-4019-93b3-916e11fd432f', + metadata: { + name: 'Ledger Hardware 2', + keyring: { + type: 'Ledger Hardware', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '784225f4-d30b-4e77-a900-c8bbce735b88': { + address: '0xeb9e64b93097bc15f01f13eae97015c57ab64823', + id: '784225f4-d30b-4e77-a900-c8bbce735b88', + metadata: { + name: 'Test Account 3', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, }, }; diff --git a/ui/components/app/confirm-page-container/confirm-page-container.component.js b/ui/components/app/confirm-page-container/confirm-page-container.component.js index e40dc23a2887..be0040021163 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.component.js @@ -45,9 +45,9 @@ import useTransactionInsights from '../../../hooks/useTransactionInsights'; import { getAccountName, getAddressBookEntry, + getInternalAccounts, getIsBuyableChain, getMetadataContractName, - getMetaMaskIdentities, getNetworkIdentifier, getSwapsDefaultToken, } from '../../../selectors'; @@ -124,8 +124,8 @@ const ConfirmPageContainer = (props) => { const networkIdentifier = useSelector(getNetworkIdentifier); const defaultToken = useSelector(getSwapsDefaultToken); const accountBalance = defaultToken.string; - const identities = useSelector(getMetaMaskIdentities); - const ownedAccountName = getAccountName(identities, toAddress); + const accounts = useSelector(getInternalAccounts); + const ownedAccountName = getAccountName(accounts, toAddress); const toName = ownedAccountName || contact?.name; const recipientIsOwnedAccount = Boolean(ownedAccountName); const toMetadataName = useSelector((state) => diff --git a/ui/components/app/confirm-page-container/confirm-page-container.container.js b/ui/components/app/confirm-page-container/confirm-page-container.container.js index fb628fbb2ee5..2815b600f5b5 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.container.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.container.js @@ -6,7 +6,7 @@ import { getSwapsDefaultToken, getMetadataContractName, getAccountName, - getMetaMaskIdentities, + getInternalAccounts, } from '../../../selectors'; import ConfirmPageContainer from './confirm-page-container.component'; @@ -17,8 +17,8 @@ function mapStateToProps(state, ownProps) { const networkIdentifier = getNetworkIdentifier(state); const defaultToken = getSwapsDefaultToken(state); const accountBalance = defaultToken.string; - const identities = getMetaMaskIdentities(state); - const ownedAccountName = getAccountName(identities, to); + const accounts = getInternalAccounts(state); + const ownedAccountName = getAccountName(accounts, to); const toName = ownedAccountName || contact?.name; const toMetadataName = getMetadataContractName(state, to); diff --git a/ui/components/app/confirm-subtitle/confirm-subtitle.test.js b/ui/components/app/confirm-subtitle/confirm-subtitle.test.js index 6b8d1a156759..3c123afc5a98 100644 --- a/ui/components/app/confirm-subtitle/confirm-subtitle.test.js +++ b/ui/components/app/confirm-subtitle/confirm-subtitle.test.js @@ -43,9 +43,13 @@ describe('ConfirmSubTitle', () => { }); it('should not return null if it is NFT Transfer', async () => { + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; mockState.metamask.preferences.showFiatInTestnets = false; mockState.metamask.allNftContracts = { - [mockState.metamask.selectedAddress]: { + [selectedAddress]: { [mockState.metamask.providerConfig.chainId]: [{ address: '0x9' }], }, }; diff --git a/ui/components/app/connected-accounts-list/connected-accounts-list.component.js b/ui/components/app/connected-accounts-list/connected-accounts-list.component.js index 11563efc82d5..3b7350381cc3 100644 --- a/ui/components/app/connected-accounts-list/connected-accounts-list.component.js +++ b/ui/components/app/connected-accounts-list/connected-accounts-list.component.js @@ -16,20 +16,43 @@ export default class ConnectedAccountsList extends PureComponent { static propTypes = { accountToConnect: PropTypes.shape({ + id: PropTypes.string.isRequired, address: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, + balance: PropTypes.string.isRequired, + metadata: PropTypes.shape({ + name: PropTypes.string.isRequired, + snap: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string, + enabled: PropTypes.bool, + }), + keyring: PropTypes.shape({ + type: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, }), connectedAccounts: PropTypes.arrayOf( PropTypes.shape({ + id: PropTypes.string.isRequired, address: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - lastActive: PropTypes.number, - }), - ).isRequired, + balance: PropTypes.string.isRequired, + metadata: PropTypes.shape({ + name: PropTypes.string.isRequired, + snap: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string, + enabled: PropTypes.bool, + }), + keyring: PropTypes.shape({ + type: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + }).isRequired, + ), connectAccount: PropTypes.func.isRequired, selectedAddress: PropTypes.string.isRequired, removePermittedAccount: PropTypes.func, - setSelectedAddress: PropTypes.func.isRequired, + setSelectedAccount: PropTypes.func.isRequired, shouldRenderListOptions: (props, propName, componentName) => { if (typeof props[propName] !== 'boolean') { return new Error( @@ -57,7 +80,7 @@ export default class ConnectedAccountsList extends PureComponent { switchAccount = (address) => { this.hideAccountOptions(); - this.props.setSelectedAddress(address); + this.props.setSelectedAccount(address); }; hideAccountOptions = () => { @@ -76,7 +99,10 @@ export default class ConnectedAccountsList extends PureComponent { return null; } - const { address, name } = accountToConnect; + const { + address, + metadata: { name }, + } = accountToConnect; return ( this.switchAccount(address)} + onClick={() => this.switchAccount(accountId)} > {t('switchToThisAccount')} @@ -134,26 +160,28 @@ export default class ConnectedAccountsList extends PureComponent { <>
{this.renderUnconnectedAccount()} - {connectedAccounts.map(({ address, name }, index) => { - return ( - - ); - })} + {connectedAccounts.map( + ({ id, address, metadata: { name } }, index) => { + return ( + + ); + }, + )}
); diff --git a/ui/components/app/connected-accounts-list/connected-accounts-list.stories.js b/ui/components/app/connected-accounts-list/connected-accounts-list.stories.js index 793b0acca9b4..064605b317d0 100644 --- a/ui/components/app/connected-accounts-list/connected-accounts-list.stories.js +++ b/ui/components/app/connected-accounts-list/connected-accounts-list.stories.js @@ -8,7 +8,7 @@ export default { connectedAccounts: { control: 'array', }, - selectedAddress: { + setSelectedAccount: { control: 'text', }, shouldRenderListOptions: { @@ -18,16 +18,46 @@ export default { args: { connectedAccounts: [ { - name: 'This is a Really Long Account Name', + id: 'mock-id', address: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', - index: 0, balance: '0x176e5b6f173ebe66', + metadata: { + name: 'This is a Really Long Account Name', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', }, { - name: 'Account 2', + id: 'mock-id-2', address: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', - index: 1, balance: '0x2d3142f5000', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', }, ], }, diff --git a/ui/components/app/connected-status-indicator/connected-status-indicator.js b/ui/components/app/connected-status-indicator/connected-status-indicator.js index c1a4eae402f0..4099bd17ceb8 100644 --- a/ui/components/app/connected-status-indicator/connected-status-indicator.js +++ b/ui/components/app/connected-status-indicator/connected-status-indicator.js @@ -15,14 +15,14 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import { getAddressConnectedSubjectMap, getOriginOfCurrentTab, - getSelectedAddress, + getSelectedInternalAccount, } from '../../../selectors'; import { ConnectedSiteMenu } from '../../multichain'; export default function ConnectedStatusIndicator({ onClick }) { const t = useI18nContext(); - const selectedAddress = useSelector(getSelectedAddress); + const { address: selectedAddress } = useSelector(getSelectedInternalAccount); const addressConnectedSubjectMap = useSelector(getAddressConnectedSubjectMap); const originOfCurrentTab = useSelector(getOriginOfCurrentTab); diff --git a/ui/components/app/contact-list/recipient-group/recipient-group.component.js b/ui/components/app/contact-list/recipient-group/recipient-group.component.js index 031761f6a340..87d806a956fd 100644 --- a/ui/components/app/contact-list/recipient-group/recipient-group.component.js +++ b/ui/components/app/contact-list/recipient-group/recipient-group.component.js @@ -40,43 +40,47 @@ export default function RecipientGroup({ {label} )} - {items.map(({ address, name }) => ( - onSelect(address, name)} - className={classnames({ - 'send__select-recipient-wrapper__group-item': !addressesEqual( - address, - selectedAddress, - ), - 'send__select-recipient-wrapper__group-item--selected': - addressesEqual(address, selectedAddress), - })} - padding={4} - > - + {items.map((item) => { + const { address } = item; + const name = item.name ? item.name : item.metadata.name; // item could be an address book entry or internal account + return ( onSelect(address, name)} + className={classnames({ + 'send__select-recipient-wrapper__group-item': !addressesEqual( + address, + selectedAddress, + ), + 'send__select-recipient-wrapper__group-item--selected': + addressesEqual(address, selectedAddress), + })} + padding={4} > - + - {name || ellipsify(address)} - - {name && ( - {ellipsify(address)} + {name || ellipsify(address)} - )} + {name && ( + + {ellipsify(address)} + + )} + - - ))} + ); + })} ); } @@ -86,7 +90,9 @@ RecipientGroup.propTypes = { items: PropTypes.arrayOf( PropTypes.shape({ address: PropTypes.string.isRequired, - name: PropTypes.string, + metadata: PropTypes.shape({ + name: PropTypes.string, + }), }), ), onSelect: PropTypes.func.isRequired, diff --git a/ui/components/app/edit-gas-fee-button/edit-gas-fee-button.test.js b/ui/components/app/edit-gas-fee-button/edit-gas-fee-button.test.js index df20bc8efcbf..d177cfaf961a 100644 --- a/ui/components/app/edit-gas-fee-button/edit-gas-fee-button.test.js +++ b/ui/components/app/edit-gas-fee-button/edit-gas-fee-button.test.js @@ -26,12 +26,16 @@ jest.mock('../../../store/actions', () => ({ })); const render = ({ componentProps, contextProps } = {}) => { + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; const store = configureStore({ metamask: { ...mockState.metamask, accounts: { - [mockState.metamask.selectedAddress]: { - address: mockState.metamask.selectedAddress, + [selectedAddress]: { + address: selectedAddress, balance: '0x1F4', }, }, diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.test.js b/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.test.js index b7382bc5ddb0..535f72ffe947 100644 --- a/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.test.js +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.test.js @@ -1,5 +1,6 @@ import React from 'react'; import { screen } from '@testing-library/react'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { EditGasModes } from '../../../../shared/constants/gas'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; @@ -61,10 +62,24 @@ const render = ({ txProps, contextProps } = {}) => { balance: '0x1F4', }, }, - identities: { - '0xAddress': {}, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xAddress', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, - selectedAddress: '0xAddress', featureFlags: { advancedInlineGas: true }, gasFeeEstimates: MOCK_FEE_ESTIMATE, advancedGasFee: {}, diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js b/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js index ea3191e2d7e5..c8b45a700139 100644 --- a/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js @@ -1,5 +1,6 @@ import React from 'react'; import { screen } from '@testing-library/react'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { EditGasModes, @@ -70,10 +71,24 @@ const renderComponent = ({ balance: '0x176e5b6f173ebe66', }, }, - identities: { - '0xAddress': {}, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xAddress', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, - selectedAddress: '0xAddress', featureFlags: { advancedInlineGas: true }, gasEstimateType: 'fee-market', gasFeeEstimates: MOCK_FEE_ESTIMATE, diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.test.js b/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.test.js index 97797c1108e7..06565063a1bb 100644 --- a/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.test.js +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.test.js @@ -1,4 +1,5 @@ import React from 'react'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import configureStore from '../../../../store/store'; import { renderWithProvider } from '../../../../../test/jest'; import { GasFeeContextProvider } from '../../../../contexts/gasFee'; @@ -41,10 +42,24 @@ const renderComponent = (componentProps) => { balance: '0x176e5b6f173ebe66', }, }, - identities: { - '0xAddress': {}, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xAddress', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, - selectedAddress: '0xAddress', featureFlags: { advancedInlineGas: true }, }, }; diff --git a/ui/components/app/gas-details-item/gas-details-item-title/gas-details-item-title.test.js b/ui/components/app/gas-details-item/gas-details-item-title/gas-details-item-title.test.js index b57c7e32e7c8..7c21406d1c02 100644 --- a/ui/components/app/gas-details-item/gas-details-item-title/gas-details-item-title.test.js +++ b/ui/components/app/gas-details-item/gas-details-item-title/gas-details-item-title.test.js @@ -1,5 +1,6 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { CHAIN_IDS } from '../../../../../shared/constants/network'; import { GasFeeContextProvider } from '../../../../contexts/gasFee'; @@ -28,10 +29,24 @@ const render = () => { balance: '0x176e5b6f173ebe66', }, }, - identities: { - '0xAddress': {}, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xAddress', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, - selectedAddress: '0xAddress', }, }); diff --git a/ui/components/app/gas-details-item/gas-details-item.test.js b/ui/components/app/gas-details-item/gas-details-item.test.js index 87511133a1f9..ec2cea335940 100644 --- a/ui/components/app/gas-details-item/gas-details-item.test.js +++ b/ui/components/app/gas-details-item/gas-details-item.test.js @@ -20,12 +20,16 @@ jest.mock('../../../store/actions', () => ({ })); const render = ({ contextProps } = {}) => { + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; const store = configureStore({ metamask: { ...mockState.metamask, accounts: { - [mockState.metamask.selectedAddress]: { - address: mockState.metamask.selectedAddress, + [selectedAddress]: { + address: selectedAddress, balance: '0x1F4', }, }, diff --git a/ui/components/app/modals/confirm-remove-account/__snapshots__/confirm-remove-account.test.js.snap b/ui/components/app/modals/confirm-remove-account/__snapshots__/confirm-remove-account.test.js.snap index 0585d2b1d6b3..501ce9397d4a 100644 --- a/ui/components/app/modals/confirm-remove-account/__snapshots__/confirm-remove-account.test.js.snap +++ b/ui/components/app/modals/confirm-remove-account/__snapshots__/confirm-remove-account.test.js.snap @@ -36,7 +36,7 @@ exports[`Confirm Remove Account should match snapshot 1`] = ` style="height: 32px; width: 32px; border-radius: 16px;" >
{ this.props - .removeAccount(this.props.identity.address) + .removeAccount(this.props.account.id) .then(() => this.props.hideModal()); }; @@ -33,31 +48,31 @@ export default class ConfirmRemoveAccount extends Component { renderSelectedAccount() { const { t } = this.context; - const { identity, rpcPrefs, chainId } = this.props; + const { account, rpcPrefs, chainId } = this.props; return (
- +
{t('name')} - {identity.name} + {account.metadata.name}
{t('publicAddress')} - {addressSummary(identity.address, 4, 4)} + {addressSummary(account.address, 4, 4)}
{ const accountLink = getAccountLink( - identity.address, + account.address, chainId, rpcPrefs, ); diff --git a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.container.js b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.container.js index c7c07d0dfd8b..1c74a92e5215 100644 --- a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.container.js +++ b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.container.js @@ -17,7 +17,7 @@ const mapStateToProps = (state) => { const mapDispatchToProps = (dispatch) => { return { - removeAccount: (address) => dispatch(removeAccount(address)), + removeAccount: (accountId) => dispatch(removeAccount(accountId)), }; }; diff --git a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.stories.js b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.stories.js index b7f9bcea2bb8..a7f9931fa03f 100644 --- a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.stories.js +++ b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.stories.js @@ -1,4 +1,5 @@ import React from 'react'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import ConfirmRemoveAccount from '.'; export default { @@ -6,13 +7,23 @@ export default { component: ConfirmRemoveAccount, argTypes: { - identity: { + account: { control: 'object', }, }, args: { - identity: { - control: 'object', + account: { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, }, }; diff --git a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js index 39e353565ea9..bd5b03556373 100644 --- a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js +++ b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js @@ -1,6 +1,7 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import { fireEvent } from '@testing-library/react'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import ConfirmRemoveAccount from '.'; @@ -10,15 +11,42 @@ describe('Confirm Remove Account', () => { providerConfig: { chainId: '0x99', }, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, }, }; const props = { hideModal: jest.fn(), removeAccount: jest.fn().mockResolvedValue(), - identity: { - address: '0x0', - name: 'Account 1', + account: { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + mmethods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, chainId: '0x99', rpcPrefs: {}, @@ -55,7 +83,7 @@ describe('Confirm Remove Account', () => { fireEvent.click(queryByText('Remove')); - expect(props.removeAccount).toHaveBeenCalledWith(props.identity.address); + expect(props.removeAccount).toHaveBeenCalledWith(props.account.id); expect(props.hideModal).toHaveBeenCalled(); }); diff --git a/ui/components/app/modals/edit-approval-permission/edit-approval-permission.component.js b/ui/components/app/modals/edit-approval-permission/edit-approval-permission.component.js index caccc2ad6874..14e488246dfe 100644 --- a/ui/components/app/modals/edit-approval-permission/edit-approval-permission.component.js +++ b/ui/components/app/modals/edit-approval-permission/edit-approval-permission.component.js @@ -22,7 +22,7 @@ export default class EditApprovalPermission extends PureComponent { static propTypes = { decimals: PropTypes.number, hideModal: PropTypes.func.isRequired, - selectedIdentity: PropTypes.object, + selectedAccount: PropTypes.object, tokenAmount: PropTypes.string, customTokenAmount: PropTypes.string, tokenSymbol: PropTypes.string, @@ -46,14 +46,14 @@ export default class EditApprovalPermission extends PureComponent { const { t } = this.context; const { hideModal, - selectedIdentity, + selectedAccount, tokenAmount, tokenSymbol, tokenBalance, customTokenAmount, origin, } = this.props; - const { name, address } = selectedIdentity || {}; + const { name, address } = selectedAccount || {}; const { selectedOptionIsUnlimited } = this.state; return ( diff --git a/ui/components/app/modals/edit-approval-permission/edit-approval-permission.container.js b/ui/components/app/modals/edit-approval-permission/edit-approval-permission.container.js index 4a3777c72400..73cdd08eb5c5 100644 --- a/ui/components/app/modals/edit-approval-permission/edit-approval-permission.container.js +++ b/ui/components/app/modals/edit-approval-permission/edit-approval-permission.container.js @@ -1,13 +1,13 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'; -import { getSelectedIdentity } from '../../../../selectors'; +import { getSelectedInternalAccount } from '../../../../selectors'; import EditApprovalPermission from './edit-approval-permission.component'; const mapStateToProps = (state) => { const modalStateProps = state.appState.modal.modalState.props || {}; return { - selectedIdentity: getSelectedIdentity(state), + selectedAccount: getSelectedInternalAccount(state), ...modalStateProps, }; }; diff --git a/ui/components/app/modals/new-account-modal/new-account-modal.container.js b/ui/components/app/modals/new-account-modal/new-account-modal.container.js index 2a1ba3175e80..8756309ec3ac 100644 --- a/ui/components/app/modals/new-account-modal/new-account-modal.container.js +++ b/ui/components/app/modals/new-account-modal/new-account-modal.container.js @@ -12,11 +12,11 @@ function mapDispatchToProps(dispatch) { return { hideModal: () => dispatch(actions.hideModal()), createAccount: (newAccountName) => { - return dispatch(actions.addNewAccount()).then((newAccountAddress) => { + return dispatch(actions.addNewAccount(newAccountName)).then((account) => { if (newAccountName) { - dispatch(actions.setAccountLabel(newAccountAddress, newAccountName)); + dispatch(actions.setAccountLabel(account.id, newAccountName)); } - return newAccountAddress; + return account.id; }); }, }; @@ -30,8 +30,8 @@ function mergeProps(stateProps, dispatchProps) { ...stateProps, ...dispatchProps, onSave: (newAccountName) => { - return createAccount(newAccountName).then((newAccountAddress) => - onCreateNewAccount(newAccountAddress), + return createAccount(newAccountName).then((account) => + onCreateNewAccount(account), ); }, }; diff --git a/ui/components/app/nft-details/nft-details.js b/ui/components/app/nft-details/nft-details.js index cb5c0f121e34..4a2d4c9a7512 100644 --- a/ui/components/app/nft-details/nft-details.js +++ b/ui/components/app/nft-details/nft-details.js @@ -26,7 +26,7 @@ import { getCurrentChainId, getCurrentNetwork, getIpfsGateway, - getSelectedIdentity, + getSelectedInternalAccount, } from '../../../selectors'; import AssetNavigation from '../../../pages/asset/components/asset-navigation'; import { getNftContracts } from '../../../ducks/metamask/metamask'; @@ -81,9 +81,9 @@ export default function NftDetails({ nft }) { const nftContractName = nftContracts.find(({ address: contractAddress }) => isEqualCaseInsensitive(contractAddress, address), )?.name; - const selectedAccountName = useSelector( - (state) => getSelectedIdentity(state).name, - ); + const { + metadata: { name: selectedAccountName }, + } = useSelector(getSelectedInternalAccount); const nftImageAlt = getNftImageAlt(nft); const nftSrcUrl = imageOriginal ?? image; const nftImageURL = getAssetImageURL(imageOriginal ?? image, ipfsGateway); diff --git a/ui/components/app/nft-details/nft-details.test.js b/ui/components/app/nft-details/nft-details.test.js index 7733e75aed3b..92ea24c859da 100644 --- a/ui/components/app/nft-details/nft-details.test.js +++ b/ui/components/app/nft-details/nft-details.test.js @@ -48,9 +48,11 @@ jest.mock('../../../store/actions.ts', () => ({ describe('NFT Details', () => { const mockStore = configureMockStore([thunk])(mockState); - - const nfts = - mockState.metamask.allNfts[mockState.metamask.selectedAddress][toHex(5)]; + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; + const nfts = mockState.metamask.allNfts[selectedAddress][toHex(5)]; const props = { nft: nfts[5], diff --git a/ui/components/app/nfts-items/nfts-items.js b/ui/components/app/nfts-items/nfts-items.js index d2e5eeb6630d..59dd23e1efac 100644 --- a/ui/components/app/nfts-items/nfts-items.js +++ b/ui/components/app/nfts-items/nfts-items.js @@ -20,7 +20,7 @@ import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { getCurrentChainId, getIpfsGateway, - getSelectedAddress, + getSelectedInternalAccount, getCurrentNetwork, getOpenSeaEnabled, } from '../../../selectors'; @@ -49,7 +49,7 @@ export default function NftsItems({ const collectionsKeys = Object.keys(collections); const nftsDropdownState = useSelector(getNftsDropdownState); const previousCollectionKeys = usePrevious(collectionsKeys); - const selectedAddress = useSelector(getSelectedAddress); + const { address: selectedAddress } = useSelector(getSelectedInternalAccount); const chainId = useSelector(getCurrentChainId); const currentChain = useSelector(getCurrentNetwork); const t = useI18nContext(); diff --git a/ui/components/app/nfts-items/nfts-items.test.js b/ui/components/app/nfts-items/nfts-items.test.js index cd57d3e52427..da1389bb91ab 100644 --- a/ui/components/app/nfts-items/nfts-items.test.js +++ b/ui/components/app/nfts-items/nfts-items.test.js @@ -24,8 +24,11 @@ jest.mock('../../../store/actions.ts', () => ({ })); describe('NFTs Item Component', () => { - const nfts = - mockState.metamask.allNfts[mockState.metamask.selectedAddress][toHex(5)]; + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; + const nfts = mockState.metamask.allNfts[selectedAddress][toHex(5)]; const props = { collections: { '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { diff --git a/ui/components/app/nfts-tab/nfts-tab.test.js b/ui/components/app/nfts-tab/nfts-tab.test.js index bae914d4139f..b9c98166f055 100644 --- a/ui/components/app/nfts-tab/nfts-tab.test.js +++ b/ui/components/app/nfts-tab/nfts-tab.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import reactRouterDom from 'react-router-dom'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import configureStore from '../../../store/store'; import { renderWithProvider } from '../../../../test/jest/rendering'; import { SECURITY_ROUTE } from '../../../helpers/constants/routes'; @@ -164,7 +165,24 @@ const render = ({ }, }, providerConfig: { chainId }, - selectedAddress, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: selectedAddress, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, useNftDetection, nftsDropdownState, }, diff --git a/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js b/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js index 96a39ec0fe4c..499f472a79ef 100644 --- a/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js +++ b/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js @@ -17,13 +17,20 @@ export default class PermissionPageContainerContent extends PureComponent { iconUrl: PropTypes.string, }), selectedPermissions: PropTypes.object.isRequired, - selectedIdentities: PropTypes.array, - allIdentitiesSelected: PropTypes.bool, + selectedAccounts: PropTypes.arrayOf( + PropTypes.shape({ + address: PropTypes.string.isRequired, + addressLabel: PropTypes.string.isRequired, + balance: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + }), + ), + allAccountsSelected: PropTypes.bool, }; static defaultProps = { - selectedIdentities: [], - allIdentitiesSelected: false, + selectedAccounts: [], + allAccountsSelected: false, }; static contextTypes = { @@ -44,7 +51,7 @@ export default class PermissionPageContainerContent extends PureComponent { } renderAccountTooltip(textContent) { - const { selectedIdentities } = this.props; + const { selectedAccounts } = this.props; const { t } = this.context; return ( @@ -54,15 +61,15 @@ export default class PermissionPageContainerContent extends PureComponent { wrapperClassName="permission-approval-container__bold-title-elements" html={
- {selectedIdentities.slice(0, 6).map((identity, index) => { + {selectedAccounts.slice(0, 6).map((account, index) => { return (
- {identity.addressLabel} + {account.addressLabel}
); })} - {selectedIdentities.length > 6 - ? t('plusXMore', [selectedIdentities.length - 6]) + {selectedAccounts.length > 6 + ? t('plusXMore', [selectedAccounts.length - 6]) : null}
} @@ -75,8 +82,8 @@ export default class PermissionPageContainerContent extends PureComponent { getTitle() { const { subjectMetadata, - selectedIdentities, - allIdentitiesSelected, + selectedAccounts, + allAccountsSelected, selectedPermissions, } = this.props; const { t } = this.context; @@ -85,18 +92,18 @@ export default class PermissionPageContainerContent extends PureComponent { return t('externalExtension', [subjectMetadata.extensionId]); } else if (!selectedPermissions.eth_accounts) { return t('permissionRequestCapitalized'); - } else if (allIdentitiesSelected) { + } else if (allAccountsSelected) { return t('connectToAll', [ this.renderAccountTooltip(t('connectToAllAccounts')), ]); - } else if (selectedIdentities.length > 1) { + } else if (selectedAccounts.length > 1) { return t('connectToMultiple', [ this.renderAccountTooltip( - t('connectToMultipleNumberOfAccounts', [selectedIdentities.length]), + t('connectToMultipleNumberOfAccounts', [selectedAccounts.length]), ), ]); } - return t('connectTo', [selectedIdentities[0]?.addressLabel]); + return t('connectTo', [selectedAccounts[0]?.addressLabel]); } getHeaderText() { diff --git a/ui/components/app/permission-page-container/permission-page-container.component.js b/ui/components/app/permission-page-container/permission-page-container.component.js index 1f817edf893e..43f61ba056fa 100644 --- a/ui/components/app/permission-page-container/permission-page-container.component.js +++ b/ui/components/app/permission-page-container/permission-page-container.component.js @@ -22,8 +22,15 @@ export default class PermissionPageContainer extends Component { static propTypes = { approvePermissionsRequest: PropTypes.func.isRequired, rejectPermissionsRequest: PropTypes.func.isRequired, - selectedIdentities: PropTypes.array, - allIdentitiesSelected: PropTypes.bool, + selectedAccounts: PropTypes.arrayOf( + PropTypes.shape({ + address: PropTypes.string.isRequired, + addressLabel: PropTypes.string.isRequired, + balance: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + }), + ), + allAccountsSelected: PropTypes.bool, ///: BEGIN:ONLY_INCLUDE_IN(snaps) currentPermissions: PropTypes.object, snapsInstallPrivacyWarningShown: PropTypes.bool.isRequired, @@ -43,8 +50,8 @@ export default class PermissionPageContainer extends Component { static defaultProps = { request: {}, requestMetadata: {}, - selectedIdentities: [], - allIdentitiesSelected: false, + selectedAccounts: [], + allAccountsSelected: false, ///: BEGIN:ONLY_INCLUDE_IN(snaps) currentPermissions: {}, ///: END:ONLY_INCLUDE_IN @@ -144,14 +151,14 @@ export default class PermissionPageContainer extends Component { request: _request, approvePermissionsRequest, rejectPermissionsRequest, - selectedIdentities, + selectedAccounts, } = this.props; const request = { ..._request, permissions: { ..._request.permissions }, - approvedAccounts: selectedIdentities.map( - (selectedIdentity) => selectedIdentity.address, + approvedAccounts: selectedAccounts.map( + (selectedAccount) => selectedAccount.address, ), }; @@ -172,8 +179,8 @@ export default class PermissionPageContainer extends Component { const { requestMetadata, targetSubjectMetadata, - selectedIdentities, - allIdentitiesSelected, + selectedAccounts, + allAccountsSelected, } = this.props; ///: BEGIN:ONLY_INCLUDE_IN(snaps) @@ -207,8 +214,8 @@ export default class PermissionPageContainer extends Component { requestMetadata={requestMetadata} subjectMetadata={targetSubjectMetadata} selectedPermissions={this.state.selectedPermissions} - selectedIdentities={selectedIdentities} - allIdentitiesSelected={allIdentitiesSelected} + selectedAccounts={selectedAccounts} + allAccountsSelected={allAccountsSelected} />
{targetSubjectMetadata?.subjectType !== SubjectType.Snap && ( diff --git a/ui/components/app/permission-page-container/permission-page-container.container.js b/ui/components/app/permission-page-container/permission-page-container.container.js index f84a7c254877..7fa311789de3 100644 --- a/ui/components/app/permission-page-container/permission-page-container.container.js +++ b/ui/components/app/permission-page-container/permission-page-container.container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { - getMetaMaskIdentities, + getMetaMaskAccounts, ///: BEGIN:ONLY_INCLUDE_IN(snaps) getPermissions, ///: END:ONLY_INCLUDE_IN @@ -8,20 +8,20 @@ import { import PermissionPageContainer from './permission-page-container.component'; const mapStateToProps = (state, ownProps) => { - const { selectedIdentities } = ownProps; + const { selectedAccounts } = ownProps; ///: BEGIN:ONLY_INCLUDE_IN(snaps) const currentPermissions = getPermissions( state, ownProps.request.metadata?.origin, ); ///: END:ONLY_INCLUDE_IN - const allIdentities = getMetaMaskIdentities(state); - const allIdentitiesSelected = - Object.keys(selectedIdentities).length === - Object.keys(allIdentities).length && selectedIdentities.length > 1; + const allAccounts = getMetaMaskAccounts(state); + const allAccountsSelected = + Object.keys(selectedAccounts).length === Object.keys(allAccounts).length && + selectedAccounts.length > 1; return { - allIdentitiesSelected, + allAccountsSelected, ///: BEGIN:ONLY_INCLUDE_IN(snaps) currentPermissions, ///: END:ONLY_INCLUDE_IN diff --git a/ui/components/app/selected-account/selected-account-component.test.js b/ui/components/app/selected-account/selected-account-component.test.js index 61a6a833d8ed..095334650a1f 100644 --- a/ui/components/app/selected-account/selected-account-component.test.js +++ b/ui/components/app/selected-account/selected-account-component.test.js @@ -11,9 +11,25 @@ import { import { getAccountType } from '../../../selectors'; import SelectedAccount from '.'; -const mockSelectedIdentity = { +const mockSelectedAccount = { address: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc', - name: 'Test Account', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', }; jest.mock('copy-to-clipboard'); @@ -30,11 +46,11 @@ jest.mock('../../../selectors/institutional/selectors', () => { jest.mock('../../../selectors', () => { const mockGetAccountType = jest.fn(() => undefined); - const mockGetSelectedIdentity = jest.fn(() => mockSelectedIdentity); + const mockGetSelectedAccount = jest.fn(() => mockSelectedAccount); return { getAccountType: mockGetAccountType, - getSelectedIdentity: mockGetSelectedIdentity, + getSelectedInternalAccount: mockGetSelectedAccount, }; }); @@ -52,7 +68,7 @@ describe('SelectedAccount Component', () => { ); expect(tooltipTitle).toBeInTheDocument(); - expect(getByText(mockSelectedIdentity.name)).toBeInTheDocument(); + expect(getByText(mockSelectedAccount.metadata.name)).toBeInTheDocument(); expect(getByTestId('selected-account-copy')).toBeInTheDocument(); }); @@ -92,7 +108,7 @@ describe('SelectedAccount Component', () => { it('should display custody labels if they exist', () => { const mockAccountDetails = { - [mockSelectedIdentity.address]: { + [mockSelectedAccount.address]: { labels: [ { key: 'Label 1', value: 'Label 1' }, { key: 'Label 2', value: 'Label 2' }, diff --git a/ui/components/app/selected-account/selected-account.component.js b/ui/components/app/selected-account/selected-account.component.js index 271211f358bb..37fc02840df1 100644 --- a/ui/components/app/selected-account/selected-account.component.js +++ b/ui/components/app/selected-account/selected-account.component.js @@ -24,7 +24,7 @@ class SelectedAccount extends Component { }; static propTypes = { - selectedIdentity: PropTypes.object.isRequired, + selectedAccount: PropTypes.object.isRequired, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) accountType: PropTypes.string, accountDetails: PropTypes.object, @@ -47,7 +47,7 @@ class SelectedAccount extends Component { render() { const { t } = this.context; const { - selectedIdentity, + selectedAccount, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) accountType, accountDetails, @@ -56,7 +56,7 @@ class SelectedAccount extends Component { ///: END:ONLY_INCLUDE_IN } = this.props; - const checksummedAddress = toChecksumHexAddress(selectedIdentity.address); + const checksummedAddress = toChecksumHexAddress(selectedAccount.address); let title = this.state.copied ? t('copiedExclamation') @@ -108,7 +108,7 @@ class SelectedAccount extends Component { }} >
- {selectedIdentity.name} + {selectedAccount.metadata.name}
{ diff --git a/ui/components/app/selected-account/selected-account.container.js b/ui/components/app/selected-account/selected-account.container.js index 40bb2bbf219f..d7f92ed33270 100644 --- a/ui/components/app/selected-account/selected-account.container.js +++ b/ui/components/app/selected-account/selected-account.container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { - getSelectedIdentity, + getSelectedInternalAccount, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) getAccountType, ///: END:ONLY_INCLUDE_IN @@ -16,7 +16,7 @@ import SelectedAccount from './selected-account.component'; const mapStateToProps = (state) => { return { - selectedIdentity: getSelectedIdentity(state), + selectedAccount: getSelectedInternalAccount(state), ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) accountType: getAccountType(state), accountDetails: getCustodyAccountDetails(state), diff --git a/ui/components/app/signature-request-header/signature-request-header.js b/ui/components/app/signature-request-header/signature-request-header.js index 6fb3149dc432..dd9691e5322e 100644 --- a/ui/components/app/signature-request-header/signature-request-header.js +++ b/ui/components/app/signature-request-header/signature-request-header.js @@ -32,6 +32,7 @@ const SignatureRequestHeader = ({ txData }) => { } = txData; const allAccounts = useSelector(accountsWithSendEtherInfoSelector); const fromAccount = getAccountByAddress(allAccounts, from); + const nativeCurrency = useSelector(getNativeCurrency); const currentCurrency = useSelector(getCurrentCurrency); const currentChainId = useSelector(getCurrentChainId); @@ -70,7 +71,7 @@ const SignatureRequestHeader = ({ txData }) => { return ( { diff --git a/ui/components/app/signature-request/signature-request-data/signature-request-data.js b/ui/components/app/signature-request/signature-request-data/signature-request-data.js index e89d67befcf7..a2afc05c7a6e 100644 --- a/ui/components/app/signature-request/signature-request-data/signature-request-data.js +++ b/ui/components/app/signature-request/signature-request-data/signature-request-data.js @@ -3,10 +3,7 @@ import { useSelector } from 'react-redux'; import { isEqual } from 'lodash'; import PropTypes from 'prop-types'; import { NameType } from '@metamask/name-controller'; -import { - getMemoizedMetaMaskIdentities, - getAccountName, -} from '../../../../selectors'; +import { getInternalAccounts, getAccountName } from '../../../../selectors'; import Address from '../../transaction-decoding/components/decoding/address'; import { isValidHexAddress, @@ -24,7 +21,7 @@ import { usePetnamesEnabled } from '../../../../hooks/usePetnamesEnabled'; import Name from '../../name/name'; function SignatureRequestData({ data }) { - const identities = useSelector(getMemoizedMetaMaskIdentities); + const accounts = useSelector(getInternalAccounts); const petnamesEnabled = usePetnamesEnabled(); return ( @@ -79,7 +76,7 @@ function SignatureRequestData({ data }) {
)} diff --git a/ui/components/app/signature-request/signature-request-data/signature-request-data.test.js b/ui/components/app/signature-request/signature-request-data/signature-request-data.test.js index fd90d85db492..33c9ac602c49 100644 --- a/ui/components/app/signature-request/signature-request-data/signature-request-data.test.js +++ b/ui/components/app/signature-request/signature-request-data/signature-request-data.test.js @@ -1,5 +1,6 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import { sanitizeMessage } from '../../../../helpers/utils/util'; import Identicon from '../../../ui/identicon'; @@ -38,15 +39,36 @@ describe('Signature Request Data', () => { type: 'test', chainId: '0x5', }, - identities: { - '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826': { - name: 'Account 1', - address: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', - }, - '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF': { - name: 'Account 2', - address: '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '07c2cfec-36c9-46c4-8115-3836d3ac9047': { + address: '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF', + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, addressBook: { '0x5': { @@ -180,7 +202,7 @@ describe('Signature Request Data', () => { expect(iconImage).toBeDefined(); }); - it('should render first account name from wallets array if address exists in identities object', () => { + it('should render first account name from wallets array if address exists in internal account object', () => { const { getByText } = renderWithProvider( , store, @@ -199,7 +221,7 @@ describe('Signature Request Data', () => { expect(iconImage).toBeDefined(); }); - it('should render second account name from wallets array if address exists in identities object', () => { + it('should render second account name from wallets array if address exists in internal account object', () => { const { getByText } = renderWithProvider( , store, @@ -264,7 +286,7 @@ describe('Signature Request Data', () => { expect(iconImage).toBeDefined(); }); - it('should render first shorten address from wallets array if address does not exists in identities and address book objects', () => { + it('should render first shorten address from wallets array if address does not exists in internal account and address book objects', () => { const { getByText } = renderWithProvider( , store, @@ -283,7 +305,7 @@ describe('Signature Request Data', () => { expect(iconImage).toBeDefined(); }); - it('should render second shorten address from wallets array if address does not exists in identities and address book objects', () => { + it('should render second shorten address from wallets array if address does not exists in internal account and address book objects', () => { const { getByText } = renderWithProvider( , store, @@ -302,7 +324,7 @@ describe('Signature Request Data', () => { expect(iconImage).toBeDefined(); }); - it('should render third shorten address from wallets array if address does not exists in identities and address book objects', () => { + it('should render third shorten address from wallets array if address does not exists in internal account and address book objects', () => { const { getByText } = renderWithProvider( , store, diff --git a/ui/components/app/signature-request/signature-request-header/signature-request-header.component.test.js b/ui/components/app/signature-request/signature-request-header/signature-request-header.component.test.js index 69795f28de04..aaf5a0fda7f2 100644 --- a/ui/components/app/signature-request/signature-request-header/signature-request-header.component.test.js +++ b/ui/components/app/signature-request/signature-request-header/signature-request-header.component.test.js @@ -7,7 +7,11 @@ import SignatureRequestHeader from './signature-request-header.component'; describe('SignatureRequestHeader', () => { const store = configureMockStore()(mockState); it('renders correctly with fromAccount', () => { - const fromAccount = { address: '0x' }; + const fromAccount = { + address: '0x', + metadata: { name: '' }, + balance: '0x', + }; const { container } = renderWithProvider( , diff --git a/ui/components/app/signature-request/signature-request.js b/ui/components/app/signature-request/signature-request.js index d7146a304a0e..daf399cbe128 100644 --- a/ui/components/app/signature-request/signature-request.js +++ b/ui/components/app/signature-request/signature-request.js @@ -23,8 +23,8 @@ import { getTotalUnapprovedMessagesCount, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) accountsWithSendEtherInfoSelector, - getSelectedAccount, getAccountType, + getSelectedInternalAccountWithBalance, ///: END:ONLY_INCLUDE_IN } from '../../../selectors'; import { @@ -124,7 +124,7 @@ const SignatureRequest = ({ txData }) => { ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) // Used to show a warning if the signing account is not the selected account // Largely relevant for contract wallet custodians - const selectedAccount = useSelector(getSelectedAccount); + const selectedAccount = useSelector(getSelectedInternalAccountWithBalance); const mmiActions = mmiActionsFactory(); const accountType = useSelector(getAccountType); const isNotification = getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION; diff --git a/ui/components/app/signature-request/signature-request.test.js b/ui/components/app/signature-request/signature-request.test.js index 47ae0b7f2b15..fa3e72026bf8 100644 --- a/ui/components/app/signature-request/signature-request.test.js +++ b/ui/components/app/signature-request/signature-request.test.js @@ -2,6 +2,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { fireEvent } from '@testing-library/react'; import configureMockStore from 'redux-mock-store'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { showCustodianDeepLink } from '@metamask-institutional/extension'; import mockState from '../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; @@ -15,10 +16,11 @@ import { conversionRateSelector, getCurrentCurrency, getMemoizedAddressBook, - getMemoizedMetaMaskIdentities, getPreferences, getSelectedAccount, + getSelectedInternalAccountWithBalance, getTotalUnapprovedMessagesCount, + getInternalAccounts, unconfirmedTransactionsHashSelector, getAccountType, } from '../../../selectors'; @@ -51,7 +53,24 @@ const mockStore = { name: 'John Doe', }, }, - selectedAddress: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'John Doe', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, nativeCurrency: 'ETH', currentCurrency: 'usd', conversionRate: null, @@ -93,15 +112,41 @@ const generateUseSelectorRouter = (opts) => (selector) => { case conversionRateSelector: return opts.metamask.conversionRate; case getSelectedAccount: - return opts.metamask.accounts[opts.metamask.selectedAddress]; + return opts.metamask.accounts[ + opts.metamask.internalAccounts.accounts[ + opts.metamask.internalAccounts.selectedAccount + ].address + ]; + case getInternalAccounts: + return Object.values(opts.metamask.internalAccounts.accounts); + case getSelectedInternalAccountWithBalance: + return { + ...opts.metamask.internalAccounts.accounts[ + opts.metamask.internalAccounts.selectedAccount + ], + balance: + opts.metamask.accounts[ + opts.metamask.internalAccounts.accounts[ + opts.metamask.internalAccounts.selectedAccount + ].address + ]?.balance ?? 0, + }; case getMemoizedAddressBook: return []; case accountsWithSendEtherInfoSelector: - return Object.values(opts.metamask.accounts); + return Object.values(opts.metamask.internalAccounts.accounts).map( + (internalAccount) => { + return { + ...internalAccount, + ...(opts.metamask.accounts[internalAccount.address] ?? {}), + balance: + opts.metamask.accounts[internalAccount.address]?.balance ?? 0, + }; + }, + ); case getAccountType: return 'custody'; case unconfirmedTransactionsHashSelector: - case getMemoizedMetaMaskIdentities: return {}; default: return undefined; @@ -431,7 +476,6 @@ describe('Signature Request Component', () => { ...mockStore, metamask: { ...mockStore.metamask, - selectedAddress: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', accounts: { ...mockStore.metamask.accounts, '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { @@ -439,6 +483,42 @@ describe('Signature Request Component', () => { balance: '0x0', name: 'Account 1', }, + '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5': { + address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + balance: '0x0', + name: 'Account 2', + }, + }, + internalAccounts: { + accounts: { + 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1', }, }, }), diff --git a/ui/components/app/snaps/keyring-snap-removal-result/index.ts b/ui/components/app/snaps/keyring-snap-removal-result/index.ts new file mode 100644 index 000000000000..333d7a5c3d83 --- /dev/null +++ b/ui/components/app/snaps/keyring-snap-removal-result/index.ts @@ -0,0 +1 @@ +export { default } from './keyring-snap-removal-result'; diff --git a/ui/components/app/snaps/keyring-snap-removal-result/keyring-snap-removal-result.test.tsx b/ui/components/app/snaps/keyring-snap-removal-result/keyring-snap-removal-result.test.tsx new file mode 100644 index 000000000000..337657636a58 --- /dev/null +++ b/ui/components/app/snaps/keyring-snap-removal-result/keyring-snap-removal-result.test.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import mockStore from '../../../../../test/data/mock-state.json'; +import { + fireEvent, + renderWithProvider, + waitFor, +} from '../../../../../test/jest'; +import KeyringSnapRemovalResult from './keyring-snap-removal-result'; + +const mockOnClose = jest.fn(); + +const defaultArgs = { + isOpen: true, + snapName: 'mock-snap', + result: 'success', + onClose: mockOnClose, +}; + +describe('Keyring Snap Remove Result', () => { + let store; + beforeAll(() => { + store = configureMockStore()(mockStore); + }); + it('show render the success message', async () => { + const { getByText, getByLabelText } = renderWithProvider( + , + store, + ); + + expect(getByText('mock-snap removed')).toBeInTheDocument(); + + const closeButton = getByLabelText('Close'); + + fireEvent.click(closeButton); + + await waitFor(() => { + expect(mockOnClose).toHaveBeenCalled(); + }); + }); + it('show render the failure message', () => { + const { getByText } = renderWithProvider( + , + store, + ); + expect(getByText('mock-snap not removed')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/snaps/keyring-snap-removal-result/keyring-snap-removal-result.tsx b/ui/components/app/snaps/keyring-snap-removal-result/keyring-snap-removal-result.tsx new file mode 100644 index 000000000000..c090a2e58a24 --- /dev/null +++ b/ui/components/app/snaps/keyring-snap-removal-result/keyring-snap-removal-result.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { + FlexDirection, + AlignItems, + Display, + JustifyContent, + IconColor, + TextVariant, +} from '../../../../helpers/constants/design-system'; +import { + Box, + Icon, + IconName, + IconSize, + Modal, + ModalContent, + ModalHeader, + ModalOverlay, + Text, +} from '../../../component-library'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; + +const KeyringSnapRemovalResult = ({ + snapName, + result, + isOpen, + onClose, +}: { + snapName: string; + result: string; + isOpen: boolean; + onClose: () => void; +}) => { + const t = useI18nContext(); + + return ( + <> + onClose()}> + + + {''} + + + + {t('keyringSnapRemovalResult1', [ + snapName, + result === 'failed' + ? t('keyringSnapRemovalResultNotSuccessful') + : '', + ])} + + + + + + ); +}; + +export default KeyringSnapRemovalResult; diff --git a/ui/components/app/snaps/keyring-snap-removal-warning/index.ts b/ui/components/app/snaps/keyring-snap-removal-warning/index.ts new file mode 100644 index 000000000000..85d2c02bb062 --- /dev/null +++ b/ui/components/app/snaps/keyring-snap-removal-warning/index.ts @@ -0,0 +1 @@ +export { default } from './keyring-snap-removal-warning'; diff --git a/ui/components/app/snaps/keyring-snap-removal-warning/keyring-account-list-item.tsx b/ui/components/app/snaps/keyring-snap-removal-warning/keyring-account-list-item.tsx new file mode 100644 index 000000000000..1bf55633b4d1 --- /dev/null +++ b/ui/components/app/snaps/keyring-snap-removal-warning/keyring-account-list-item.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { InternalAccount } from '@metamask/keyring-api'; +import { ButtonIcon, IconName, Text, Box } from '../../../component-library'; +import { + BlockSize, + BorderColor, + BorderRadius, + Display, + FlexDirection, + IconColor, + JustifyContent, + OverflowWrap, + TextColor, +} from '../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; + +export const KeyringAccountListItem = ({ + account, + snapUrl, +}: { + account: InternalAccount; + snapUrl: string; +}) => { + const t = useI18nContext(); + return ( + + + + {t('keyringAccountName')} + {account.metadata.name} + + + + {t('keyringAccountPublicAddress')} + + {account.address} + + + + { + global.platform.openTab({ + url: snapUrl, + }); + }} + /> + + + ); +}; diff --git a/ui/components/app/snaps/keyring-snap-removal-warning/keyring-snap-removal-warning.stories.js b/ui/components/app/snaps/keyring-snap-removal-warning/keyring-snap-removal-warning.stories.js new file mode 100644 index 000000000000..9dc8a09b4ba1 --- /dev/null +++ b/ui/components/app/snaps/keyring-snap-removal-warning/keyring-snap-removal-warning.stories.js @@ -0,0 +1,58 @@ +import React from 'react'; +import KeyringSnapRemovalWarning from './keyring-snap-removal-warning'; + +export default { + title: 'Components/App/Snaps/KeyringSnapRemovalWarning', + component: KeyringSnapRemovalWarning, + argTypes: { + onCancel: { + action: 'onCancel', + }, + onSubmit: { + action: 'onSubmit', + }, + snapName: { + control: 'text', + }, + snapUrl: { + control: 'text', + }, + isOpen: { + control: 'boolean', + }, + keyringAccounts: { + control: 'array', + }, + }, + args: { + snapName: 'ABC Snap', + snapUrl: 'mock-url', + isOpen: true, + keyringAccounts: [ + { + address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b', + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Test Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + type: 'eip155:eoa', + }, + ], + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/snaps/keyring-snap-removal-warning/keyring-snap-removal-warning.test.tsx b/ui/components/app/snaps/keyring-snap-removal-warning/keyring-snap-removal-warning.test.tsx new file mode 100644 index 000000000000..42e0ada6d7ca --- /dev/null +++ b/ui/components/app/snaps/keyring-snap-removal-warning/keyring-snap-removal-warning.test.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import { waitFor, fireEvent } from '@testing-library/react'; +import configureMockStore from 'redux-mock-store'; +import { Snap } from '@metamask/snaps-utils'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; +import mockStore from '../../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../../test/jest'; +import messages from '../../../../../app/_locales/en/messages.json'; +import KeyringSnapRemovalWarning from './keyring-snap-removal-warning'; + +const mockOnClose = jest.fn(); +const mockOnCancel = jest.fn(); +const mockOnBack = jest.fn(); +const mockOnSubmit = jest.fn(); + +const mockSnap = { + id: 'mock-snap-id', + manifest: { + proposedName: 'mock-snap', + }, +} as Snap; + +const defaultArgs = { + isOpen: true, + snap: mockSnap, + onClose: mockOnClose, + onCancel: mockOnCancel, + onBack: mockOnBack, + onSubmit: mockOnSubmit, + keyringAccounts: [ + { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + { + address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Test Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + ], +}; + +describe('Keyring Snap Remove Warning', () => { + let store; + beforeAll(() => { + store = configureMockStore()(mockStore); + }); + it('show render the keyring snap warning and content', () => { + const { getByText } = renderWithProvider( + , + store, + ); + expect( + getByText(messages.backupKeyringSnapReminder.message), + ).toBeInTheDocument(); + expect(getByText(messages.removeKeyringSnap.message)).toBeInTheDocument(); + + for (const account of defaultArgs.keyringAccounts) { + expect(getByText(account.metadata.name)).toBeInTheDocument(); + expect(getByText(account.address)).toBeInTheDocument(); + } + }); + + it('displays the keyring snap confirmation removal modal', async () => { + const { getByText, getByTestId, getAllByText } = renderWithProvider( + , + store, + ); + + const nextButton = getByText('Continue'); + + fireEvent.click(nextButton); + + await waitFor(() => { + // translation is broken into three pieces + expect(getByText(/Type/u)).toBeInTheDocument(); + expect( + getByText(mockSnap.manifest?.proposedName as string), + ).toBeInTheDocument(); + expect( + getByText(/to confirm you want to remove this snap:/u), + ).toBeInTheDocument(); + }); + + const confirmationInput = getByTestId('remove-snap-confirmation-input'); + + fireEvent.change(confirmationInput, { + target: { value: mockSnap.manifest?.proposedName }, + }); + + await waitFor(() => { + const removeSnapButton = getAllByText('Remove Snap')[1]; + expect(removeSnapButton).not.toBeDisabled(); + fireEvent.click(removeSnapButton); + expect(mockOnSubmit).toBeCalled(); + }); + }); + + it('opens block explorer for account', async () => { + global.platform = { openTab: jest.fn() }; + const { getByText, getAllByTestId } = renderWithProvider( + , + store, + ); + + const getAccountsToBeRemoved = getAllByTestId('keyring-account-list-item'); + expect(getAccountsToBeRemoved.length).toBe(2); + + expect( + getByText(defaultArgs.keyringAccounts[0].metadata.name), + ).toBeInTheDocument(); + expect( + getByText(defaultArgs.keyringAccounts[1].metadata.name), + ).toBeInTheDocument(); + + const accountLink = getAllByTestId('keyring-account-link'); + + fireEvent.click(accountLink[0]); + + await waitFor(() => { + expect(global.platform.openTab).toHaveBeenCalled(); + }); + }); +}); diff --git a/ui/components/app/snaps/keyring-snap-removal-warning/keyring-snap-removal-warning.tsx b/ui/components/app/snaps/keyring-snap-removal-warning/keyring-snap-removal-warning.tsx new file mode 100644 index 000000000000..280b6e5181c6 --- /dev/null +++ b/ui/components/app/snaps/keyring-snap-removal-warning/keyring-snap-removal-warning.tsx @@ -0,0 +1,190 @@ +import React, { useState } from 'react'; +import { InternalAccount } from '@metamask/keyring-api'; +import { getAccountLink } from '@metamask/etherscan-link'; +import { Snap } from '@metamask/snaps-utils'; +import { useSelector } from 'react-redux'; +import { + BannerAlert, + BannerAlertSeverity, + Box, + Button, + ButtonSize, + ButtonVariant, + Modal, + ModalContent, + ModalHeader, + ModalOverlay, + Text, + TextField, +} from '../../../component-library'; +import { + BlockSize, + Display, + FlexDirection, + FontWeight, + JustifyContent, +} from '../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import InfoTooltip from '../../../ui/info-tooltip'; +import { getProviderConfig } from '../../../../ducks/metamask/metamask'; +import { KeyringAccountListItem } from './keyring-account-list-item'; + +export default function KeyringRemovalSnapWarning({ + snap, + keyringAccounts, + onCancel, + onClose, + onBack, + onSubmit, + isOpen, +}: { + snap: Snap; + keyringAccounts: InternalAccount[]; + onCancel: () => void; + onClose: () => void; + onBack: () => void; + onSubmit: () => void; + isOpen: boolean; +}) { + const t = useI18nContext(); + const [showConfirmation, setShowConfirmation] = useState(false); + const [confirmedRemoval, setConfirmedRemoval] = useState(false); + const [confirmationInput, setConfirmationInput] = useState(''); + const [error, setError] = useState(false); + const { chainId } = useSelector(getProviderConfig); + + const validateConfirmationInput = (input: string): boolean => { + setError(false); + if (input === snap.manifest.proposedName) { + return true; + } + setError(true); + return false; + }; + + return ( + <> + + + + { + setShowConfirmation(false); + onBack(); + }} + onClose={() => { + setShowConfirmation(false); + onClose(); + }} + > + {t('removeSnap')} + + {showConfirmation === false ? ( + <> + + {t('backupKeyringSnapReminder')} + + + {t('removeKeyringSnap')} + + + {keyringAccounts.map((account, index) => { + return ( + + ); + })} + + ) : ( + <> + + + {t('backupKeyringSnapReminder')} + + + {t('keyringSnapRemoveConfirmation', [ + + {snap.manifest.proposedName} + , + ])} + + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore TODO: fix TextField props */} + { + setConfirmationInput(e.target.value); + setConfirmedRemoval( + validateConfirmationInput(e.target.value), + ); + }} + error={error} + inputProps={{ + 'data-testid': 'remove-snap-confirmation-input', + }} + type="text" + /> + + + )} + + + + + + + + ); +} diff --git a/ui/components/app/snaps/snap-removal-result/index.ts b/ui/components/app/snaps/snap-removal-result/index.ts new file mode 100644 index 000000000000..81046071afa6 --- /dev/null +++ b/ui/components/app/snaps/snap-removal-result/index.ts @@ -0,0 +1 @@ +export { default } from './snap-removal-result'; diff --git a/ui/components/app/snaps/snap-removal-result/snap-removal-result.tsx b/ui/components/app/snaps/snap-removal-result/snap-removal-result.tsx new file mode 100644 index 000000000000..dcd39a862881 --- /dev/null +++ b/ui/components/app/snaps/snap-removal-result/snap-removal-result.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { + Box, + Icon, + IconName, + Modal, + ModalContent, + ModalHeader, + ModalOverlay, + Text, +} from '../../../component-library'; +import { + AlignItems, + Display, + FlexDirection, + JustifyContent, +} from '../../../../helpers/constants/design-system'; + +export default function SnapRemovalResult({ + success, + snapName, + isOpen, + onClose, +}: { + success: boolean; + snapName: string; + isOpen: boolean; + onClose: () => void; +}) { + return ( + + + + + + + {`${snapName} ${success ? 'removed' : 'not removed'}`} + + + + ); +} diff --git a/ui/components/app/snaps/snap-remove-warning/snap-remove-warning.js b/ui/components/app/snaps/snap-remove-warning/snap-remove-warning.js index e7c342faee77..20ae959b3f5a 100644 --- a/ui/components/app/snaps/snap-remove-warning/snap-remove-warning.js +++ b/ui/components/app/snaps/snap-remove-warning/snap-remove-warning.js @@ -9,8 +9,8 @@ import { ModalContent, ModalHeader, ModalOverlay, - BUTTON_VARIANT, - BUTTON_SIZES, + ButtonSize, + ButtonVariant, } from '../../../component-library'; import { @@ -26,6 +26,7 @@ export default function SnapRemoveWarning({ snapName, }) { const t = useI18nContext(); + return ( @@ -41,15 +42,15 @@ export default function SnapRemoveWarning({
); -ChaosDataItem.args = { identity: CHAOS_IDENTITY }; +ChaosDataItem.args = { account: CHAOS_ACCOUNT }; export const ConnectedSiteItem = (args) => (
@@ -121,7 +121,7 @@ export const ConnectedSiteChaosItem = (args) => (
); ConnectedSiteChaosItem.args = { - identity: CHAOS_IDENTITY, + account: CHAOS_ACCOUNT, connectedAvatar: 'https://uniswap.org/favicon.ico', connectedAvatarName: 'Uniswap', }; diff --git a/ui/components/multichain/account-list-item/account-list-item.test.js b/ui/components/multichain/account-list-item/account-list-item.test.js index 354fdac0c2c1..930f045072e8 100644 --- a/ui/components/multichain/account-list-item/account-list-item.test.js +++ b/ui/components/multichain/account-list-item/account-list-item.test.js @@ -2,21 +2,22 @@ import React from 'react'; import { screen, fireEvent } from '@testing-library/react'; import { toChecksumHexAddress } from '@metamask/controller-utils'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { renderWithProvider } from '../../../../test/jest'; import configureStore from '../../../store/store'; import mockState from '../../../../test/data/mock-state.json'; import { shortenAddress } from '../../../helpers/utils/util'; import { AccountListItem } from '.'; -const identity = { - ...mockState.metamask.identities[ - '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' +const account = { + ...mockState.metamask.internalAccounts.accounts[ + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3' ], balance: '0x152387ad22c3f0', }; const DEFAULT_PROPS = { - identity, + account, onClick: jest.fn(), }; @@ -33,9 +34,9 @@ const render = (props = {}) => { describe('AccountListItem', () => { it('renders AccountListItem component and shows account name, address, and balance', () => { const { container } = render(); - expect(screen.getByText(identity.name)).toBeInTheDocument(); + expect(screen.getByText(account.metadata.name)).toBeInTheDocument(); expect( - screen.getByText(shortenAddress(toChecksumHexAddress(identity.address))), + screen.getByText(shortenAddress(toChecksumHexAddress(account.address))), ).toBeInTheDocument(); expect(document.querySelector('[title="0.006 ETH"]')).toBeInTheDocument(); @@ -52,9 +53,12 @@ describe('AccountListItem', () => { it('renders the account name tooltip for long names', () => { render({ selected: true, - identity: { - ...identity, - name: 'This is a super long name that requires tooltip', + account: { + ...account, + metadata: { + ...account.metadata, + name: 'This is a super long name that requires tooltip', + }, }, }); expect( @@ -104,16 +108,29 @@ describe('AccountListItem', () => { expect(getByAltText(`${connectedAvatarName} logo`)).toBeInTheDocument(); }); - ///: BEGIN:ONLY_INCLUDE_IN(keyring-snaps) it('renders the snap label for snap accounts', () => { const { getByText } = render({ - identity: { - address: '0xb552685e3d2790eFd64a175B00D51F02cdaFEe5D', - name: 'Snap Account', + account: { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Snap Account', + keyring: { + type: 'Snap Keyring', + }, + snap: { + name: 'Metamask Simple Snap', + id: 'metamask-simple-snap', + enabled: true, + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + balance: '0x0', }, }); - expect(getByText('Snaps')).toBeInTheDocument(); + expect(getByText('Metamask Simple Snap')).toBeInTheDocument(); }); - ///: END:ONLY_INCLUDE_IN }); diff --git a/ui/components/multichain/account-list-menu/account-list-menu.js b/ui/components/multichain/account-list-menu/account-list-menu.js index a51ba2e0d612..0e89aee9e9c9 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.js +++ b/ui/components/multichain/account-list-menu/account-list-menu.js @@ -25,15 +25,18 @@ import { import { useI18nContext } from '../../../hooks/useI18nContext'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { - getSelectedAccount, - getMetaMaskAccountsOrdered, getConnectedSubjectsForAllAddresses, getOriginOfCurrentTab, + getMetaMaskAccountsOrdered, + getSelectedInternalAccount, ///: BEGIN:ONLY_INCLUDE_IN(keyring-snaps) getIsAddSnapAccountEnabled, ///: END:ONLY_INCLUDE_IN } from '../../../selectors'; -import { toggleAccountMenu, setSelectedAccount } from '../../../store/actions'; +import { + toggleAccountMenu, + setSelectedInternalAccount, +} from '../../../store/actions'; import { MetaMetricsEventAccountType, MetaMetricsEventCategory, @@ -54,8 +57,10 @@ import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; export const AccountListMenu = ({ onClose }) => { const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); + const accounts = useSelector(getMetaMaskAccountsOrdered); - const selectedAccount = useSelector(getSelectedAccount); + const selectedAccount = useSelector(getSelectedInternalAccount); + const connectedSites = useSelector(getConnectedSubjectsForAllAddresses); const currentTabOrigin = useSelector(getOriginOfCurrentTab); const history = useHistory(); @@ -75,7 +80,7 @@ export const AccountListMenu = ({ onClose }) => { distance: 100, maxPatternLength: 32, minMatchCharLength: 1, - keys: ['name', 'address'], + keys: ['metadata.name', 'address', 'id'], }); fuse.setCollection(accounts); searchResults = fuse.search(searchQuery); @@ -190,11 +195,11 @@ export const AccountListMenu = ({ onClose }) => { location: 'Main Menu', }, }); - dispatch(setSelectedAccount(account.address)); + dispatch(setSelectedInternalAccount(account.id)); }} - identity={account} + account={account} key={account.address} - selected={selectedAccount.address === account.address} + selected={selectedAccount.id === account.id} closeMenu={onClose} connectedAvatar={connectedSite?.iconUrl} connectedAvatarName={connectedSite?.name} diff --git a/ui/components/multichain/account-list-menu/account-list-menu.test.js b/ui/components/multichain/account-list-menu/account-list-menu.test.js index 28d2af13f699..6046dc9929b6 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.test.js +++ b/ui/components/multichain/account-list-menu/account-list-menu.test.js @@ -1,6 +1,7 @@ /* eslint-disable jest/require-top-level-describe */ import React from 'react'; import reactRouterDom from 'react-router-dom'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { fireEvent, renderWithProvider, waitFor } from '../../../../test/jest'; import configureStore from '../../../store/store'; import mockState from '../../../../test/data/mock-state.json'; @@ -134,14 +135,27 @@ describe('AccountListMenu', () => { }, metamask: { ...mockState.metamask, - accounts: { - '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { - balance: '0x346ba7725f412cbfdb', - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + internalAccounts: { + ...mockState.metamask.internalAccounts, + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, }, }, }, }); + const props = { onClose: () => jest.fn() }; const { container } = renderWithProvider( , diff --git a/ui/components/multichain/app-header/app-header.js b/ui/components/multichain/app-header/app-header.js index 4a1840da8fc2..0196cf513df3 100644 --- a/ui/components/multichain/app-header/app-header.js +++ b/ui/components/multichain/app-header/app-header.js @@ -41,11 +41,10 @@ import { getCurrentNetwork, getOnboardedInThisUISession, getOriginOfCurrentTab, - getSelectedIdentity, getShowProductTour, getTestNetworkBackgroundColor, + getSelectedInternalAccount, getUnapprovedTransactions, - getSelectedAddress, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) getTheme, ///: END:ONLY_INCLUDE_IN @@ -80,27 +79,27 @@ export const AppHeader = ({ location }) => { const t = useI18nContext(); const chainId = useSelector(getCurrentChainId); + // Used for account picker + const internalAccount = useSelector(getSelectedInternalAccount); + const dispatch = useDispatch(); + const completedOnboarding = useSelector(getCompletedOnboarding); + const onboardedInThisUISession = useSelector(getOnboardedInThisUISession); + const showProductTourPopup = useSelector(getShowProductTour); + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - const selectedAddress = useSelector(getSelectedAddress); + const selectedAddress = internalAccount?.address; const custodianIcon = useSelector((state) => getCustodianIconForAddress(state, selectedAddress), ); const theme = useSelector((state) => getTheme(state)); ///: END:ONLY_INCLUDE_IN - // Used for account picker - const identity = useSelector(getSelectedIdentity); - const dispatch = useDispatch(); - const completedOnboarding = useSelector(getCompletedOnboarding); - const onboardedInThisUISession = useSelector(getOnboardedInThisUISession); - const showProductTourPopup = useSelector(getShowProductTour); - // Used for network icon / dropdown const currentNetwork = useSelector(getCurrentNetwork); const testNetworkBackgroundColor = useSelector(getTestNetworkBackgroundColor); // Used for copy button - const currentAddress = useSelector(getSelectedAddress); + const currentAddress = internalAccount?.address; const checksummedCurrentAddress = toChecksumHexAddress(currentAddress); const [copied, handleCopy] = useCopyToClipboard(MINUTE); @@ -281,10 +280,10 @@ export const AppHeader = ({ location }) => { /> ) : null} - {identity ? ( + {internalAccount ? ( { dispatch(toggleAccountMenu()); diff --git a/ui/components/multichain/connected-site-menu/connected-site-menu.js b/ui/components/multichain/connected-site-menu/connected-site-menu.js index 16d7a345d9ee..8ed76f556c36 100644 --- a/ui/components/multichain/connected-site-menu/connected-site-menu.js +++ b/ui/components/multichain/connected-site-menu/connected-site-menu.js @@ -22,7 +22,7 @@ import { IconSize, Box, } from '../../component-library'; -import { getSelectedIdentity } from '../../../selectors'; +import { getSelectedInternalAccount } from '../../../selectors'; import Tooltip from '../../ui/tooltip'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -34,7 +34,7 @@ export const ConnectedSiteMenu = ({ onClick, }) => { const t = useI18nContext(); - const selectedAccount = useSelector(getSelectedIdentity); + const selectedAccount = useSelector(getSelectedInternalAccount); return ( { - const selectedAddress = '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b'; - - const identities = { - '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - name: 'Account 1', - }, - '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b': { - address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b', - name: 'Account 2', + const internalAccounts = { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '07c2cfec-36c9-46c4-8115-3836d3ac9047': { + address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b', + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, }, + selectedAccount: '07c2cfec-36c9-46c4-8115-3836d3ac9047', }; const accounts = { @@ -38,8 +58,7 @@ describe('Connected Site Menu', () => { }; const mockStore = { metamask: { - selectedAddress, - identities, + internalAccounts, accounts, }, }; diff --git a/ui/components/multichain/create-account/create-account.js b/ui/components/multichain/create-account/create-account.js index 9cab2737df5e..989ebaf7c76c 100644 --- a/ui/components/multichain/create-account/create-account.js +++ b/ui/components/multichain/create-account/create-account.js @@ -10,10 +10,7 @@ import { } from '../../component-library'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getAccountNameErrorMessage } from '../../../helpers/utils/accounts'; -import { - getMetaMaskAccountsOrdered, - getMetaMaskIdentities, -} from '../../../selectors'; +import { getMetaMaskAccountsOrdered } from '../../../selectors'; import { addNewAccount, setAccountLabel } from '../../../store/actions'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { @@ -32,10 +29,9 @@ export const CreateAccount = ({ onActionComplete }) => { const trackEvent = useContext(MetaMetricsContext); const accounts = useSelector(getMetaMaskAccountsOrdered); - const identities = useSelector(getMetaMaskIdentities); const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage); - const newAccountNumber = Object.keys(identities).length + 1; + const newAccountNumber = Object.keys(accounts).length + 1; const defaultAccountName = t('newAccountNumberName', [newAccountNumber]); const [newAccountName, setNewAccountName] = useState(''); @@ -49,9 +45,9 @@ export const CreateAccount = ({ onActionComplete }) => { ); const onCreateAccount = async (name) => { - const newAccountAddress = await dispatch(addNewAccount()); + const newAccount = await dispatch(addNewAccount(name)); if (name) { - dispatch(setAccountLabel(newAccountAddress, name)); + dispatch(setAccountLabel(newAccount.id, name)); } }; diff --git a/ui/components/multichain/create-account/create-account.test.js b/ui/components/multichain/create-account/create-account.test.js index f93f013afc08..84a6a5e94d8a 100644 --- a/ui/components/multichain/create-account/create-account.test.js +++ b/ui/components/multichain/create-account/create-account.test.js @@ -1,5 +1,6 @@ /* eslint-disable jest/require-top-level-describe */ import React from 'react'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { fireEvent, renderWithProvider, waitFor } from '../../../../test/jest'; import configureStore from '../../../store/store'; import mockState from '../../../../test/data/mock-state.json'; @@ -10,7 +11,21 @@ const render = (props = { onActionComplete: () => jest.fn() }) => { return renderWithProvider(, store); }; -const mockAddNewAccount = jest.fn().mockReturnValue({ type: 'TYPE' }); +const mockInternalAccount = { + id: '0179ecc1-19c2-4c78-8df9-e08b604665e9', + address: '0x1', + metadata: { + name: 'test', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, +}; + +const mockAddNewAccount = jest.fn().mockReturnValue(mockInternalAccount); const mockSetAccountLabel = jest.fn().mockReturnValue({ type: 'TYPE' }); jest.mock('../../../store/actions', () => ({ @@ -44,7 +59,7 @@ describe('CreateAccount', () => { await waitFor(() => expect(mockAddNewAccount).toHaveBeenCalled()); await waitFor(() => expect(mockSetAccountLabel).toHaveBeenCalledWith( - { type: 'TYPE' }, + mockInternalAccount.id, newAccountName, ), ); diff --git a/ui/components/multichain/global-menu/global-menu.js b/ui/components/multichain/global-menu/global-menu.js index 0e7fccc76675..94b6eb881db6 100644 --- a/ui/components/multichain/global-menu/global-menu.js +++ b/ui/components/multichain/global-menu/global-menu.js @@ -43,7 +43,7 @@ import { ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) getMetaMetricsId, ///: END:ONLY_INCLUDE_IN(build-mmi) - getSelectedAddress, + getSelectedInternalAccount, getUnapprovedTransactions, ///: BEGIN:ONLY_INCLUDE_IN(snaps) getNotifySnaps, @@ -70,7 +70,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { const dispatch = useDispatch(); const trackEvent = useContext(MetaMetricsContext); const history = useHistory(); - const address = useSelector(getSelectedAddress); + const account = useSelector(getSelectedInternalAccount); const unapprovedTransactons = useSelector(getUnapprovedTransactions); ///: BEGIN:ONLY_INCLUDE_IN(snaps) @@ -102,12 +102,12 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { { const loadingMessage = getLoadingMessage(strategy); try { - const { selectedAddress } = await dispatch( + const updatedState = await dispatch( actions.importNewAccount(strategy, importArgs, loadingMessage), ); + const { internalAccounts } = updatedState; + const { address: selectedAddress } = + internalAccounts.accounts[internalAccounts.selectedAccount]; if (selectedAddress) { trackImportEvent(strategy, true); dispatch(actions.hideWarning()); diff --git a/ui/components/multichain/import-nfts-modal/import-nfts-modal.js b/ui/components/multichain/import-nfts-modal/import-nfts-modal.js index 51c7eb89ff03..886633bdcb20 100644 --- a/ui/components/multichain/import-nfts-modal/import-nfts-modal.js +++ b/ui/components/multichain/import-nfts-modal/import-nfts-modal.js @@ -24,7 +24,7 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import { getCurrentChainId, getIsMainnet, - getSelectedAddress, + getSelectedInternalAccount, getUseNftDetection, } from '../../../selectors'; import { @@ -60,7 +60,7 @@ export const ImportNftsModal = ({ onClose }) => { const useNftDetection = useSelector(getUseNftDetection); const isMainnet = useSelector(getIsMainnet); const nftsDropdownState = useSelector(getNftsDropdownState); - const selectedAddress = useSelector(getSelectedAddress); + const selectedAccount = useSelector(getSelectedInternalAccount); const chainId = useSelector(getCurrentChainId); const { tokenAddress: initialTokenAddress, @@ -79,10 +79,10 @@ export const ImportNftsModal = ({ onClose }) => { await dispatch(addNftVerifyOwnership(nftAddress, tokenId)); const newNftDropdownState = { ...nftsDropdownState, - [selectedAddress]: { - ...nftsDropdownState?.[selectedAddress], + [selectedAccount.address]: { + ...nftsDropdownState?.[selectedAccount.address], [chainId]: { - ...nftsDropdownState?.[selectedAddress]?.[chainId], + ...nftsDropdownState?.[selectedAccount.address]?.[chainId], [nftAddress]: true, }, }, @@ -202,7 +202,7 @@ export const ImportNftsModal = ({ onClose }) => { { { it('should enable the "Import" button when valid entries are input into both Address and TokenId fields', () => { const { getByText, getByPlaceholderText } = renderWithProvider( - , + , store, ); expect(getByText('Import')).not.toBeEnabled(); @@ -68,7 +68,7 @@ describe('ImportNftsModal', () => { it('should not enable the "Import" button when an invalid entry is input into one or both Address and TokenId fields', () => { const { getByText, getByPlaceholderText } = renderWithProvider( - , + , store, ); expect(getByText('Import')).not.toBeEnabled(); @@ -144,7 +144,7 @@ describe('ImportNftsModal', () => { ); const { getByTestId, getByText, getByPlaceholderText } = renderWithProvider( - , + , store, ); const addressInput = getByPlaceholderText('0x...'); diff --git a/ui/components/multichain/import-tokens-modal/import-tokens-modal.js b/ui/components/multichain/import-tokens-modal/import-tokens-modal.js index 3bc95be4636f..0d62a720a173 100644 --- a/ui/components/multichain/import-tokens-modal/import-tokens-modal.js +++ b/ui/components/multichain/import-tokens-modal/import-tokens-modal.js @@ -13,14 +13,14 @@ import { Tab, Tabs } from '../../ui/tabs'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getCurrentChainId, + getInternalAccounts, getIsDynamicTokenListAvailable, getIsMainnet, getIsTokenDetectionInactiveOnMainnet, getIsTokenDetectionSupported, getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, - getMetaMaskIdentities, getRpcPrefsForCurrentProvider, - getSelectedAddress, + getSelectedAccount, getTokenDetectionSupportNetworkByChainId, getTokenList, } from '../../../selectors'; @@ -111,9 +111,9 @@ export const ImportTokensModal = ({ onClose }) => { const isDynamicTokenListAvailable = useSelector( getIsDynamicTokenListAvailable, ); - const selectedAddress = useSelector(getSelectedAddress); + const selectedAccount = useSelector(getSelectedAccount); const isMainnet = useSelector(getIsMainnet); - const identities = useSelector(getMetaMaskIdentities); + const accounts = useSelector(getInternalAccounts); const tokens = useSelector((state) => state.metamask.tokens); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); @@ -337,7 +337,7 @@ export const ImportTokensModal = ({ onClose }) => { try { ({ standard } = await getTokenStandardAndDetails( standardAddress, - selectedAddress, + selectedAccount.address, null, )); } catch (error) { @@ -383,7 +383,9 @@ export const ImportTokensModal = ({ onClose }) => { setCustomDecimalsError(null); break; - case Boolean(identities[standardAddress]): + case Boolean( + accounts.find((internalAccount) => internalAccount.address === address), + ): setCustomAddressError(t('personalAddressDetected')); break; diff --git a/ui/components/multichain/menu-items/account-details-menu-item.js b/ui/components/multichain/menu-items/account-details-menu-item.js index 3a2af09d1536..73e897e62584 100644 --- a/ui/components/multichain/menu-items/account-details-menu-item.js +++ b/ui/components/multichain/menu-items/account-details-menu-item.js @@ -2,7 +2,7 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; -import { setAccountDetailsAddress } from '../../../store/actions'; +import { setAccountDetailsAccountId } from '../../../store/actions'; import { MenuItem } from '../../ui/menu'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -16,7 +16,7 @@ import { IconName, Text } from '../../component-library'; export const AccountDetailsMenuItem = ({ metricsLocation, closeMenu, - address, + accountId, textProps, }) => { const t = useI18nContext(); @@ -28,7 +28,7 @@ export const AccountDetailsMenuItem = ({ return ( { - dispatch(setAccountDetailsAddress(address)); + dispatch(setAccountDetailsAccountId(accountId)); trackEvent({ event: MetaMetricsEventName.NavAccountDetailsOpened, category: MetaMetricsEventCategory.Navigation, @@ -56,9 +56,9 @@ AccountDetailsMenuItem.propTypes = { */ closeMenu: PropTypes.func, /** - * Address to show account details for + * Account ID */ - address: PropTypes.string.isRequired, + accountId: PropTypes.string.isRequired, /** * Custom properties for the menu item text */ diff --git a/ui/components/multichain/menu-items/account-details-menu-item.test.js b/ui/components/multichain/menu-items/account-details-menu-item.test.js index f13c27d63a9f..32923f487399 100644 --- a/ui/components/multichain/menu-items/account-details-menu-item.test.js +++ b/ui/components/multichain/menu-items/account-details-menu-item.test.js @@ -10,7 +10,7 @@ const render = () => { return renderWithProvider( , store, @@ -19,7 +19,7 @@ const render = () => { jest.mock('../../../store/actions', () => ({ ...jest.requireActual('../../../store/actions.ts'), - setAccountDetailsAddress: jest.fn().mockReturnValue({ type: 'TYPE' }), + setAccountDetailsAccountId: jest.fn().mockReturnValue({ type: 'TYPE' }), })); describe('AccountDetailsMenuItem', () => { @@ -31,8 +31,8 @@ describe('AccountDetailsMenuItem', () => { fireEvent.click(getByTestId('account-list-menu-details')); - expect(actions.setAccountDetailsAddress).toHaveBeenCalledWith( - mockState.metamask.selectedAddress, + expect(actions.setAccountDetailsAccountId).toHaveBeenCalledWith( + mockState.metamask.internalAccounts.selectedAccount, ); }); }); diff --git a/ui/components/multichain/select-action-modal/select-action-modal.test.js b/ui/components/multichain/select-action-modal/select-action-modal.test.js index 8582b1bec20b..778558215282 100644 --- a/ui/components/multichain/select-action-modal/select-action-modal.test.js +++ b/ui/components/multichain/select-action-modal/select-action-modal.test.js @@ -2,6 +2,7 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { fireEvent, waitFor } from '@testing-library/react'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import mockState from '../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../test/jest/rendering'; @@ -71,10 +72,23 @@ describe('Select Action Modal', () => { }, useCurrencyRateCheck: true, conversionRate: 2, - identities: { - '0x1': { - address: '0x1', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x1', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, accounts: { '0x1': { @@ -82,7 +96,6 @@ describe('Select Action Modal', () => { balance: '0x1F4', }, }, - selectedAddress: '0x1', keyrings: [ { type: KeyringType.imported, diff --git a/ui/ducks/alerts/unconnected-account.js b/ui/ducks/alerts/unconnected-account.js index a1cc12ba6058..049f038d79d0 100644 --- a/ui/ducks/alerts/unconnected-account.js +++ b/ui/ducks/alerts/unconnected-account.js @@ -6,9 +6,12 @@ import * as actionConstants from '../../store/actionConstants'; import { addPermittedAccount, setAlertEnabledness, - setSelectedAddress, + setSelectedInternalAccount, } from '../../store/actions'; -import { getOriginOfCurrentTab, getSelectedAddress } from '../../selectors'; +import { + getOriginOfCurrentTab, + getSelectedInternalAccount, +} from '../../selectors'; import { ALERT_STATE } from './enums'; // Constants @@ -111,11 +114,11 @@ export const dismissAndDisableAlert = () => { }; }; -export const switchToAccount = (address) => { +export const switchToAccount = (accountId) => { return async (dispatch) => { try { await dispatch(switchAccountRequested()); - await dispatch(setSelectedAddress(address)); + await dispatch(setSelectedInternalAccount(accountId)); await dispatch(switchAccountSucceeded()); } catch (error) { console.error(error); @@ -128,7 +131,7 @@ export const switchToAccount = (address) => { export const connectAccount = () => { return async (dispatch, getState) => { const state = getState(); - const selectedAddress = getSelectedAddress(state); + const { address: selectedAddress } = getSelectedInternalAccount(state); const origin = getOriginOfCurrentTab(state); try { await dispatch(connectAccountRequested()); diff --git a/ui/ducks/app/app.test.js b/ui/ducks/app/app.test.js index 714e2b514966..4b26aa826f98 100644 --- a/ui/ducks/app/app.test.js +++ b/ui/ducks/app/app.test.js @@ -1,3 +1,4 @@ +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import * as actionConstants from '../../store/actionConstants'; import { HardwareDeviceNames } from '../../../shared/constants/hardware-wallets'; import reduceApp from './app'; @@ -6,12 +7,23 @@ const actions = actionConstants; describe('App State', () => { const metamaskState = { - selectedAddress: '0xAddress', - identities: { - '0xAddress': { - name: 'account 1', - address: '0xAddress', + internalAccounts: { + accounts: { + 'mock-id': { + address: '0xAddress', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Accounts 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, }, + selectedAccount: 'mock-id', }, }; diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index e0c0bc708415..87fdb6c67671 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -74,8 +74,8 @@ interface AppState { newTokensImported: string; onboardedInThisUISession: boolean; customTokenAmount: string; - txId: string | null; - accountDetailsAddress: string; + txId: number | null; + accountDetailsAccountId: string; ///: BEGIN:ONLY_INCLUDE_IN(snaps) snapsInstallPrivacyWarningShown: boolean; ///: END:ONLY_INCLUDE_IN @@ -147,7 +147,7 @@ const initialState: AppState = { customTokenAmount: '', scrollToBottom: true, txId: null, - accountDetailsAddress: '', + accountDetailsAccountId: '', ///: BEGIN:ONLY_INCLUDE_IN(snaps) snapsInstallPrivacyWarningShown: false, ///: END:ONLY_INCLUDE_IN @@ -244,10 +244,10 @@ export default function reduceApp( alertMessage: null, }; - case actionConstants.SET_ACCOUNT_DETAILS_ADDRESS: { + case actionConstants.SET_ACCOUNT_DETAILS_ACCOUNT_ID: { return { ...appState, - accountDetailsAddress: action.payload, + accountDetailsAccountId: action.payload, }; } diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index e55d6be2fe79..e42b009502d0 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -9,6 +9,8 @@ import { accountsWithSendEtherInfoSelector, checkNetworkAndAccountSupports1559, getAddressBook, + getFirstInternalAccountByAddress, + getSelectedInternalAccount, getSelectedNetworkClientId, getUseCurrencyRateCheck, } from '../../selectors'; @@ -17,8 +19,6 @@ import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck'; import { KeyringType } from '../../../shared/constants/keyring'; import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../shared/constants/preferences'; -import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; -import { stripHexPrefix } from '../../../shared/modules/hexstring-utils'; import { decGWEIToHexWEI } from '../../../shared/modules/conversion.utils'; const initialState = { @@ -26,7 +26,10 @@ const initialState = { isUnlocked: false, isAccountMenuOpen: false, isNetworkMenuOpen: false, - identities: {}, + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, transactions: [], networkConfigurations: {}, addressBook: [], @@ -83,12 +86,21 @@ export default function reduceMetamask(state = initialState, action) { }; case actionConstants.SET_ACCOUNT_LABEL: { - const { account } = action.value; - const name = action.value.label; - const id = {}; - id[account] = { ...metamaskState.identities[account], name }; - const identities = { ...metamaskState.identities, ...id }; - return Object.assign(metamaskState, { identities }); + const { accountId, label } = action.value; + const internalAccounts = { + ...metamaskState.internalAccounts, + accounts: { + ...metamaskState.internalAccounts.accounts, + [accountId]: { + ...metamaskState.internalAccounts.accounts[accountId], + metadata: { + ...metamaskState.internalAccounts.accounts[accountId].metadata, + name: label, + }, + }, + }, + }; + return Object.assign(metamaskState, { internalAccounts }); } case actionConstants.UPDATE_CUSTOM_NONCE: @@ -259,8 +271,9 @@ export function getNftsDropdownState(state) { export const getNfts = (state) => { const { - metamask: { allNfts, selectedAddress }, + metamask: { allNfts }, } = state; + const { address: selectedAddress } = getSelectedInternalAccount(state); const { chainId } = getProviderConfig(state); return allNfts?.[selectedAddress]?.[chainId] ?? []; @@ -268,8 +281,9 @@ export const getNfts = (state) => { export const getNftContracts = (state) => { const { - metamask: { allNftContracts, selectedAddress }, + metamask: { allNftContracts }, } = state; + const { address: selectedAddress } = getSelectedInternalAccount(state); const { chainId } = getProviderConfig(state); return allNftContracts?.[selectedAddress]?.[chainId] ?? []; @@ -382,20 +396,13 @@ export function getSeedPhraseBackedUp(state) { * Given the redux state object and an address, finds a keyring that contains that address, if one exists * * @param {object} state - the redux state object - * @param {string} address - the address to search for among the keyring addresses + * @param {string} address - the address to search for among the internal accounts * @returns {object | undefined} The keyring which contains the passed address, or undefined */ export function findKeyringForAddress(state, address) { - const keyring = state.metamask.keyrings.find((kr) => { - return kr.accounts.some((account) => { - return ( - isEqualCaseInsensitive(account, addHexPrefix(address)) || - isEqualCaseInsensitive(account, stripHexPrefix(address)) - ); - }); - }); + const account = getFirstInternalAccountByAddress(state, address); - return keyring; + return account?.metadata?.keyring; } /** @@ -412,11 +419,11 @@ export function getLedgerTransportType(state) { * Given the redux state object and an address, returns a boolean indicating whether the passed address is part of a Ledger keyring * * @param {object} state - the redux state object - * @param {string} address - the address to search for among all keyring addresses + * @param {string} accountId - the account id to search for among all internal accounts * @returns {boolean} true if the passed address is part of a ledger keyring, and false otherwise */ -export function isAddressLedger(state, address) { - const keyring = findKeyringForAddress(state, address); +export function isAddressLedger(state, accountId) { + const keyring = findKeyringForAddress(state, accountId); return keyring?.type === KeyringType.ledger; } @@ -429,9 +436,11 @@ export function isAddressLedger(state, address) { * @returns {boolean} true if the user has a Ledger account and false otherwise */ export function doesUserHaveALedgerAccount(state) { - return state.metamask.keyrings.some((kr) => { - return kr.type === KeyringType.ledger; - }); + return Object.values(state.metamask.internalAccounts.accounts).some( + (account) => { + return account.metadata.keyring.type === KeyringType.ledger; + }, + ); } export function isLineaMainnetNetworkReleased(state) { diff --git a/ui/ducks/metamask/metamask.test.js b/ui/ducks/metamask/metamask.test.js index 1f43f9fea15c..dca2830186b4 100644 --- a/ui/ducks/metamask/metamask.test.js +++ b/ui/ducks/metamask/metamask.test.js @@ -1,5 +1,6 @@ import { NetworkType } from '@metamask/controller-utils'; import { NetworkStatus } from '@metamask/network-controller'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { TransactionStatus } from '../../../shared/constants/transaction'; import * as actionConstants from '../../store/actionConstants'; import reduceMetamask, { @@ -19,23 +20,62 @@ describe('MetaMask Reducers', () => { isInitialized: true, isUnlocked: true, featureFlags: { sendHexData: true }, - identities: { - '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { - address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', - name: 'Send Account 1', - }, - '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { - address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - name: 'Send Account 2', - }, - '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { - address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', - name: 'Send Account 3', - }, - '0xd85a4b6a394794842887b8284293d69163007bbb': { - address: '0xd85a4b6a394794842887b8284293d69163007bbb', - name: 'Send Account 4', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Send Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '07c2cfec-36c9-46c4-8115-3836d3ac9047': { + address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Send Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '15e69915-2a1a-4019-93b3-916e11fd432f': { + address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', + id: '15e69915-2a1a-4019-93b3-916e11fd432f', + metadata: { + name: 'Send Account 3', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '784225f4-d30b-4e77-a900-c8bbce735b88': { + address: '0xd85a4b6a394794842887b8284293d69163007bbb', + id: '784225f4-d30b-4e77-a900-c8bbce735b88', + metadata: { + name: 'Send Account 4', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, cachedBalances: {}, currentBlockGasLimit: '0x4c1878', @@ -124,7 +164,24 @@ describe('MetaMask Reducers', () => { it('locks MetaMask', () => { const unlockMetaMaskState = { isUnlocked: true, - selectedAddress: 'test address', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Send Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, }; const lockMetaMask = reduceMetamask(unlockMetaMaskState, { type: actionConstants.LOCK_METAMASK, @@ -135,18 +192,49 @@ describe('MetaMask Reducers', () => { it('sets account label', () => { const state = reduceMetamask( - {}, + { + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Send Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, + }, { type: actionConstants.SET_ACCOUNT_LABEL, value: { - account: 'test account', + accountId: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', label: 'test label', }, }, ); - expect(state.identities).toStrictEqual({ - 'test account': { name: 'test label' }, + expect(state.internalAccounts.accounts).toStrictEqual({ + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'test label', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, }); }); @@ -290,32 +378,68 @@ describe('MetaMask Reducers', () => { it('should return an array including all the users accounts and the address book', () => { expect(getSendToAccounts(mockState)).toStrictEqual([ { + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Send Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, code: '0x', balance: '0x47c9d71831c76efe', nonce: '0x1b', address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', - name: 'Send Account 1', }, { + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Send Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, code: '0x', balance: '0x37452b1315889f80', nonce: '0xa', address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - name: 'Send Account 2', }, { + id: '15e69915-2a1a-4019-93b3-916e11fd432f', + metadata: { + name: 'Send Account 3', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, code: '0x', balance: '0x30c9d71831c76efe', nonce: '0x1c', address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', - name: 'Send Account 3', }, { + id: '784225f4-d30b-4e77-a900-c8bbce735b88', + metadata: { + name: 'Send Account 4', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, code: '0x', balance: '0x0', nonce: '0x0', address: '0xd85a4b6a394794842887b8284293d69163007bbb', - name: 'Send Account 4', }, { address: '0x06195827297c7a80a443b6894d3bdb8824b43896', diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index 4af8d9bb8a9b..e5d46b4e0276 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -29,7 +29,6 @@ import { getCurrentChainId, getGasPriceInHexWei, getIsMainnet, - getTargetAccount, getIsNonStandardEthChain, checkNetworkAndAccountSupports1559, getUseTokenDetection, @@ -38,7 +37,9 @@ import { getIsMultiLayerFeeNetwork, getEnsResolutionByAddress, getSelectedAccount, - getSelectedAddress, + getSelectedInternalAccount, + getSelectedInternalAccountWithBalance, + getInternalAccountWithBalanceByAddress, getUnapprovedTransactions, } from '../../selectors'; import { @@ -504,6 +505,7 @@ export const computeEstimatedGasLimit = createAsyncThunk( const transaction = unapprovedTxs[draftTransaction.id]; const isNonStandardEthChain = getIsNonStandardEthChain(state); const chainId = getCurrentChainId(state); + const selectedAccount = getSelectedInternalAccountWithBalance(state); let gasTotalForLayer1; if (isMultiLayerFeeNetwork) { @@ -531,7 +533,7 @@ export const computeEstimatedGasLimit = createAsyncThunk( const gasLimit = await estimateGasLimitForSend({ gasPrice: draftTransaction.gas.gasPrice, blockGasLimit: metamask.currentBlockGasLimit, - selectedAddress: metamask.selectedAddress, + selectedAddress: selectedAccount.address, sendToken: draftTransaction.asset.details, to: draftTransaction.recipient.address?.toLowerCase(), value: draftTransaction.amount.value, @@ -667,7 +669,8 @@ export const initializeSendState = createAsyncThunk( blockGasLimit: metamask.currentBlockGasLimit, selectedAddress: draftTransaction.fromAccount?.address ?? - sendState.selectedAccount.address, + sendState.selectedAccount.address ?? + account.address, sendToken: draftTransaction.asset.details, to: draftTransaction.recipient.address.toLowerCase(), value: draftTransaction.amount.value, @@ -1733,7 +1736,10 @@ export function editExistingTransaction(assetType, transactionId) { const state = getState(); const unapprovedTransactions = getUnapprovedTransactions(state); const transaction = unapprovedTransactions[transactionId]; - const account = getTargetAccount(state, transaction.txParams.from); + const account = getInternalAccountWithBalanceByAddress( + state, + transaction.txParams.from, + ); if (assetType === AssetType.native) { await dispatch( @@ -1908,7 +1914,7 @@ export function updateRecipientUserInput(userInput) { const sendingAddress = draftTransaction.fromAccount?.address ?? state[name].selectedAccount.address ?? - getSelectedAddress(state); + getSelectedInternalAccount(state).address; const chainId = getCurrentChainId(state); const tokens = getTokens(state); const useTokenDetection = getUseTokenDetection(state); @@ -2030,8 +2036,11 @@ export function updateSendAsset( const sendingAddress = draftTransaction.fromAccount?.address ?? state[name].selectedAccount.address ?? - getSelectedAddress(state); - const account = getTargetAccount(state, sendingAddress); + getSelectedInternalAccount(state).address; + const account = getInternalAccountWithBalanceByAddress( + state, + sendingAddress, + ); if (type === AssetType.native) { const unapprovedTxs = getUnapprovedTransactions(state); const unapprovedTx = unapprovedTxs?.[draftTransaction.id]; diff --git a/ui/ducks/send/send.test.js b/ui/ducks/send/send.test.js index 4835c4d43079..40c20638c0e4 100644 --- a/ui/ducks/send/send.test.js +++ b/ui/ducks/send/send.test.js @@ -4,6 +4,7 @@ import thunk from 'redux-thunk'; import { BigNumber } from '@ethersproject/bignumber'; import { NetworkType } from '@metamask/controller-utils'; import { NetworkStatus } from '@metamask/network-controller'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { CONTRACT_ADDRESS_ERROR, INSUFFICIENT_FUNDS_ERROR, @@ -1283,8 +1284,24 @@ describe('Send Slice', () => { status: NetworkStatus.Available, }, }, - selectedAddress: mockAddress1, - identities: { [mockAddress1]: { address: mockAddress1 } }, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: mockAddress1, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, keyrings: [ { type: KeyringType.hdKeyTree, @@ -1434,7 +1451,25 @@ describe('Send Slice', () => { const sendState = { metamask: { blockGasLimit: '', - selectedAddress: '', + internalAccounts: { + accounts: { + 'mock-id': { + address: '0x0', + id: 'mock-id', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'mock-id', + }, + accounts: {}, providerConfig: { chainId: '0x1', }, @@ -1490,7 +1525,25 @@ describe('Send Slice', () => { const sendState = { metamask: { blockGasLimit: '', - selectedAddress: '', + internalAccounts: { + accounts: { + 'mock-id': { + address: '0x0', + id: 'mock-id', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'mock-id', + }, + accounts: {}, providerConfig: { chainId: '0x1', }, @@ -1545,7 +1598,25 @@ describe('Send Slice', () => { const tokenAssetTypeSendState = { metamask: { blockGasLimit: '', - selectedAddress: '', + internalAccounts: { + accounts: { + 'mock-id': { + address: '0x0', + id: 'mock-id', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'mock-id', + }, + accounts: {}, providerConfig: { chainId: '0x1', }, @@ -1593,7 +1664,24 @@ describe('Send Slice', () => { const defaultSendAssetState = { metamask: { blockGasLimit: '', - selectedAddress: '', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: mockAddress1, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, providerConfig: { chainId: CHAIN_IDS.GOERLI, }, @@ -1607,9 +1695,6 @@ describe('Send Slice', () => { address: mockAddress1, }, }, - identities: { - [mockAddress1]: {}, - }, }, send: { ...getInitialSendStateWithExistingTxState({ @@ -1802,6 +1887,24 @@ describe('Send Slice', () => { occurrences: 12, }, }, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, }, send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, }; @@ -1909,7 +2012,10 @@ describe('Send Slice', () => { const updateRecipientState = { metamask: { addressBook: {}, - identities: {}, + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, providerConfig: { chainId: '0x1', }, @@ -2017,7 +2123,10 @@ describe('Send Slice', () => { const tokenState = { metamask: { addressBook: {}, - identities: {}, + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, blockGasLimit: '', selectedAddress: '', providerConfig: { @@ -2067,7 +2176,6 @@ describe('Send Slice', () => { const updateRecipientState = { metamask: { addressBook: {}, - identities: {}, providerConfig: { chainId: '', }, @@ -2098,6 +2206,25 @@ describe('Send Slice', () => { occurrences: 12, }, }, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, + accounts: {}, }, send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, }; @@ -2250,6 +2377,25 @@ describe('Send Slice', () => { providerConfig: { chainId: CHAIN_IDS.GOERLI, }, + internalAccounts: { + accounts: { + 'mock-id': { + address: '0x0', + id: 'mock-id', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'mock-id', + }, + accounts: {}, }, }; const store = mockStore(sendMaxModeState); @@ -2423,8 +2569,23 @@ describe('Send Slice', () => { addressBook: { [CHAIN_IDS.GOERLI]: {}, }, - identities: { - [mockAddress1]: {}, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: mockAddress1, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, accounts: { [mockAddress1]: { @@ -2493,6 +2654,16 @@ describe('Send Slice', () => { fromAccount: { address: mockAddress1, balance: '0x0', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, gas: { error: null, @@ -2562,15 +2733,30 @@ describe('Send Slice', () => { addressBook: { [CHAIN_IDS.GOERLI]: {}, }, - identities: { - [mockAddress1]: {}, - }, accounts: { [mockAddress1]: { address: mockAddress1, balance: '0x0', }, }, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: mockAddress1, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, cachedBalances: { [CHAIN_IDS.GOERLI]: { [mockAddress1]: '0x0', @@ -2640,6 +2826,16 @@ describe('Send Slice', () => { fromAccount: { address: mockAddress1, balance: '0x0', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, gas: { error: null, @@ -2730,7 +2926,24 @@ describe('Send Slice', () => { const editTransactionState = { metamask: { blockGasLimit: '0x3a98', - selectedAddress: '', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: mockAddress1, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, providerConfig: { chainId: CHAIN_IDS.GOERLI, }, @@ -2749,9 +2962,6 @@ describe('Send Slice', () => { addressBook: { [CHAIN_IDS.GOERLI]: {}, }, - identities: { - [mockAddress1]: {}, - }, accounts: { [mockAddress1]: { address: mockAddress1, @@ -2835,6 +3045,16 @@ describe('Send Slice', () => { fromAccount: { address: mockAddress1, balance: '0x0', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, }, gas: { error: null, @@ -3170,7 +3390,10 @@ describe('Send Slice', () => { send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, metamask: { ensResolutionsByAddress: {}, - identities: {}, + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, addressBook: {}, providerConfig: { chainId: '0x5', @@ -3186,7 +3409,10 @@ describe('Send Slice', () => { metamask: { ensResolutionsByAddress: {}, addressBook: {}, - identities: {}, + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, providerConfig: { chainId: '0x5', }, @@ -3233,7 +3459,10 @@ describe('Send Slice', () => { send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, metamask: { ensResolutionsByAddress: {}, - identities: {}, + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, addressBook: {}, providerConfig: { chainId: '0x5', diff --git a/ui/ducks/swaps/swaps.test.js b/ui/ducks/swaps/swaps.test.js index 6ba7c7677019..f2700a78bfd6 100644 --- a/ui/ducks/swaps/swaps.test.js +++ b/ui/ducks/swaps/swaps.test.js @@ -421,7 +421,9 @@ describe('Ducks - Swaps', () => { it('returns false if feature flag is enabled, is a HW and is Ethereum network', () => { const state = createSwapsMockStore(); - state.metamask.keyrings[0].type = 'Trezor Hardware'; + state.metamask.internalAccounts.accounts[ + state.metamask.internalAccounts.selectedAccount + ].metadata.keyring.type = 'Trezor Hardware'; expect(swaps.getSmartTransactionsEnabled(state)).toBe(false); }); diff --git a/ui/helpers/utils/accounts.js b/ui/helpers/utils/accounts.js index f47f54981a4b..d058ecc517da 100644 --- a/ui/helpers/utils/accounts.js +++ b/ui/helpers/utils/accounts.js @@ -13,7 +13,7 @@ export function getAccountNameErrorMessage( defaultAccountName, ) { const isDuplicateAccountName = accounts.some( - (item) => item.name.toLowerCase() === newAccountName.toLowerCase(), + (item) => item.metadata.name.toLowerCase() === newAccountName.toLowerCase(), ); const isEmptyAccountName = newAccountName === ''; diff --git a/ui/helpers/utils/accounts.test.js b/ui/helpers/utils/accounts.test.js index e0bea4312f76..fb03cf343010 100644 --- a/ui/helpers/utils/accounts.test.js +++ b/ui/helpers/utils/accounts.test.js @@ -7,7 +7,10 @@ import { import { BackgroundColor } from '../constants/design-system'; import { getAccountNameErrorMessage, getAvatarNetworkColor } from './accounts'; -const mockAccounts = [{ name: 'Account 1' }, { name: 'Account 2' }]; +const mockAccounts = [ + { metadata: { name: 'Account 1' } }, + { metadata: { name: 'Account 2' } }, +]; const mockLocalization = { t: jest.fn().mockReturnValue('Account') }; diff --git a/ui/hooks/gasFeeInput/test-utils.js b/ui/hooks/gasFeeInput/test-utils.js index 86e00a822447..2a0cd7cd046c 100644 --- a/ui/hooks/gasFeeInput/test-utils.js +++ b/ui/hooks/gasFeeInput/test-utils.js @@ -14,6 +14,7 @@ import { txDataSelector, getCurrentKeyring, getTokenExchangeRates, + getSelectedInternalAccountWithBalance, } from '../../selectors'; import { useGasFeeEstimates } from '../useGasFeeEstimates'; @@ -142,6 +143,12 @@ export const generateUseSelectorRouter = if (selector === getTokenExchangeRates) { return { '0x1': '1' }; } + if (selector === getSelectedInternalAccountWithBalance) { + return { + balance: '0x440aa47cc2556', + }; + } + return undefined; }; diff --git a/ui/hooks/gasFeeInput/useGasFeeErrors.js b/ui/hooks/gasFeeInput/useGasFeeErrors.js index b48f5cef0fcf..70ef8c3772e1 100644 --- a/ui/hooks/gasFeeInput/useGasFeeErrors.js +++ b/ui/hooks/gasFeeInput/useGasFeeErrors.js @@ -3,7 +3,7 @@ import { shallowEqual, useSelector } from 'react-redux'; import { GasEstimateTypes, GAS_LIMITS } from '../../../shared/constants/gas'; import { checkNetworkAndAccountSupports1559, - getSelectedAccount, + getSelectedInternalAccountWithBalance, } from '../../selectors'; import { isLegacyTransaction } from '../../helpers/utils/transactions.util'; import { bnGreaterThan, bnLessThan } from '../../helpers/utils/util'; @@ -267,7 +267,10 @@ export function useGasFeeErrors({ [gasErrors, gasWarnings], ); - const { balance: ethBalance } = useSelector(getSelectedAccount, shallowEqual); + const { balance: ethBalance } = useSelector( + getSelectedInternalAccountWithBalance, + shallowEqual, + ); const balanceError = hasBalanceError( minimumCostInHexWei, transaction, diff --git a/ui/hooks/useAddressDetails.js b/ui/hooks/useAddressDetails.js index 63a1b58ec25e..96a7ed0f7e37 100644 --- a/ui/hooks/useAddressDetails.js +++ b/ui/hooks/useAddressDetails.js @@ -3,28 +3,32 @@ import { useSelector } from 'react-redux'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { getAddressBook, - getMetaMaskIdentities, + getInternalAccounts, getTokenList, } from '../selectors'; import { shortenAddress } from '../helpers/utils/util'; const useAddressDetails = (toAddress) => { const addressBook = useSelector(getAddressBook); - const identities = useSelector(getMetaMaskIdentities); + const accounts = useSelector(getInternalAccounts); const tokenList = useSelector(getTokenList); const checksummedAddress = toChecksumHexAddress(toAddress); if (!toAddress) { return {}; } + const toAccount = accounts.find( + (account) => toChecksumHexAddress(account.address) === checksummedAddress, + ); + const addressBookEntryObject = addressBook.find( (entry) => entry.address === checksummedAddress, ); if (addressBookEntryObject?.name) { return { toName: addressBookEntryObject.name, isTrusted: true }; } - if (identities[toAddress]?.name) { - return { toName: identities[toAddress].name, isTrusted: true }; + if (toAccount) { + return { toName: toAccount.metadata.name, isTrusted: true }; } if (tokenList[toAddress?.toLowerCase()]?.name) { return { diff --git a/ui/hooks/useAddressDetails.test.js b/ui/hooks/useAddressDetails.test.js index cf09f1b9d4a4..0773ebe8d264 100644 --- a/ui/hooks/useAddressDetails.test.js +++ b/ui/hooks/useAddressDetails.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { renderHook } from '@testing-library/react-hooks'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import configureStore from '../store/store'; import useAddressDetails from './useAddressDetails'; @@ -13,6 +14,24 @@ const renderUseAddressDetails = (toAddress, stateVariables = {}) => { chainId: '0x5', }, tokenList: {}, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, ...stateVariables, }, }; @@ -54,15 +73,27 @@ describe('useAddressDetails', () => { expect(isTrusted).toBe(true); }); - it('should return name from identities if address is present in identities', () => { + it('should return name from internal account if address is present in internalAccounts', () => { const { result } = renderUseAddressDetails( '0x06195827297c7A80a443b6894d3BDB8824b43896', { - identities: { - '0x06195827297c7A80a443b6894d3BDB8824b43896': { - address: '0x06195827297c7A80a443b6894d3BDB8824b43896', - name: 'Account 1', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x06195827297c7A80a443b6894d3BDB8824b43896', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, }, ); diff --git a/ui/hooks/useAssetDetails.test.js b/ui/hooks/useAssetDetails.test.js index 074a3aec65c8..a62091ac228b 100644 --- a/ui/hooks/useAssetDetails.test.js +++ b/ui/hooks/useAssetDetails.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { renderHook } from '@testing-library/react-hooks'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import configureStore from '../store/store'; import * as Actions from '../store/actions'; @@ -20,6 +21,24 @@ const renderUseAssetDetails = ({ }, tokenList: {}, tokens: [], + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: userAddress, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, }, }; diff --git a/ui/hooks/useNftsCollections.js b/ui/hooks/useNftsCollections.js index 17d639a15287..4df71373882b 100644 --- a/ui/hooks/useNftsCollections.js +++ b/ui/hooks/useNftsCollections.js @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { isEqual } from 'lodash'; import { getNfts, getNftContracts } from '../ducks/metamask/metamask'; -import { getCurrentChainId, getSelectedAddress } from '../selectors'; +import { getCurrentChainId, getSelectedInternalAccount } from '../selectors'; import { usePrevious } from './usePrevious'; import { useI18nContext } from './useI18nContext'; @@ -18,7 +18,7 @@ export function useNftsCollections() { }); const nfts = useSelector(getNfts); const [nftsLoading, setNftsLoading] = useState(() => nfts?.length >= 0); - const selectedAddress = useSelector(getSelectedAddress); + const { address: selectedAddress } = useSelector(getSelectedInternalAccount); const chainId = useSelector(getCurrentChainId); const nftContracts = useSelector(getNftContracts); const prevNfts = usePrevious(nfts); diff --git a/ui/hooks/useTokenTracker.js b/ui/hooks/useTokenTracker.js index b58562168806..a0952b4bca91 100644 --- a/ui/hooks/useTokenTracker.js +++ b/ui/hooks/useTokenTracker.js @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import TokenTracker from '@metamask/eth-token-tracker'; import { shallowEqual, useSelector } from 'react-redux'; -import { getCurrentChainId, getSelectedAddress } from '../selectors'; +import { getCurrentChainId, getSelectedInternalAccount } from '../selectors'; import { SECOND } from '../../shared/constants/time'; import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; import { useEqualityCheck } from './useEqualityCheck'; @@ -12,7 +12,10 @@ export function useTokenTracker( hideZeroBalanceTokens = false, ) { const chainId = useSelector(getCurrentChainId); - const userAddress = useSelector(getSelectedAddress, shallowEqual); + const { address: userAddress } = useSelector( + getSelectedInternalAccount, + shallowEqual, + ); const [loading, setLoading] = useState(() => tokens?.length >= 0); const [tokensWithBalances, setTokensWithBalances] = useState([]); const [error, setError] = useState(null); diff --git a/ui/hooks/useTransactionInfo.js b/ui/hooks/useTransactionInfo.js index eec5f9a6f572..316f1bb44bc6 100644 --- a/ui/hooks/useTransactionInfo.js +++ b/ui/hooks/useTransactionInfo.js @@ -2,11 +2,12 @@ import { useSelector } from 'react-redux'; import { getProviderConfig } from '../ducks/metamask/metamask'; import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; +import { getSelectedInternalAccount } from '../selectors'; export const useTransactionInfo = (txData = {}) => { - const { allNftContracts, selectedAddress } = useSelector( - (state) => state.metamask, - ); + const { allNftContracts } = useSelector((state) => state.metamask); + const { address: selectedAddress } = useSelector(getSelectedInternalAccount); + const { chainId } = useSelector(getProviderConfig); const isNftTransfer = Boolean( diff --git a/ui/hooks/useTransactionInfo.test.js b/ui/hooks/useTransactionInfo.test.js index 8d962e3e03ac..a3233b4f3872 100644 --- a/ui/hooks/useTransactionInfo.test.js +++ b/ui/hooks/useTransactionInfo.test.js @@ -15,8 +15,12 @@ describe('useTransactionInfo', () => { expect(result.current.isNftTransfer).toStrictEqual(false); }); it('should return true if transaction is NFT transfer', () => { + const selectedAddress = + mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].address; mockState.metamask.allNftContracts = { - [mockState.metamask.selectedAddress]: { + [selectedAddress]: { [mockState.metamask.providerConfig.chainId]: [{ address: '0x9' }], }, }; diff --git a/ui/index.js b/ui/index.js index e715e38faef2..1b4138b9a3b7 100644 --- a/ui/index.js +++ b/ui/index.js @@ -16,7 +16,7 @@ import * as actions from './store/actions'; import configureStore from './store/store'; import { getPermittedAccountsForCurrentTab, - getSelectedAddress, + getSelectedInternalAccount, getUnapprovedTransactions, } from './selectors'; import { ALERT_STATE } from './ducks/alerts'; @@ -130,7 +130,8 @@ async function startApp(metamaskState, backgroundConnection, opts) { const { origin } = draftInitialState.activeTab; const permittedAccountsForCurrentTab = getPermittedAccountsForCurrentTab(draftInitialState); - const selectedAddress = getSelectedAddress(draftInitialState); + const selectedAddress = + getSelectedInternalAccount(draftInitialState)?.address ?? ''; const unconnectedAccountAlertShownOrigins = getUnconnectedAccountAlertShown(draftInitialState); const unconnectedAccountAlertIsEnabled = diff --git a/ui/pages/asset/components/native-asset.js b/ui/pages/asset/components/native-asset.js index b0cdf6b702f2..a951711e5e1f 100644 --- a/ui/pages/asset/components/native-asset.js +++ b/ui/pages/asset/components/native-asset.js @@ -6,11 +6,10 @@ import { getAccountLink } from '@metamask/etherscan-link'; import TransactionList from '../../../components/app/transaction-list'; import { EthOverview } from '../../../components/app/wallet-overview'; import { - getSelectedIdentity, getCurrentChainId, getRpcPrefsForCurrentProvider, - getSelectedAddress, getIsCustomNetwork, + getSelectedInternalAccount, } from '../../../selectors/selectors'; import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { getURLHostName } from '../../../helpers/utils/util'; @@ -20,13 +19,11 @@ import AssetNavigation from './asset-navigation'; import AssetOptions from './asset-options'; export default function NativeAsset({ nativeCurrency }) { - const selectedAccountName = useSelector( - (state) => getSelectedIdentity(state).name, - ); + const { name: selectedAccountName } = useSelector(getSelectedInternalAccount); const chainId = useSelector(getCurrentChainId); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); - const address = useSelector(getSelectedAddress); + const { address } = useSelector(getSelectedInternalAccount); const history = useHistory(); const accountLink = getAccountLink(address, chainId, rpcPrefs); const trackEvent = useContext(MetaMetricsContext); diff --git a/ui/pages/asset/components/token-asset.js b/ui/pages/asset/components/token-asset.js index 18b1ee5103e4..027e97811e83 100644 --- a/ui/pages/asset/components/token-asset.js +++ b/ui/pages/asset/components/token-asset.js @@ -7,7 +7,7 @@ import TransactionList from '../../../components/app/transaction-list'; import { TokenOverview } from '../../../components/app/wallet-overview'; import { getCurrentChainId, - getSelectedIdentity, + getSelectedInternalAccount, getRpcPrefsForCurrentProvider, getIsCustomNetwork, } from '../../../selectors/selectors'; @@ -26,9 +26,9 @@ export default function TokenAsset({ token }) { const dispatch = useDispatch(); const chainId = useSelector(getCurrentChainId); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); - const selectedIdentity = useSelector(getSelectedIdentity); - const selectedAccountName = selectedIdentity.name; - const selectedAddress = selectedIdentity.address; + const selectedAccount = useSelector(getSelectedInternalAccount); + const selectedAccountName = selectedAccount.metadata.name; + const selectedAddress = selectedAccount.address; const history = useHistory(); const tokenTrackerLink = getTokenTrackerLink( token.address, diff --git a/ui/pages/confirm-add-suggested-nft/__snapshots__/confirm-add-suggested-nft.test.js.snap b/ui/pages/confirm-add-suggested-nft/__snapshots__/confirm-add-suggested-nft.test.js.snap index 02d00db76cf9..acfd8aa362b0 100644 --- a/ui/pages/confirm-add-suggested-nft/__snapshots__/confirm-add-suggested-nft.test.js.snap +++ b/ui/pages/confirm-add-suggested-nft/__snapshots__/confirm-add-suggested-nft.test.js.snap @@ -24,49 +24,9 @@ exports[`ConfirmAddSuggestedNFT Component should match snapshot 1`] = ` class="box box--display-flex box--flex-direction-row box--align-items-center" >
-
-
- - - - - -
-
-
+ class="identicon__image-border" + style="height: 32px; width: 32px; border-radius: 16px;" + />
-
-
- - - - - -
-
-
+ class="identicon__image-border" + style="height: 32px; width: 32px; border-radius: 16px;" + />
{ pendingApprovals: PENDING_APPROVALS, tokens, providerConfig: { chainId: '0x1' }, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, }, history: { mostRecentOverviewPage: '/', diff --git a/ui/pages/confirm-decrypt-message/confirm-decrypt-message.component.test.js b/ui/pages/confirm-decrypt-message/confirm-decrypt-message.component.test.js index 4939352d2bed..bdd49dd21369 100644 --- a/ui/pages/confirm-decrypt-message/confirm-decrypt-message.component.test.js +++ b/ui/pages/confirm-decrypt-message/confirm-decrypt-message.component.test.js @@ -1,5 +1,6 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import mockState from '../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../test/lib/render-helpers'; import ConfirmDecryptMessage from './confirm-decrypt-message.component'; @@ -80,7 +81,16 @@ const baseProps = { fromAccount: { address: '0x123456789abcdef', balance: '0x346ba7725f412cbfdb', - name: 'Antonio', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Antonio', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, }; diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js index 4d84fbbf6ddf..1cd76046bd4a 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js @@ -1,5 +1,6 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import mockState from '../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../test/lib/render-helpers'; import ConfirmEncryptionPublicKey from './confirm-encryption-public-key.component'; @@ -29,6 +30,16 @@ const baseProps = { fromAccount: { address: '0x123456789abcdef', balance: '0x346ba7725f412cbfdb', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Antonio', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, name: 'Antonio', }, }; diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.stories.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.stories.js index d0ec8f1469f7..c85f904c80ca 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.stories.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.stories.js @@ -65,7 +65,7 @@ export default { }, }, args: { - fromAccount: metamask.accountArray[0], + fromAccount: Object.values(metamask.internalAccounts.accounts)[0], history: { push: action('history.push()'), }, diff --git a/ui/pages/confirm-signature-request/index.js b/ui/pages/confirm-signature-request/index.js index 35c5b9a824ed..f87cd66af3bf 100644 --- a/ui/pages/confirm-signature-request/index.js +++ b/ui/pages/confirm-signature-request/index.js @@ -55,7 +55,6 @@ const ConfirmTxScreen = ({ match }) => { ); const sendTo = useSelector(getSendTo); const { - identities, currentCurrency, unapprovedMsgs, unapprovedPersonalMsgs, @@ -187,7 +186,6 @@ const ConfirmTxScreen = ({ match }) => { history={history} txData={txData} key={txData.id} - identities={identities} currentCurrency={currentCurrency} blockGasLimit={blockGasLimit} ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) diff --git a/ui/pages/confirm-signature-request/index.test.js b/ui/pages/confirm-signature-request/index.test.js index 2292694b225c..f75a0e30aee2 100644 --- a/ui/pages/confirm-signature-request/index.test.js +++ b/ui/pages/confirm-signature-request/index.test.js @@ -1,17 +1,11 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { renderWithProvider } from '../../../test/lib/render-helpers'; import ConfTx from '.'; const mockState = { metamask: { - identities: { - '0x8eeee1781fd885ff5ddef7789486676961873d12': { - address: '0x8eeee1781fd885ff5ddef7789486676961873d12', - lastSelected: 1673587189888, - name: 'Account 1', - }, - }, unapprovedMsgs: {}, unapprovedMsgCount: 0, unapprovedPersonalMsgs: {}, @@ -38,13 +32,30 @@ const mockState = { cachedBalances: { '0x5': {}, }, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x8eeee1781fd885ff5ddef7789486676961873d12', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, accounts: { '0x8eeee1781fd885ff5ddef7789486676961873d12': { address: '0x8eeee1781fd885ff5ddef7789486676961873d12', balance: '0x7e64033f2fdb0436', }, }, - selectedAddress: '0x8eeee1781fd885ff5ddef7789486676961873d12', addressBook: {}, tokenList: {}, preferences: {}, diff --git a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.js b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.js index 94a64145ac69..a8532d2e3b4c 100644 --- a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.js +++ b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.js @@ -18,7 +18,7 @@ import { getCurrentChainId, getCurrentCurrency, getRpcPrefsForCurrentProvider, - getSelectedAddress, + getSelectedInternalAccount, } from '../../selectors'; import { getConversionRate, @@ -54,7 +54,7 @@ export default function ConfirmTokenTransactionBase({ const conversionRate = useSelector(getConversionRate); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); const chainId = useSelector(getCurrentChainId); - const userAddress = useSelector(getSelectedAddress); + const { address: userAddress } = useSelector(getSelectedInternalAccount); const nftCollections = useSelector(getNftContracts); const ethTransactionTotalMaxAmount = Number( diff --git a/ui/pages/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap b/ui/pages/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap index 01cecf920f09..7a2f7a81583b 100644 --- a/ui/pages/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap +++ b/ui/pages/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap @@ -140,7 +140,9 @@ exports[`Confirm Transaction Base should match snapshot 1`] = ` >
+ > + Account 1 +
diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js index 5265d3b14cc9..2e4076fa5ab3 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -25,7 +25,6 @@ import { getCustomNonceValue, getIsMainnet, getKnownMethodData, - getMetaMaskAccounts, getUseNonceField, transactionFeeSelector, getNoGasPriceFetched, @@ -41,6 +40,8 @@ import { getUnapprovedTransaction, getFullTxData, getUseCurrencyRateCheck, + getFirstInternalAccountByAddress, + getInternalAccountWithBalanceByAddress, getUnapprovedTransactions, } from '../../selectors'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; @@ -122,8 +123,7 @@ const mapStateToProps = (state, ownProps) => { const gasLoadingAnimationIsShowing = getGasLoadingAnimationIsShowing(state); const isBuyableChain = getIsBuyableChain(state); const { confirmTransaction, metamask } = state; - const { conversionRate, identities, addressBook, networkId, nextNonce } = - metamask; + const { conversionRate, addressBook, networkId, nextNonce } = metamask; const unapprovedTxs = getUnapprovedTransactions(state); const { chainId } = getProviderConfig(state); const { tokenData, txData, tokenProps, nonce } = confirmTransaction; @@ -138,13 +138,18 @@ const mapStateToProps = (state, ownProps) => { value: amount, data, } = (transaction && transaction.txParams) || txParams; - const accounts = getMetaMaskAccounts(state); const transactionData = parseStandardTokenTransactionData(data); const tokenToAddress = getTokenAddressParam(transactionData); - const { balance } = accounts[fromAddress]; - const { name: fromName } = identities[fromAddress]; + const fromAccount = getInternalAccountWithBalanceByAddress( + state, + fromAddress, + ); + const { + metadata: { name: fromName }, + balance, + } = fromAccount; const isSendingAmount = type === TransactionType.simpleSend || !isEmptyHexString(amount); @@ -158,7 +163,7 @@ const mapStateToProps = (state, ownProps) => { const tokenList = getTokenList(state); const toName = - identities[toAddress]?.name || + getFirstInternalAccountByAddress(state, toAddress)?.metadata.name || tokenList[toAddress?.toLowerCase()]?.name || shortenAddress(toChecksumHexAddress(toAddress)); diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.test.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.test.js index 882d32dc4dd9..bcf1bfe77340 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.test.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.test.js @@ -5,6 +5,7 @@ import { fireEvent } from '@testing-library/react'; import { NetworkType } from '@metamask/controller-utils'; import { NetworkStatus } from '@metamask/network-controller'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { renderWithProvider } from '../../../test/lib/render-helpers'; import { setBackgroundConnection } from '../../../test/jest'; import { INITIAL_SEND_STATE_FOR_EXISTING_DRAFT } from '../../../test/jest/mocks'; @@ -80,7 +81,6 @@ const baseStore = { medium: '1', fast: '2', }, - selectedAddress: mockTxParamsFromAddress, keyrings: [ { type: KeyringType.hdKeyTree, @@ -119,11 +119,23 @@ const baseStore = { address: mockTxParamsFromAddress, }, }, - identities: { - [mockTxParamsFromAddress]: { address: mockTxParamsFromAddress }, - [mockTxParamsToAddress]: { - name: 'Test Address 1', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: mockTxParamsFromAddress, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, tokenAddress: '0x32e6c34cd57087abbd59b5a4aecc4cb495924356', tokenList: {}, diff --git a/ui/pages/connected-accounts/connected-accounts.component.js b/ui/pages/connected-accounts/connected-accounts.component.js index 6be0862d1074..0b35a5b7663d 100644 --- a/ui/pages/connected-accounts/connected-accounts.component.js +++ b/ui/pages/connected-accounts/connected-accounts.component.js @@ -25,7 +25,7 @@ export default class ConnectedAccounts extends PureComponent { isActiveTabExtension: PropTypes.bool.isRequired, selectedAddress: PropTypes.string.isRequired, removePermittedAccount: PropTypes.func.isRequired, - setSelectedAddress: PropTypes.func.isRequired, + setSelectedAccount: PropTypes.func.isRequired, history: PropTypes.object.isRequired, }; @@ -41,7 +41,7 @@ export default class ConnectedAccounts extends PureComponent { permissions, selectedAddress, removePermittedAccount, - setSelectedAddress, + setSelectedAccount, } = this.props; const { t } = this.context; @@ -76,7 +76,7 @@ export default class ConnectedAccounts extends PureComponent { connectedAccounts={connectedAccounts} selectedAddress={selectedAddress} removePermittedAccount={removePermittedAccount} - setSelectedAddress={setSelectedAddress} + setSelectedAccount={setSelectedAccount} shouldRenderListOptions /> diff --git a/ui/pages/connected-accounts/connected-accounts.container.js b/ui/pages/connected-accounts/connected-accounts.container.js index 8a8cffb3b530..ffe550ca8c7f 100644 --- a/ui/pages/connected-accounts/connected-accounts.container.js +++ b/ui/pages/connected-accounts/connected-accounts.container.js @@ -3,13 +3,13 @@ import { getAccountToConnectToActiveTab, getOrderedConnectedAccountsForActiveTab, getPermissionsForActiveTab, - getSelectedAddress, + getSelectedInternalAccount, } from '../../selectors'; import { isExtensionUrl } from '../../helpers/utils/util'; import { addPermittedAccount, removePermittedAccount, - setSelectedAddress, + setSelectedAccount, } from '../../store/actions'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; import ConnectedAccounts from './connected-accounts.component'; @@ -19,7 +19,7 @@ const mapStateToProps = (state) => { const accountToConnect = getAccountToConnectToActiveTab(state); const connectedAccounts = getOrderedConnectedAccountsForActiveTab(state); const permissions = getPermissionsForActiveTab(state); - const selectedAddress = getSelectedAddress(state); + const { address: selectedAddress } = getSelectedInternalAccount(state); const isActiveTabExtension = isExtensionUrl(activeTab); return { @@ -39,7 +39,7 @@ const mapDispatchToProps = (dispatch) => { dispatch(addPermittedAccount(origin, address)), removePermittedAccount: (origin, address) => dispatch(removePermittedAccount(origin, address)), - setSelectedAddress: (address) => dispatch(setSelectedAddress(address)), + setSelectedAccount: (accountId) => dispatch(setSelectedAccount(accountId)), }; }; diff --git a/ui/pages/connected-accounts/connected-accounts.stories.js b/ui/pages/connected-accounts/connected-accounts.stories.js index ea2aa55af55c..7f158c14148f 100644 --- a/ui/pages/connected-accounts/connected-accounts.stories.js +++ b/ui/pages/connected-accounts/connected-accounts.stories.js @@ -1,30 +1,50 @@ import React from 'react'; import { action } from '@storybook/addon-actions'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import ConnectedAccounts from './connected-accounts.component'; export default { title: 'Pages/ConnectedAccounts', }; -const account = [ +const accounts = [ { - name: 'Account 1', - address: '0x983211ce699ea5ab57cc528086154b6db1ad8e55', + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + { + address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b', + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Test Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, ]; -const identities = { - name: 'Account 1', - address: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', -}; + export const DefaultStory = () => { return ( ); }; diff --git a/ui/pages/connected-sites/connected-sites.container.js b/ui/pages/connected-sites/connected-sites.container.js index 7e7b2a9c47fd..ac813d8fb2b2 100644 --- a/ui/pages/connected-sites/connected-sites.container.js +++ b/ui/pages/connected-sites/connected-sites.container.js @@ -11,7 +11,7 @@ import { getOriginOfCurrentTab, getPermissionSubjects, getPermittedAccountsByOrigin, - getSelectedAddress, + getSelectedInternalAccount, } from '../../selectors'; import { CONNECT_ROUTE } from '../../helpers/constants/routes'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; @@ -23,7 +23,7 @@ const mapStateToProps = (state) => { const connectedSubjects = getConnectedSubjectsForSelectedAddress(state); const originOfCurrentTab = getOriginOfCurrentTab(state); const permittedAccountsByOrigin = getPermittedAccountsByOrigin(state); - const selectedAddress = getSelectedAddress(state); + const { address: selectedAddress } = getSelectedInternalAccount(state); const currentTabHasNoAccounts = !permittedAccountsByOrigin[originOfCurrentTab]?.length; @@ -36,7 +36,7 @@ const mapStateToProps = (state) => { } return { - accountLabel: getCurrentAccountWithSendEtherInfo(state).name, + accountLabel: getCurrentAccountWithSendEtherInfo(state).metadata.name, connectedSubjects, subjects: getPermissionSubjects(state), mostRecentOverviewPage: getMostRecentOverviewPage(state), diff --git a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.js b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.js index db0c2d5af4b6..074167d6559f 100644 --- a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.js +++ b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.js @@ -3,7 +3,10 @@ import { useSelector, useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; -import { getMetaMaskAccounts } from '../../../selectors'; +import { + getMetaMaskAccounts, + getSelectedInternalAccount, +} from '../../../selectors'; import CustodyLabels from '../../../components/institutional/custody-labels/custody-labels'; import PulseLoader from '../../../components/ui/pulse-loader'; import { INSTITUTIONAL_FEATURES_DONE_ROUTE } from '../../../helpers/constants/routes'; @@ -52,14 +55,15 @@ export default function InteractiveReplacementTokenPage({ history }) { (state) => state.appState.modal.modalState.props?.address, ); const { - selectedAddress, custodyAccountDetails, interactiveReplacementToken, mmiConfiguration, } = useSelector((state) => state.metamask); + const selectedAccount = useSelector(getSelectedInternalAccount); const { custodianName } = - custodyAccountDetails[toChecksumHexAddress(address || selectedAddress)] || - {}; + custodyAccountDetails[ + toChecksumHexAddress(address || selectedAccount.address) + ] || {}; const { url } = interactiveReplacementToken || {}; const { custodians } = mmiConfiguration; const custodian = @@ -106,20 +110,21 @@ export default function InteractiveReplacementTokenPage({ history }) { ), ); - const filteredAccounts = custodianAccounts.filter( - (account) => metaMaskAccounts[account.address.toLowerCase()], - ); - - const mappedAccounts = filteredAccounts.map((account) => ({ - address: account.address, - name: account.name, - labels: account.labels, - balance: - metaMaskAccounts[account.address.toLowerCase()]?.balance || 0, - })); + const custodianAccountsWithBalances = custodianAccounts + .map((custodianAccount) => { + return { + ...custodianAccount, + balance: Object.values(metaMaskAccounts).find( + (account) => + account.address.toLowerCase() === + custodianAccount.address.toLowerCase(), + )?.balance, + }; + }) + .filter((custodianAccount) => custodianAccount.balance !== undefined); if (isMounted) { - setTokenAccounts(mappedAccounts); + setTokenAccounts(custodianAccountsWithBalances); setIsLoading(false); } } catch (e) { diff --git a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.js b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.js index a888b0a89032..54cf51123283 100644 --- a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.js +++ b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.js @@ -85,7 +85,6 @@ const render = ({ newState } = {}) => { metamask: { ...mockState.metamask, modal: { props: address }, - selectedAddress: address, interactiveReplacementToken: { url: 'https://saturn-custody-ui.codefi.network/', }, @@ -106,9 +105,28 @@ const render = ({ newState } = {}) => { institutionalFeatures: { connectRequests, }, + internalAccounts: { + ...mockState.metamask.internalAccounts, + accounts: { + ...mockState.metamask.internalAccounts.accounts, + [mockState.metamask.internalAccounts.selectedAccount]: { + ...mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ], + address, + metadata: { + ...mockState.metamask.internalAccounts.accounts[ + mockState.metamask.internalAccounts.selectedAccount + ].metadata, + name: accountName, + }, + }, + }, + }, ...newState, }, }; + const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); const store = mockStore(state); diff --git a/ui/pages/onboarding-flow/create-password/create-password.test.js b/ui/pages/onboarding-flow/create-password/create-password.test.js index bbb330d3bb52..f87c791f0774 100644 --- a/ui/pages/onboarding-flow/create-password/create-password.test.js +++ b/ui/pages/onboarding-flow/create-password/create-password.test.js @@ -24,8 +24,10 @@ jest.mock('react-router-dom', () => ({ describe('Onboarding Create Password', () => { const mockState = { metamask: { - identities: {}, - selectedAddress: '', + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, metaMetricsId: '0x00000000', }, }; diff --git a/ui/pages/onboarding-flow/import-srp/import-srp.test.js b/ui/pages/onboarding-flow/import-srp/import-srp.test.js index ef88f715eab3..5d4004dc0dec 100644 --- a/ui/pages/onboarding-flow/import-srp/import-srp.test.js +++ b/ui/pages/onboarding-flow/import-srp/import-srp.test.js @@ -21,8 +21,10 @@ const TEST_SEED = describe('Import SRP', () => { const mockState = { metamask: { - identities: {}, - selectedAddress: '', + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, }, }; diff --git a/ui/pages/onboarding-flow/onboarding-flow.test.js b/ui/pages/onboarding-flow/onboarding-flow.test.js index 1f42e52eb09e..5f23bed6bd2a 100644 --- a/ui/pages/onboarding-flow/onboarding-flow.test.js +++ b/ui/pages/onboarding-flow/onboarding-flow.test.js @@ -36,7 +36,10 @@ jest.mock('../../store/actions', () => ({ describe('Onboarding Flow', () => { const mockState = { metamask: { - identities: {}, + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, providerConfig: { type: NETWORK_TYPES.GOERLI, chainId: '0x0', diff --git a/ui/pages/onboarding-flow/welcome/welcome.test.js b/ui/pages/onboarding-flow/welcome/welcome.test.js index c7965efd52f4..9aaca063c45e 100644 --- a/ui/pages/onboarding-flow/welcome/welcome.test.js +++ b/ui/pages/onboarding-flow/welcome/welcome.test.js @@ -42,8 +42,10 @@ jest.mock('react-router-dom', () => ({ describe('Onboarding Welcome Component', () => { const mockState = { metamask: { - identities: {}, - selectedAddress: '', + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, }, }; diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js index 4820bc1ab55a..f70899179e08 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -410,7 +410,7 @@ export default class PermissionConnect extends Component { rejectPermissionsRequest={(requestId) => this.cancelPermissionsRequest(requestId) } - selectedIdentities={accounts.filter((account) => + selectedAccounts={accounts.filter((account) => selectedAccountAddresses.has(account.address), )} targetSubjectMetadata={targetSubjectMetadata} diff --git a/ui/pages/permissions-connect/permissions-connect.container.js b/ui/pages/permissions-connect/permissions-connect.container.js index a5f1ee145092..41576cc2fc0e 100644 --- a/ui/pages/permissions-connect/permissions-connect.container.js +++ b/ui/pages/permissions-connect/permissions-connect.container.js @@ -8,7 +8,7 @@ import { getAccountsWithLabels, getLastConnectedInfo, getPermissionsRequests, - getSelectedAddress, + getSelectedInternalAccount, ///: BEGIN:ONLY_INCLUDE_IN(snaps) getSnapInstallOrUpdateRequests, getRequestState, @@ -57,7 +57,7 @@ const mapStateToProps = (state, ownProps) => { ...getSnapInstallOrUpdateRequests(state), ]; ///: END:ONLY_INCLUDE_IN - const currentAddress = getSelectedAddress(state); + const { address: currentAddress } = getSelectedInternalAccount(state); const permissionsRequest = permissionsRequests.find( (req) => req.metadata.id === permissionsRequestId, diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index bb384a17e03a..6201aa7079ed 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -158,7 +158,7 @@ export default class Routes extends Component { toggleAccountMenu: PropTypes.func, isNetworkMenuOpen: PropTypes.bool, toggleNetworkMenu: PropTypes.func, - accountDetailsAddress: PropTypes.string, + accountDetailsAccountId: PropTypes.string, isImportNftsModalOpen: PropTypes.bool.isRequired, hideImportNftsModal: PropTypes.func.isRequired, isIpfsModalOpen: PropTypes.bool.isRequired, @@ -512,7 +512,7 @@ export default class Routes extends Component { toggleAccountMenu, isNetworkMenuOpen, toggleNetworkMenu, - accountDetailsAddress, + accountDetailsAccountId, isImportTokensModalOpen, isSelectActionModalOpen, location, @@ -578,8 +578,8 @@ export default class Routes extends Component { {isNetworkMenuOpen ? ( toggleNetworkMenu()} /> ) : null} - {accountDetailsAddress ? ( - + {accountDetailsAccountId ? ( + ) : null} {isImportNftsModalOpen ? ( hideImportNftsModal()} /> diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index 4773b65dea44..93c9791efaa1 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -66,8 +66,8 @@ function mapStateToProps(state) { completedOnboarding, isAccountMenuOpen: state.metamask.isAccountMenuOpen, isNetworkMenuOpen: state.metamask.isNetworkMenuOpen, + accountDetailsAccountId: state.appState.accountDetailsAccountId, isImportTokensModalOpen: state.appState.importTokensModalOpen, - accountDetailsAddress: state.appState.accountDetailsAddress, isImportNftsModalOpen: state.appState.importNftsModal.open, isIpfsModalOpen: state.appState.showIpfsModalOpen, isSelectActionModalOpen: state.appState.showSelectActionModal, diff --git a/ui/pages/send/send-content/add-recipient/__snapshots__/add-recipient.component.test.js.snap b/ui/pages/send/send-content/add-recipient/__snapshots__/add-recipient.component.test.js.snap index b6587dd99582..0adf9aa4a196 100644 --- a/ui/pages/send/send-content/add-recipient/__snapshots__/add-recipient.component.test.js.snap +++ b/ui/pages/send/send-content/add-recipient/__snapshots__/add-recipient.component.test.js.snap @@ -158,7 +158,7 @@ exports[`Add Recipient Component Domain Resolution should match snapshot 1`] = ` style="height: 28px; width: 28px; border-radius: 14px;" >
- Test Ledger 1 + Test Account 3

- 0xc42e...8813 + 0xeb9e...4823

@@ -221,7 +221,7 @@ exports[`Add Recipient Component Domain Resolution should match snapshot 1`] = ` style="height: 28px; width: 28px; border-radius: 14px;" >
- Test Account 3 + Ledger Hardware 2

- 0xeb9e...4823 + 0xc42e...8813

@@ -594,7 +594,7 @@ exports[`Add Recipient Component render should match snapshot 1`] = ` style="height: 28px; width: 28px; border-radius: 14px;" >
- Test Ledger 1 + Test Account 3

- 0xc42e...8813 + 0xeb9e...4823

@@ -657,7 +657,7 @@ exports[`Add Recipient Component render should match snapshot 1`] = ` style="height: 28px; width: 28px; border-radius: 14px;" >
- Test Account 3 + Ledger Hardware 2

- 0xeb9e...4823 + 0xc42e...8813

diff --git a/ui/pages/send/send-content/add-recipient/add-recipient.component.js b/ui/pages/send/send-content/add-recipient/add-recipient.component.js index 67533a739787..23725cfaf6e5 100644 --- a/ui/pages/send/send-content/add-recipient/add-recipient.component.js +++ b/ui/pages/send/send-content/add-recipient/add-recipient.component.js @@ -168,7 +168,8 @@ export default class AddRecipient extends Component { if (userInput) { ownedAccounts = ownedAccounts.filter( (item) => - item.name.toLowerCase().indexOf(userInput.toLowerCase()) > -1 || + item.metadata.name.toLowerCase().indexOf(userInput.toLowerCase()) > + -1 || item.address.toLowerCase().indexOf(userInput.toLowerCase()) > -1, ); } diff --git a/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js b/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js index 15d741d04b95..f2bc591271fb 100644 --- a/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js +++ b/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js @@ -27,7 +27,22 @@ export default class SendAssetRow extends Component { }), ).isRequired, accounts: PropTypes.object.isRequired, - selectedAddress: PropTypes.string.isRequired, + selectedAccount: PropTypes.shape({ + id: PropTypes.string.isRequired, + address: PropTypes.string.isRequired, + balance: PropTypes.string.isRequired, + metadata: PropTypes.shape({ + name: PropTypes.string.isRequired, + snap: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string, + enabled: PropTypes.bool, + }), + keyring: PropTypes.shape({ + type: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + }).isRequired, sendAsset: PropTypes.object, updateSendAsset: PropTypes.func.isRequired, nativeCurrency: PropTypes.string, @@ -197,12 +212,12 @@ export default class SendAssetRow extends Component { renderNativeCurrency(insideDropdown = false) { const { t } = this.context; - const { accounts, selectedAddress, nativeCurrency, nativeCurrencyImage } = + const { accounts, selectedAccount, nativeCurrency, nativeCurrencyImage } = this.props; const { sendableTokens, sendableNfts } = this.state; - const balanceValue = accounts[selectedAddress] - ? accounts[selectedAddress].balance + const balanceValue = accounts[selectedAccount.id] + ? accounts[selectedAccount.id].balance : ''; const sendableAssets = [...sendableTokens, ...sendableNfts]; diff --git a/ui/pages/send/send-content/send-asset-row/send-asset-row.container.js b/ui/pages/send/send-content/send-asset-row/send-asset-row.container.js index 63c579c57705..863682012228 100644 --- a/ui/pages/send/send-content/send-asset-row/send-asset-row.container.js +++ b/ui/pages/send/send-content/send-asset-row/send-asset-row.container.js @@ -7,6 +7,7 @@ import { import { getMetaMaskAccounts, getNativeCurrencyImage, + getSelectedAccount, } from '../../../../selectors'; import { updateSendAsset, getSendAsset } from '../../../../ducks/send'; import SendAssetRow from './send-asset-row.component'; @@ -14,7 +15,7 @@ import SendAssetRow from './send-asset-row.component'; function mapStateToProps(state) { return { tokens: state.metamask.tokens, - selectedAddress: state.metamask.selectedAddress, + selectedAccount: getSelectedAccount(state), nfts: getNfts(state), collections: getNftContracts(state), sendAsset: getSendAsset(state), diff --git a/ui/pages/send/send-content/send-asset-row/send-asset-row.stories.js b/ui/pages/send/send-content/send-asset-row/send-asset-row.stories.js index bc18085d02d3..e5962a3ac113 100644 --- a/ui/pages/send/send-content/send-asset-row/send-asset-row.stories.js +++ b/ui/pages/send/send-content/send-asset-row/send-asset-row.stories.js @@ -18,13 +18,16 @@ export default { export const DefaultStory = () => { const { metamask } = store.getState(); - const { identities, assetImages, tokens } = metamask; + const { internalAccounts, assetImages, tokens } = metamask; + const accounts = Object.values(internalAccounts.accounts); + const selectedAccount = + internalAccounts.accounts[internalAccounts.selectedAccount]; return ( undefined} setUnsendableAssetError={() => undefined} diff --git a/ui/pages/send/send.test.js b/ui/pages/send/send.test.js index 591e499fc64e..6408936275cb 100644 --- a/ui/pages/send/send.test.js +++ b/ui/pages/send/send.test.js @@ -3,6 +3,7 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { useLocation } from 'react-router-dom'; import { NetworkType } from '@metamask/controller-utils'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { SEND_STAGES, startNewDraftTransaction } from '../../ducks/send'; import { domainInitialState } from '../../ducks/domains'; import { CHAIN_IDS } from '../../../shared/constants/network'; @@ -79,7 +80,24 @@ const baseStore = { medium: '1', fast: '2', }, - selectedAddress: '0x0', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x0', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, keyrings: [ { type: KeyringType.hdKeyTree, @@ -114,7 +132,6 @@ const baseStore = { accounts: { '0x0': { balance: '0x0', address: '0x0' }, }, - identities: { '0x0': { address: '0x0' } }, tokenAddress: '0x32e6c34cd57087abbd59b5a4aecc4cb495924356', tokenList: { '0x32e6c34cd57087abbd59b5a4aecc4cb495924356': { diff --git a/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js b/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js index 72cb084795b3..2ce34dab7f81 100644 --- a/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js +++ b/ui/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js @@ -23,7 +23,10 @@ const mapStateToProps = (state, ownProps) => { : ownProps.match.params.id; const contact = - getAddressBookEntry(state, address) || state.metamask.identities[address]; + getAddressBookEntry(state, address) || + state.metamask.internalAccounts.accounts.find( + (account) => account.address.toLowerCase() === address, + ); const { memo, name } = contact || {}; const { chainId } = getProviderConfig(state); diff --git a/ui/pages/settings/contact-list-tab/view-contact/view-contact.container.js b/ui/pages/settings/contact-list-tab/view-contact/view-contact.container.js index 275273d0af38..2cb9e240f46f 100644 --- a/ui/pages/settings/contact-list-tab/view-contact/view-contact.container.js +++ b/ui/pages/settings/contact-list-tab/view-contact/view-contact.container.js @@ -19,7 +19,10 @@ const mapStateToProps = (state, ownProps) => { : ownProps.match.params.id; const contact = - getAddressBookEntry(state, address) || state.metamask.identities[address]; + getAddressBookEntry(state, address) || + state.metamask.internalAccounts.accounts.find( + (account) => account.address.toLowerCase() === address, + ); const { memo, name } = contact || {}; return { diff --git a/ui/pages/settings/settings-tab/settings-tab.container.js b/ui/pages/settings/settings-tab/settings-tab.container.js index 0888670daa7e..75a21d16f126 100644 --- a/ui/pages/settings/settings-tab/settings-tab.container.js +++ b/ui/pages/settings/settings-tab/settings-tab.container.js @@ -21,8 +21,10 @@ const mapStateToProps = (state, ownProps) => { nativeCurrency, useBlockie, currentLocale, - selectedAddress, + internalAccounts: { accounts, selectedAccount }, } = metamask; + + const { address: selectedAddress } = accounts[selectedAccount]; const { useNativeCurrencyAsPrimaryCurrency, hideZeroBalanceTokens } = getPreferences(state); diff --git a/ui/pages/settings/snaps/view-snap/view-snap.js b/ui/pages/settings/snaps/view-snap/view-snap.js index d383974977d4..d6f50e21821b 100644 --- a/ui/pages/settings/snaps/view-snap/view-snap.js +++ b/ui/pages/settings/snaps/view-snap/view-snap.js @@ -17,6 +17,8 @@ import { import SnapAuthorshipExpanded from '../../../../components/app/snaps/snap-authorship-expanded'; import Box from '../../../../components/ui/box'; import SnapRemoveWarning from '../../../../components/app/snaps/snap-remove-warning'; +import KeyringSnapRemovalWarning from '../../../../components/app/snaps/keyring-snap-removal-warning'; +import KeyringSnapRemovalResult from '../../../../components/app/snaps/keyring-snap-removal-result'; import ConnectedSitesList from '../../../../components/app/connected-sites-list'; import { SNAPS_LIST_ROUTE } from '../../../../helpers/constants/routes'; @@ -31,6 +33,7 @@ import { getPermissions, getPermissionSubjects, getTargetSubjectMetadata, + getInternalAccounts, } from '../../../../selectors'; import { getSnapName } from '../../../../helpers/utils/util'; import { Text } from '../../../../components/component-library'; @@ -38,6 +41,12 @@ import SnapPermissionsList from '../../../../components/app/snaps/snap-permissio import { SnapDelineator } from '../../../../components/app/snaps/snap-delineator'; import { DelineatorType } from '../../../../helpers/constants/snaps'; +const KeyringSnapRemovalResultStatus = { + Success: 'success', + Failed: 'failed', + None: 'none', +}; + function ViewSnap() { const t = useI18nContext(); const history = useHistory(); @@ -50,8 +59,12 @@ function ViewSnap() { const snap = Object.entries(snaps) .map(([_, snapState]) => snapState) .find((snapState) => snapState.id === decodedSnapId); + const internalAccounts = useSelector(getInternalAccounts); const [isShowingRemoveWarning, setIsShowingRemoveWarning] = useState(false); + const [removeKeyringSnapResult, setRemoveKeyringSnapResult] = useState({ + result: KeyringSnapRemovalResultStatus.None, + }); const [isDescriptionOpen, setIsDescriptionOpen] = useState(false); const [isOverflowing, setIsOverflowing] = useState(false); @@ -79,6 +92,15 @@ function ViewSnap() { const targetSubjectMetadata = useSelector((state) => getTargetSubjectMetadata(state, snap?.id), ); + const isKeyringSnap = Boolean( + subjects[snap?.id]?.permissions?.snap_manageAccounts, + ); + const keyringAccounts = internalAccounts.filter( + (internalAccount) => + isKeyringSnap && + internalAccount.metadata.keyring.type === 'Snap Keyring' && + internalAccount.metadata.snap.id === snap?.id, + ); const dispatch = useDispatch(); const onDisconnect = (connectedOrigin, snapId) => { @@ -190,13 +212,48 @@ function ViewSnap() { setIsShowingRemoveWarning(false)} onSubmit={async () => { await dispatch(removeSnap(snap.id)); }} snapName={snapName} /> + setIsShowingRemoveWarning(false)} + onClose={() => setIsShowingRemoveWarning(false)} + onBack={() => setIsShowingRemoveWarning(false)} + onSubmit={async () => { + try { + await dispatch(removeSnap(snap.id)); + setRemoveKeyringSnapResult({ + result: KeyringSnapRemovalResultStatus.Success, + }); + } catch (e) { + setRemoveKeyringSnapResult({ + result: KeyringSnapRemovalResultStatus.Failed, + }); + } finally { + setIsShowingRemoveWarning(false); + } + }} + isOpen={isShowingRemoveWarning && isKeyringSnap} + /> + + setRemoveKeyringSnapResult({ + result: KeyringSnapRemovalResultStatus.None, + }) + } + /> diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index e95364bd70d8..93c6fae9f090 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -55,7 +55,7 @@ import { } from '../../../ducks/swaps/swaps'; import { conversionRateSelector, - getSelectedAccount, + getSelectedInternalAccountWithBalance, getCurrentCurrency, getTokenExchangeRates, getSwapsDefaultToken, @@ -199,7 +199,10 @@ export default function ReviewQuote({ setReceiveToAmount }) { const swapsUserFeeLevel = useSelector(getSwapsUserFeeLevel); const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); const memoizedTokenConversionRates = useEqualityCheck(tokenConversionRates); - const { balance: ethBalance } = useSelector(getSelectedAccount, shallowEqual); + const { balance: ethBalance } = useSelector( + getSelectedInternalAccountWithBalance, + shallowEqual, + ); const conversionRate = useSelector(conversionRateSelector); const USDConversionRate = useSelector(getUSDConversionRate); const isMultiLayerFeeNetwork = useSelector(getIsMultiLayerFeeNetwork); diff --git a/ui/pages/swaps/view-quote/view-quote.js b/ui/pages/swaps/view-quote/view-quote.js index 4be72b2c6425..5c8ca9decfb0 100644 --- a/ui/pages/swaps/view-quote/view-quote.js +++ b/ui/pages/swaps/view-quote/view-quote.js @@ -55,7 +55,7 @@ import { } from '../../../ducks/swaps/swaps'; import { conversionRateSelector, - getSelectedAccount, + getSelectedInternalAccountWithBalance, getCurrentCurrency, getTokenExchangeRates, getSwapsDefaultToken, @@ -165,7 +165,10 @@ export default function ViewQuote() { const swapsUserFeeLevel = useSelector(getSwapsUserFeeLevel); const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); const memoizedTokenConversionRates = useEqualityCheck(tokenConversionRates); - const { balance: ethBalance } = useSelector(getSelectedAccount, shallowEqual); + const { balance: ethBalance } = useSelector( + getSelectedInternalAccountWithBalance, + shallowEqual, + ); const conversionRate = useSelector(conversionRateSelector); const USDConversionRate = useSelector(getUSDConversionRate); const isMultiLayerFeeNetwork = useSelector(getIsMultiLayerFeeNetwork); diff --git a/ui/pages/token-allowance/token-allowance.js b/ui/pages/token-allowance/token-allowance.js index 2bae6f067205..73f39338621b 100644 --- a/ui/pages/token-allowance/token-allowance.js +++ b/ui/pages/token-allowance/token-allowance.js @@ -368,7 +368,7 @@ export default function TokenAllowance({ { const accountAddress = '0x1'; const state = buildState({ metamask: { - identities: { - [accountAddress]: { - address: accountAddress, + internalAccounts: { + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Custody Account A', + keyring: { + type: 'Custody', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + code: '0x', + balance: '0x47c9d71831c76efe', + nonce: '0x1b', + address: accountAddress, + }, }, }, - keyrings: [ - { - type: 'Custody', - accounts: [accountAddress], - }, - ], custodianSupportedChains: { [accountAddress]: { supportedChains: ['1', '2', '3'], }, }, - selectedAddress: accountAddress, providerConfig: { chainId: toHex(1), }, @@ -192,23 +216,32 @@ describe('Institutional selectors', () => { const accountAddress = '0x1'; const state = buildState({ metamask: { - identities: { - [accountAddress]: { - address: accountAddress, + internalAccounts: { + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Custody Account A', + keyring: { + type: 'Custody', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + code: '0x', + balance: '0x47c9d71831c76efe', + nonce: '0x1b', + address: accountAddress, + }, }, }, - keyrings: [ - { - type: 'Custody', - accounts: [accountAddress], - }, - ], custodianSupportedChains: { [accountAddress]: { supportedChains: ['4'], }, }, - selectedAddress: accountAddress, providerConfig: { chainId: toHex(1), }, @@ -224,11 +257,6 @@ describe('Institutional selectors', () => { const accountAddress = '0x1'; const state = buildState({ metamask: { - identities: { - [accountAddress]: { - address: accountAddress, - }, - }, keyrings: [ { type: 'SomethingElse', @@ -240,7 +268,24 @@ describe('Institutional selectors', () => { supportedChains: ['4'], }, }, - selectedAddress: accountAddress, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: accountAddress, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, providerConfig: { chainId: toHex(1), }, @@ -252,29 +297,26 @@ describe('Institutional selectors', () => { expect(isSupported).toBe(true); }); - it('throws an error if selectedIdentity is null', () => { - const state = buildState({ - metamask: { - identities: {}, - keyrings: [], - custodianSupportedChains: {}, - selectedAddress: null, - providerConfig: {}, - }, - }); - - expect(() => getIsCustodianSupportedChain(state)).toThrow( - 'Invalid state', - ); - }); - it('throws an error if accountType is null', () => { const accountAddress = '0x1'; const state = buildState({ metamask: { - identities: { - [accountAddress]: { - address: accountAddress, + internalAccounts: { + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Custody Account A', + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + code: '0x', + balance: '0x47c9d71831c76efe', + nonce: '0x1b', + address: accountAddress, + }, }, }, keyrings: [], @@ -293,9 +335,25 @@ describe('Institutional selectors', () => { const accountAddress = '0x1'; const state = buildState({ metamask: { - identities: { - [accountAddress]: { - address: accountAddress, + internalAccounts: { + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Custody Account A', + keyring: { + type: 'Custody', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + code: '0x', + balance: '0x47c9d71831c76efe', + nonce: '0x1b', + address: accountAddress, + }, }, }, keyrings: [ @@ -319,9 +377,25 @@ describe('Institutional selectors', () => { const accountAddress = '0x1'; const state = buildState({ metamask: { - identities: { - [accountAddress]: { - address: accountAddress, + internalAccounts: { + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Custody Account A', + keyring: { + type: 'Custody', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + code: '0x', + balance: '0x47c9d71831c76efe', + nonce: '0x1b', + address: accountAddress, + }, }, }, keyrings: [ @@ -349,9 +423,25 @@ describe('Institutional selectors', () => { const accountAddress = '0x1'; const state = buildState({ metamask: { - identities: { - [accountAddress]: { - address: accountAddress, + internalAccounts: { + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Custody Account A', + keyring: { + type: 'Custody', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + code: '0x', + balance: '0x47c9d71831c76efe', + nonce: '0x1b', + address: accountAddress, + }, }, }, keyrings: [ @@ -381,9 +471,25 @@ describe('Institutional selectors', () => { const accountAddress = '0x1'; const state = buildState({ metamask: { - identities: { - [accountAddress]: { - address: accountAddress, + internalAccounts: { + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Custody Account A', + keyring: { + type: 'Custody', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + code: '0x', + balance: '0x47c9d71831c76efe', + nonce: '0x1b', + address: accountAddress, + }, }, }, keyrings: [ @@ -413,9 +519,25 @@ describe('Institutional selectors', () => { const accountAddress = '0x1'; const state = buildState({ metamask: { - identities: { - [accountAddress]: { - address: accountAddress, + internalAccounts: { + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Custody Account A', + keyring: { + type: 'Custody', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + code: '0x', + balance: '0x47c9d71831c76efe', + nonce: '0x1b', + address: '0x5Ab19e7091dD208F352F8E727B6DCC6F8aBB6275', + }, }, }, keyrings: [ diff --git a/ui/selectors/nonce-sorted-transactions-selector.test.js b/ui/selectors/nonce-sorted-transactions-selector.test.js index 81594efa090e..9af5ff457d66 100644 --- a/ui/selectors/nonce-sorted-transactions-selector.test.js +++ b/ui/selectors/nonce-sorted-transactions-selector.test.js @@ -1,4 +1,5 @@ import { head, last } from 'lodash'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { CHAIN_IDS } from '../../shared/constants/network'; import { TransactionStatus, @@ -80,6 +81,24 @@ const getStateTree = ({ }, unapprovedMsgs, selectedAddress: SENDERS.ONE, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: SENDERS.ONE, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, featureFlags: {}, transactions: [...incomingTxList, ...txList], incomingTransactionsPreferences: {}, diff --git a/ui/selectors/permissions.js b/ui/selectors/permissions.js index c14b50bbf4a8..71b5787692c6 100644 --- a/ui/selectors/permissions.js +++ b/ui/selectors/permissions.js @@ -5,9 +5,10 @@ import { WALLET_SNAP_PERMISSION_KEY } from '@metamask/rpc-methods'; import { CaveatTypes } from '../../shared/constants/permissions'; import { getApprovalRequestsByType } from './approvals'; import { + getInternalAccounts, getMetaMaskAccountsOrdered, getOriginOfCurrentTab, - getSelectedAddress, + getSelectedInternalAccount, getSubjectMetadata, getTargetSubjectMetadata, } from '.'; @@ -77,7 +78,7 @@ export function getPermittedAccountsByOrigin(state) { * @returns {Array} An array of connected subject objects. */ export function getConnectedSubjectsForSelectedAddress(state) { - const { selectedAddress } = state.metamask; + const { address: selectedAddress } = getSelectedInternalAccount(state); const subjects = getPermissionSubjects(state); const subjectMetadata = getSubjectMetadata(state); @@ -227,23 +228,20 @@ function subjectSelector(state, origin) { } export function getAccountToConnectToActiveTab(state) { - const selectedAddress = getSelectedAddress(state); + const selectedAccount = getSelectedInternalAccount(state); + const numberOfAccounts = getInternalAccounts(state).length; const connectedAccounts = getPermittedAccountsForCurrentTab(state); - const { - metamask: { identities }, - } = state; - const numberOfAccounts = Object.keys(identities).length; - if ( connectedAccounts.length && connectedAccounts.length !== numberOfAccounts ) { if ( - connectedAccounts.findIndex((address) => address === selectedAddress) === - -1 + connectedAccounts.findIndex( + (address) => address === selectedAccount.address, + ) === -1 ) { - return identities[selectedAddress]; + return selectedAccount; } } @@ -266,7 +264,10 @@ export function getOrderedConnectedAccountsForActiveTab(state) { .filter((account) => connectedAccounts.includes(account.address)) .map((account) => ({ ...account, - lastActive: permissionHistoryByAccount?.[account.address], + metadata: { + ...account.metadata, + lastActive: permissionHistoryByAccount?.[account.address], + }, })) .sort( ({ lastSelected: lastSelectedA }, { lastSelected: lastSelectedB }) => { diff --git a/ui/selectors/permissions.test.js b/ui/selectors/permissions.test.js index d531bce2f5e4..d003e92de8cd 100644 --- a/ui/selectors/permissions.test.js +++ b/ui/selectors/permissions.test.js @@ -1,3 +1,4 @@ +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { CHAIN_IDS } from '../../shared/constants/network'; import { getConnectedSubjectsForSelectedAddress, @@ -11,7 +12,24 @@ describe('selectors', () => { it('should return the list of connected subjects when there is 1 connected account', () => { const mockState = { metamask: { - selectedAddress: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Really Long Name That Should Be Truncated', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, subjectMetadata: { 'peepeth.com': { iconUrl: 'https://peepeth.com/favicon-32x32.png', @@ -78,7 +96,24 @@ describe('selectors', () => { it('should return the list of connected subjects when there are 2 connected accounts', () => { const mockState = { metamask: { - selectedAddress: '0x7250739de134d33ec7ab1ee592711e15098c9d2d', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Really Long Name That Should Be Truncated', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, subjectMetadata: { 'peepeth.com': { iconUrl: 'https://peepeth.com/favicon-32x32.png', @@ -210,30 +245,79 @@ describe('selectors', () => { }, }, }, - identities: { - '0x7250739de134d33ec7ab1ee592711e15098c9d2d': { - address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d', - name: 'Really Long Name That Should Be Truncated', - }, - '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': { - address: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', - lastSelected: 1586359844192, - name: 'Account 1', - }, - '0xb3958fb96c8201486ae20be1d5c9f58083df343a': { - lastSelected: 1586359844193, - address: '0xb3958fb96c8201486ae20be1d5c9f58083df343a', - name: 'Account 2', - }, - '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - lastSelected: 1586359844192, - name: 'Account 3', - }, - '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4': { - address: '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', - name: 'Account 4', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Really Long Name That Should Be Truncated', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '07c2cfec-36c9-46c4-8115-3836d3ac9047': { + address: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Account 1', + lastSelected: 1586359844192, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '15e69915-2a1a-4019-93b3-916e11fd432f': { + address: '0xb3958fb96c8201486ae20be1d5c9f58083df343a', + id: '15e69915-2a1a-4019-93b3-916e11fd432f', + metadata: { + name: 'Account 2', + lastActive: 1586359844192, + lastSelected: 1586359844193, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '784225f4-d30b-4e77-a900-c8bbce735b88': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: '784225f4-d30b-4e77-a900-c8bbce735b88', + metadata: { + name: 'Account 3', + lastSelected: 1586359844192, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + 'f9305241-c50f-4725-ad0f-cbd3f24ac7ab': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + id: 'f9305241-c50f-4725-ad0f-cbd3f24ac7ab', + metadata: { + name: 'Account 4', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, keyrings: [ { @@ -266,37 +350,82 @@ describe('selectors', () => { it('should return connected accounts sorted by last selected, then by keyring controller order', () => { expect(getOrderedConnectedAccountsForActiveTab(mockState)).toStrictEqual([ { - address: '0xb3958fb96c8201486ae20be1d5c9f58083df343a', + address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d', balance: undefined, - name: 'Account 2', - lastActive: 1586359844192, - lastSelected: 1586359844193, + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Really Long Name That Should Be Truncated', + lastActive: 1586359844192, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, { address: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', balance: undefined, - name: 'Account 1', - lastActive: 1586359844192, - lastSelected: 1586359844192, + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Account 1', + lastActive: 1586359844192, + lastSelected: 1586359844192, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + address: '0xb3958fb96c8201486ae20be1d5c9f58083df343a', balance: undefined, - name: 'Account 3', - lastActive: 1586359844192, - lastSelected: 1586359844192, + id: '15e69915-2a1a-4019-93b3-916e11fd432f', + metadata: { + name: 'Account 2', + lastActive: 1586359844192, + lastSelected: 1586359844193, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, { - address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d', + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', balance: undefined, - name: 'Really Long Name That Should Be Truncated', - lastActive: 1586359844192, + id: '784225f4-d30b-4e77-a900-c8bbce735b88', + metadata: { + name: 'Account 3', + lastActive: 1586359844192, + lastSelected: 1586359844192, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, { - address: '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', balance: undefined, - name: 'Account 4', - lastActive: 1586359844192, + id: 'f9305241-c50f-4725-ad0f-cbd3f24ac7ab', + metadata: { + name: 'Account 4', + lastActive: 1586359844192, + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, ]); }); @@ -338,19 +467,49 @@ describe('selectors', () => { url: 'https://remix.ethereum.org/', }, metamask: { - identities: { - '0x7250739de134d33ec7ab1ee592711e15098c9d2d': { - address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d', - name: 'Really Long Name That Should Be Truncated', - }, - '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': { - address: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', - name: 'Account 1', - }, - '0xb3958fb96c8201486ae20be1d5c9f58083df343a': { - address: '0xb3958fb96c8201486ae20be1d5c9f58083df343a', - name: 'Account 2', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Really Long Name That Should Be Truncated', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '07c2cfec-36c9-46c4-8115-3836d3ac9047': { + address: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', + id: '07c2cfec-36c9-46c4-8115-3836d3ac9047', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + '15e69915-2a1a-4019-93b3-916e11fd432f': { + address: '0xb3958fb96c8201486ae20be1d5c9f58083df343a', + id: '15e69915-2a1a-4019-93b3-916e11fd432f', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, subjects: { 'https://remix.ethereum.org': { diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 1a0bb0ad229a..b85093ce54c9 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -77,7 +77,6 @@ import { isEIP1559Network, getLedgerTransportType, isAddressLedger, - findKeyringForAddress, } from '../ducks/metamask/metamask'; import { getLedgerWebHidConnectedStatus, @@ -160,16 +159,12 @@ export function hasUnsignedQRHardwareTransaction(state) { return false; } const { from } = txParams; - const { keyrings } = state.metamask; - const qrKeyring = keyrings.find((kr) => kr.type === KeyringType.qr); - if (!qrKeyring) { + const internalAccount = getFirstInternalAccountByAddress(state, from); + + if (!internalAccount) { return false; } - return Boolean( - qrKeyring.accounts.find( - (account) => account.toLowerCase() === from.toLowerCase(), - ), - ); + return Boolean(internalAccount.metadata.keyring.type === KeyringType.qr); } export function hasUnsignedQRHardwareMessage(state) { @@ -178,35 +173,30 @@ export function hasUnsignedQRHardwareMessage(state) { return false; } const { from } = msgParams; - const { keyrings } = state.metamask; - const qrKeyring = keyrings.find((kr) => kr.type === KeyringType.qr); - if (!qrKeyring) { + const internalAccount = getFirstInternalAccountByAddress(state, from); + + if (!internalAccount) { return false; } + switch (type) { case MESSAGE_TYPE.ETH_SIGN_TYPED_DATA: case MESSAGE_TYPE.ETH_SIGN: case MESSAGE_TYPE.PERSONAL_SIGN: - return Boolean( - qrKeyring.accounts.find( - (account) => account.toLowerCase() === from.toLowerCase(), - ), - ); + return Boolean(internalAccount.metadata.keyring.type === KeyringType.qr); default: return false; } } export function getCurrentKeyring(state) { - const identity = getSelectedIdentity(state); + const account = getSelectedInternalAccount(state); - if (!identity) { + if (!account) { return null; } - const keyring = findKeyringForAddress(state, identity.address); - - return keyring; + return account.metadata.keyring; } /** @@ -279,6 +269,10 @@ export function getAccountTypeForKeyring(keyring) { return 'hardware'; case KeyringType.imported: return 'imported'; + ///: BEGIN:ONLY_INCLUDE_IN(keyring-snaps) + case KeyringType.snap: + return 'snap'; + ///: END:ONLY_INCLUDE_IN default: return 'default'; } @@ -302,64 +296,93 @@ export function deprecatedGetCurrentNetworkId(state) { * Get MetaMask accounts, including account name and balance. */ export const getMetaMaskAccounts = createSelector( - getMetaMaskIdentities, + getInternalAccounts, getMetaMaskAccountBalances, getMetaMaskCachedBalances, - (identities, balances, cachedBalances) => - Object.keys(identities).reduce((accounts, address) => { - // TODO: mix in the identity state here as well, consolidating this - // selector with `accountsWithSendEtherInfoSelector` - let account = {}; - - if (balances[address]) { - account = { - ...account, - ...balances[address], - }; - } - - if (account.balance === null || account.balance === undefined) { - account = { - ...account, - balance: cachedBalances && cachedBalances[address], + (internalAccounts, balances, cachedBalances) => { + return internalAccounts.reduce((selectedAccounts, account) => { + // TODO: consolidating this selector with `accountsWithSendEtherInfoSelector` + + if (balances[account.address]?.balance) { + return { + ...selectedAccounts, + [account.id]: { + ...account, + ...balances[account.address], + balance: balances[account.address]?.balance, + }, }; } + // use cache balance if it is not in balances return { - ...accounts, - [address]: account, + ...selectedAccounts, + [account.id]: { + ...account, + ...balances[account.address], + balance: cachedBalances && cachedBalances[account.address], + }, }; - }, {}), + }, {}); + }, ); export function getSelectedAddress(state) { return state.metamask.selectedAddress; } -export function getSelectedIdentity(state) { - const selectedAddress = getSelectedAddress(state); - const { identities } = state.metamask; +export function getSelectedInternalAccount(state) { + const accountId = state.metamask.internalAccounts.selectedAccount; + return state.metamask.internalAccounts.accounts[accountId]; +} + +export function getSelectedInternalAccountWithBalance(state) { + const selectedAccount = getSelectedInternalAccount(state); + const rawAccount = getMetaMaskAccountBalances(state)[selectedAccount.address]; - return identities[selectedAddress]; + const selectedAccountWithBalance = { + ...selectedAccount, + balance: rawAccount ? rawAccount.balance : 0, + }; + + return selectedAccountWithBalance; } -export function getNumberOfTokens(state) { - const { tokens } = state.metamask; - return tokens ? tokens.length : 0; +export function getInternalAccountWithBalanceByAddress(state, address) { + const accountsWithBalance = getMetaMaskAccounts(state); + return Object.values(accountsWithBalance).find((account) => + isEqualCaseInsensitive(account.address, address), + ); } -export function getMetaMaskKeyrings(state) { - return state.metamask.keyrings; +export function getInternalAccounts(state) { + return Object.values(state.metamask.internalAccounts.accounts); } -/** - * Get identity state. - * - * @param {object} state - Redux state - * @returns {object} A map of account addresses to identities (which includes the account name) - */ -export function getMetaMaskIdentities(state) { - return state.metamask.identities; +export function getInternalAccount(state, accountId) { + return state.metamask.internalAccounts.accounts[accountId]; +} + +export function getInternalAccountsSortedByKeyring(state) { + const accounts = getInternalAccounts(state); + + return accounts.sort((previousAccount, currentAccount) => { + // sort accounts by keyring type in alphabetical order + const previousKeyringType = previousAccount.metadata.keyring.type; + const currentKeyringType = currentAccount.metadata.keyring.type; + if (previousKeyringType < currentKeyringType) { + return -1; + } + if (previousKeyringType > currentKeyringType) { + return 1; + } + return 0; + }); +} + +export function getNumberOfTokens(state) { + const { tokens } = state.metamask; + return tokens ? tokens.length : 0; } /** @@ -386,17 +409,17 @@ export function getMetaMaskCachedBalances(state) { } /** - * Get ordered (by keyrings) accounts with identity and balance + * Get ordered (by keyrings) accounts with internal account and balance */ export const getMetaMaskAccountsOrdered = createSelector( - getMetaMaskKeyrings, - getMetaMaskIdentities, + getInternalAccountsSortedByKeyring, getMetaMaskAccounts, - (keyrings, identities, accounts) => - keyrings - .reduce((list, keyring) => list.concat(keyring.accounts), []) - .filter((address) => Boolean(identities[address])) - .map((address) => ({ ...identities[address], ...accounts[address] })), + (internalAccounts, accounts) => { + return internalAccounts.map((internalAccount) => ({ + ...internalAccount, + ...accounts[internalAccount.id], + })); + }, ); export const getMetaMaskAccountsConnected = createSelector( @@ -406,8 +429,8 @@ export const getMetaMaskAccountsConnected = createSelector( ); export function isBalanceCached(state) { - const selectedAccountBalance = - getMetaMaskAccountBalances(state)[getSelectedAddress(state)]?.balance; + const { balance: selectedAccountBalance } = + getSelectedInternalAccountWithBalance(state); const cachedBalance = getSelectedAccountCachedBalance(state); return Boolean(!selectedAccountBalance && cachedBalance); @@ -415,15 +438,19 @@ export function isBalanceCached(state) { export function getSelectedAccountCachedBalance(state) { const cachedBalances = getMetaMaskCachedBalances(state); - const selectedAddress = getSelectedAddress(state); + const { address: selectedAddress } = getSelectedInternalAccount(state); + return cachedBalances?.[selectedAddress]; } export function getSelectedAccount(state) { const accounts = getMetaMaskAccounts(state); - const selectedAddress = getSelectedAddress(state); + const selectedAccount = getSelectedInternalAccount(state); - return accounts[selectedAddress]; + return { + ...selectedAccount, + ...accounts[selectedAccount.id], + }; } export function getTargetAccount(state, targetAddress) { @@ -449,9 +476,7 @@ export function getEnsResolutionByAddress(state, address) { const entry = getAddressBookEntry(state, address) || - Object.values(state.metamask.identities).find((identity) => - isEqualCaseInsensitive(identity.address, address), - ); + getFirstInternalAccountByAddress(state, address); return entry?.name || ''; } @@ -467,17 +492,24 @@ export function getAddressBookEntry(state, address) { export function getAddressBookEntryOrAccountName(state, address) { const entry = getAddressBookEntry(state, address) || - Object.values(state.metamask.identities).find((identity) => - isEqualCaseInsensitive(identity.address, address), + Object.values(getInternalAccounts(state)).find((internalAccount) => + isEqualCaseInsensitive(internalAccount.address, address), ); return entry && entry.name !== '' ? entry.name : address; } -export function getAccountName(identities, address) { - const entry = Object.values(identities).find((identity) => - isEqualCaseInsensitive(identity.address, address), +export function getAccountName(accounts, accountAddress) { + const account = accounts.find((internalAccount) => + isEqualCaseInsensitive(internalAccount.address, accountAddress), ); - return entry && entry.name !== '' ? entry.name : ''; + return account && account.metadata.name !== '' ? account.metadata.name : ''; +} + +export function getFirstInternalAccountByAddress(state, address) { + const account = getInternalAccounts(state).find((internalAccount) => + isEqualCaseInsensitive(internalAccount.address, address), + ); + return account; } export function getMetadataContractName(state, address) { @@ -490,11 +522,14 @@ export function getMetadataContractName(state, address) { export function accountsWithSendEtherInfoSelector(state) { const accounts = getMetaMaskAccounts(state); - const identities = getMetaMaskIdentities(state); + const internalAccounts = getInternalAccounts(state); - const accountsWithSendEtherInfo = Object.entries(identities).map( - ([key, identity]) => { - return { ...identity, ...accounts[key] }; + const accountsWithSendEtherInfo = Object.values(internalAccounts).map( + (internalAccount) => { + return { + ...internalAccount, + ...accounts[internalAccount.id], + }; }, ); @@ -503,7 +538,7 @@ export function accountsWithSendEtherInfoSelector(state) { export function getAccountsWithLabels(state) { return getMetaMaskAccountsOrdered(state).map( - ({ address, name, balance }) => ({ + ({ address, balance, metadata: { name } }) => ({ address, addressLabel: `${ name.length < TRUNCATED_NAME_CHAR_LIMIT @@ -517,7 +552,7 @@ export function getAccountsWithLabels(state) { } export function getCurrentAccountWithSendEtherInfo(state) { - const currentAddress = getSelectedAddress(state); + const { address: currentAddress } = getSelectedInternalAccount(state); const accounts = accountsWithSendEtherInfoSelector(state); return getAccountByAddress(accounts, currentAddress); @@ -858,11 +893,6 @@ export function getShowWhatsNewPopup(state) { return state.appState.showWhatsNewPopup; } -export const getMemoizedMetaMaskIdentities = createDeepEqualSelector( - getMetaMaskIdentities, - (identities) => identities, -); - export const getMemoizedAddressBook = createDeepEqualSelector( getAddressBook, (addressBook) => addressBook, @@ -1478,8 +1508,10 @@ export function getIsDynamicTokenListAvailable(state) { */ export function getDetectedTokensInCurrentNetwork(state) { const currentChainId = getCurrentChainId(state); - const selectedAddress = getSelectedAddress(state); - return state.metamask.allDetectedTokens?.[currentChainId]?.[selectedAddress]; + const selectedAccount = getSelectedInternalAccount(state); + return state.metamask.allDetectedTokens?.[currentChainId]?.[ + selectedAccount.address + ]; } /** @@ -1630,7 +1662,11 @@ export function getAllAccountsOnNetworkAreEmpty(state) { export function getShouldShowSeedPhraseReminder(state) { const { tokens, seedPhraseBackedUp, dismissSeedBackUpReminder } = state.metamask; - const accountBalance = getCurrentEthBalance(state) ?? 0; + + // if there is no account, we don't need to show the seed phrase reminder + const accountBalance = getSelectedInternalAccount(state) + ? getCurrentEthBalance(state) + : 0; return ( seedPhraseBackedUp === false && (parseInt(accountBalance, 16) > 0 || tokens.length > 0) && diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index bfa8c412f2c6..a2c6cdd736b7 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -1,4 +1,6 @@ +import { deepClone } from '@metamask/snaps-utils'; import { ApprovalType, NetworkType } from '@metamask/controller-utils'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import mockState from '../../test/data/mock-state.json'; import { KeyringType } from '../../shared/constants/keyring'; import { @@ -17,6 +19,15 @@ jest.mock('../../shared/modules/network.utils', () => { }; }); +const modifyStateWithHWKeyring = (keyring) => { + const modifiedState = deepClone(mockState); + modifiedState.metamask.internalAccounts.accounts[ + modifiedState.metamask.internalAccounts.selectedAccount + ].metadata.keyring.type = keyring; + + return modifiedState; +}; + describe('Selectors', () => { describe('#getSelectedAddress', () => { it('returns undefined if selectedAddress is undefined', () => { @@ -433,46 +444,61 @@ describe('Selectors', () => { describe('#isHardwareWallet', () => { it('returns false if it is not a HW wallet', () => { - mockState.metamask.keyrings[0].type = KeyringType.imported; - expect(selectors.isHardwareWallet(mockState)).toBe(false); + const mockStateWithImported = modifyStateWithHWKeyring( + KeyringType.imported, + ); + expect(selectors.isHardwareWallet(mockStateWithImported)).toBe(false); }); it('returns true if it is a Ledger HW wallet', () => { - mockState.metamask.keyrings[0].type = KeyringType.ledger; - expect(selectors.isHardwareWallet(mockState)).toBe(true); + const mockStateWithLedger = modifyStateWithHWKeyring(KeyringType.ledger); + expect(selectors.isHardwareWallet(mockStateWithLedger)).toBe(true); }); it('returns true if it is a Trezor HW wallet', () => { - mockState.metamask.keyrings[0].type = KeyringType.trezor; - expect(selectors.isHardwareWallet(mockState)).toBe(true); + const mockStateWithTrezor = modifyStateWithHWKeyring(KeyringType.trezor); + expect(selectors.isHardwareWallet(mockStateWithTrezor)).toBe(true); }); }); describe('#getHardwareWalletType', () => { it('returns undefined if it is not a HW wallet', () => { - mockState.metamask.keyrings[0].type = KeyringType.imported; - expect(selectors.getHardwareWalletType(mockState)).toBeUndefined(); + const mockStateWithImported = modifyStateWithHWKeyring( + KeyringType.imported, + ); + expect( + selectors.getHardwareWalletType(mockStateWithImported), + ).toBeUndefined(); }); it('returns "Ledger Hardware" if it is a Ledger HW wallet', () => { - mockState.metamask.keyrings[0].type = KeyringType.ledger; - expect(selectors.getHardwareWalletType(mockState)).toBe( + const mockStateWithLedger = modifyStateWithHWKeyring(KeyringType.ledger); + expect(selectors.getHardwareWalletType(mockStateWithLedger)).toBe( KeyringType.ledger, ); }); it('returns "Trezor Hardware" if it is a Trezor HW wallet', () => { - mockState.metamask.keyrings[0].type = KeyringType.trezor; - expect(selectors.getHardwareWalletType(mockState)).toBe( + const mockStateWithTrezor = modifyStateWithHWKeyring(KeyringType.trezor); + expect(selectors.getHardwareWalletType(mockStateWithTrezor)).toBe( KeyringType.trezor, ); }); }); - it('returns selected identity', () => { - expect(selectors.getSelectedIdentity(mockState)).toStrictEqual({ + it('returns selected internal account', () => { + expect(selectors.getSelectedInternalAccount(mockState)).toStrictEqual({ address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - name: 'Test Account', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }); }); @@ -563,7 +589,9 @@ describe('Selectors', () => { expect(accountsWithSendEther[0].address).toStrictEqual( '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', ); - expect(accountsWithSendEther[0].name).toStrictEqual('Test Account'); + expect(accountsWithSendEther[0].metadata.name).toStrictEqual( + 'Test Account', + ); }); it('returns selected account with balance, address, and name from accountsWithSendEtherInfoSelector', () => { @@ -575,7 +603,9 @@ describe('Selectors', () => { expect(currentAccountwithSendEther.address).toStrictEqual( '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', ); - expect(currentAccountwithSendEther.name).toStrictEqual('Test Account'); + expect(currentAccountwithSendEther.metadata.name).toStrictEqual( + 'Test Account', + ); }); it('#getGasIsLoading', () => { diff --git a/ui/selectors/transactions.js b/ui/selectors/transactions.js index a1827f2c0443..39ca01f5bafb 100644 --- a/ui/selectors/transactions.js +++ b/ui/selectors/transactions.js @@ -16,7 +16,7 @@ import { getProviderConfig } from '../ducks/metamask/metamask'; import { getCurrentChainId, deprecatedGetCurrentNetworkId, - getSelectedAddress, + getSelectedInternalAccount, } from './selectors'; import { hasPendingApprovals, getApprovalRequestsByType } from './approvals'; import { createDeepEqualSelector } from './util'; @@ -69,7 +69,7 @@ export const incomingTxListSelector = createDeepEqualSelector( } const currentNetworkTransactions = getCurrentNetworkTransactions(state); - const selectedAddress = getSelectedAddress(state); + const { address: selectedAddress } = getSelectedInternalAccount(state); return currentNetworkTransactions.filter( (tx) => @@ -103,12 +103,14 @@ export const smartTransactionsListSelector = (state) => })); export const selectedAddressTxListSelector = createSelector( - getSelectedAddress, + getSelectedInternalAccount, getCurrentNetworkTransactions, smartTransactionsListSelector, - (selectedAddress, transactions = [], smTransactions = []) => { + (selectedInternalAccount, transactions = [], smTransactions = []) => { return transactions - .filter(({ txParams }) => txParams.from === selectedAddress) + .filter( + ({ txParams }) => txParams.from === selectedInternalAccount.address, + ) .concat(smTransactions); }, ); diff --git a/ui/selectors/transactions.test.js b/ui/selectors/transactions.test.js index fb6bb629793e..fbc92d0313c7 100644 --- a/ui/selectors/transactions.test.js +++ b/ui/selectors/transactions.test.js @@ -1,4 +1,5 @@ import { ApprovalType } from '@metamask/controller-utils'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { CHAIN_IDS } from '../../shared/constants/network'; import { TransactionStatus } from '../../shared/constants/transaction'; import { @@ -34,6 +35,24 @@ describe('Transaction Selectors', () => { providerConfig: { chainId: '0x5', }, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xAddress', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, }, }; @@ -114,7 +133,24 @@ describe('Transaction Selectors', () => { chainId: CHAIN_IDS.MAINNET, }, featureFlags: {}, - selectedAddress: '0xAddress', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xAddress', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, transactions: [ { id: 0, @@ -177,8 +213,25 @@ describe('Transaction Selectors', () => { nickname: 'mainnet', chainId: CHAIN_IDS.MAINNET, }, - selectedAddress: '0xAddress', featureFlags: {}, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xAddress', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, transactions: [tx1, tx2], }, }; @@ -259,8 +312,25 @@ describe('Transaction Selectors', () => { nickname: 'mainnet', chainId: CHAIN_IDS.MAINNET, }, - selectedAddress: '0xAddress', featureFlags: {}, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xAddress', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, transactions: [submittedTx, unapprovedTx, approvedTx, confirmedTx], }, }; diff --git a/ui/store/actionConstants.test.js b/ui/store/actionConstants.test.js index c03b857eb942..a8493d7e3d82 100644 --- a/ui/store/actionConstants.test.js +++ b/ui/store/actionConstants.test.js @@ -1,16 +1,33 @@ import freeze from 'deep-freeze-strict'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import reducers from '../ducks'; import * as actionConstants from './actionConstants'; describe('Redux actionConstants', () => { describe('SET_ACCOUNT_LABEL', () => { - it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', () => { + it('updates the state.metamask.internalAccounts.accounts[accountId].metadata.name property of the state to the action.value.label', () => { + const accountId = 'foo'; const initialState = { metamask: { - identities: { - foo: { - name: 'bar', + internalAccounts: { + accounts: { + [accountId]: { + foo: { + address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', + id: accountId, + metadata: { + name: 'bar', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, }, + selectedAccount: accountId, }, }, }; @@ -19,16 +36,17 @@ describe('Redux actionConstants', () => { const action = { type: actionConstants.SET_ACCOUNT_LABEL, value: { - account: 'foo', + accountId: 'foo', label: 'baz', }, }; freeze(action); const resultingState = reducers(initialState, action); - expect(resultingState.metamask.identities.foo.name).toStrictEqual( - action.value.label, - ); + expect( + resultingState.metamask.internalAccounts.accounts[accountId].metadata + .name, + ).toStrictEqual(action.value.label); }); }); }); diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index 1c94d42e569b..c9fe64d2716e 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -44,7 +44,7 @@ export const SHOW_SEND_TOKEN_PAGE = 'SHOW_SEND_TOKEN_PAGE'; export const SHOW_PRIVATE_KEY = 'SHOW_PRIVATE_KEY'; export const SET_ACCOUNT_LABEL = 'SET_ACCOUNT_LABEL'; export const CLEAR_ACCOUNT_DETAILS = 'CLEAR_ACCOUNT_DETAILS'; -export const SET_ACCOUNT_DETAILS_ADDRESS = 'SET_ACCOUNT_DETAILS_ADDRESS'; +export const SET_ACCOUNT_DETAILS_ACCOUNT_ID = 'SET_ACCOUNT_DETAILS_ACCOUNT_ID'; // tx conf screen export const COMPLETED_TX = 'COMPLETED_TX'; export const TRANSACTION_ERROR = 'TRANSACTION_ERROR'; diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index 26550166dd4e..2ee4d084032c 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -1,6 +1,7 @@ import sinon from 'sinon'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import enLocale from '../../app/_locales/en/messages.json'; import MetaMaskController from '../../app/scripts/metamask-controller'; import { TransactionStatus } from '../../shared/constants/transaction'; @@ -15,15 +16,29 @@ const middleware = [thunk]; const defaultState = { metamask: { currentLocale: 'test', - selectedAddress: '0xFirstAddress', providerConfig: { chainId: '0x1' }, accounts: { '0xFirstAddress': { balance: '0x0', }, }, - identities: { - '0xFirstAddress': {}, + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xFirstAddress', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, cachedBalances: { '0x1': { @@ -230,7 +245,6 @@ describe('Actions', () => { background.getState.callsFake((cb) => cb(null, { currentLocale: 'test', - selectedAddress: '0xAnotherAddress', providerConfig: { chainId: '0x1', }, @@ -239,14 +253,29 @@ describe('Actions', () => { balance: '0x0', }, }, + internalAccounts: { + accounts: { + '22497cc9-e791-42b8-adef-2f13ef216b86': { + address: '0xAnotherAddress', + id: '22497cc9-e791-42b8-adef-2f13ef216b86', + metadata: { + name: 'Test Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: '22497cc9-e791-42b8-adef-2f13ef216b86', + }, cachedBalances: { '0x1': { '0xAnotherAddress': '0x0', }, }, - identities: { - '0xAnotherAddress': {}, - }, }), ); @@ -265,7 +294,7 @@ describe('Actions', () => { ]; await store.dispatch( - actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc'), + actions.removeAccount('cf8dace4-9439-4bd4-b3a8-88c821c8fcb3'), ); expect(removeAccount.callCount).toStrictEqual(1); const actionTypes = store.getActions().map((action) => action.type); @@ -396,18 +425,21 @@ describe('Actions', () => { describe('#addNewAccount', () => { it('adds a new account', async () => { const store = mockStore({ - metamask: { identities: {}, ...defaultState.metamask }, + metamask: { + internalAccounts: { accounts: {}, selectedAccount: '' }, + ...defaultState.metamask, + }, }); const addNewAccount = background.addNewAccount.callsFake((_, cb) => cb(null, { - addedAccountAddress: '0x123', + accounts: [], }), ); _setBackgroundConnection(background); - await store.dispatch(actions.addNewAccount(1)); + await store.dispatch(actions.addNewAccount()); expect(addNewAccount.callCount).toStrictEqual(1); }); @@ -432,6 +464,40 @@ describe('Actions', () => { expect(store.getActions()).toStrictEqual(expectedActions); }); + + it('adds a new account and sets the name', async () => { + const store = mockStore({ + metamask: { + internalAccounts: { accounts: {}, selectedAccount: '' }, + ...defaultState.metamask, + }, + }); + + const addNewAccount = background.addNewAccount.callsFake((_, cb) => + cb(null, { + accounts: [ + { + address: '0xNewAddress', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb5', + metadata: { + name: 'new name', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + ], + }), + ); + + _setBackgroundConnection(background); + + await store.dispatch(actions.addNewAccount('new name')); + expect(addNewAccount.callCount).toStrictEqual(1); + }); }); describe('#checkHardwareStatus', () => { @@ -776,89 +842,37 @@ describe('Actions', () => { }); }); - describe('#setSelectedAddress', () => { + describe('#setSelectedAccount', () => { afterEach(() => { sinon.restore(); }); - it('calls setSelectedAddress in background', async () => { + it('calls setSelectedAccount in background', async () => { const store = mockStore(); - const setSelectedAddressSpy = sinon.stub().callsFake((_, cb) => cb()); + const setSelectedAccountSpy = sinon.stub().callsFake((_, cb) => cb()); background.getApi.returns({ - setSelectedAddress: setSelectedAddressSpy, + setSelectedInternalAccount: setSelectedAccountSpy, }); _setBackgroundConnection(background.getApi()); await store.dispatch( - actions.setSelectedAddress( - '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - ), + actions.setSelectedAccount('0140ecb0-185b-4067-a25a-43997fecb05d'), ); - expect(setSelectedAddressSpy.callCount).toStrictEqual(1); + expect(setSelectedAccountSpy.callCount).toStrictEqual(1); }); - it('errors when setSelectedAddress throws', async () => { + it('errors when setSelectedAccount throws', async () => { const store = mockStore(); - const setSelectedAddressSpy = sinon - .stub() - .callsFake((_, cb) => cb(new Error('error'))); - - background.getApi.returns({ - setSelectedAddress: setSelectedAddressSpy, - }); - - _setBackgroundConnection(background.getApi()); - - const expectedActions = [ - { type: 'SHOW_LOADING_INDICATION', payload: undefined }, - { type: 'DISPLAY_WARNING', payload: 'error' }, - { type: 'HIDE_LOADING_INDICATION' }, - ]; - - await store.dispatch(actions.setSelectedAddress()); - expect(store.getActions()).toStrictEqual(expectedActions); - }); - }); - - describe('#setSelectedAccount', () => { - afterEach(() => { - sinon.restore(); - }); - - it('#setSelectedAccount', async () => { - const store = mockStore({ - activeTab: {}, - metamask: { alertEnabledness: {}, selectedAddress: '0x123' }, - }); - - const setSelectedAddressSpy = sinon.stub().callsFake((_, cb) => cb()); - - background.getApi.returns({ - setSelectedAddress: setSelectedAddressSpy, - }); - - _setBackgroundConnection(background.getApi()); - - await store.dispatch(actions.setSelectedAccount()); - expect(setSelectedAddressSpy.callCount).toStrictEqual(1); - }); - - it('displays warning if setSelectedAccount throws', async () => { - const store = mockStore({ - activeTab: {}, - metamask: { alertEnabledness: {}, selectedAddress: '0x123' }, - }); - - const setSelectedAddressSpy = sinon + const setSelectedAccountSpy = sinon .stub() .callsFake((_, cb) => cb(new Error('error'))); background.getApi.returns({ - setSelectedAddress: setSelectedAddressSpy, + setSelectedInternalAccount: setSelectedAccountSpy, }); _setBackgroundConnection(background.getApi()); @@ -1985,8 +1999,23 @@ describe('Actions', () => { balance: '0x0', }, }, - identities: { - '0xFirstAddress': {}, + internalAccounts: { + accounts: { + '8e110453-2231-4e62-82de-29b913dfef4b': { + address: '0xFirstAddress', + id: '8e110453-2231-4e62-82de-29b913dfef4b', + metadata: { + name: 'Test Account 2', + keyring: { + type: 'HD Key Tree', + }, + }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, + }, + }, + selectedAccount: '8e110453-2231-4e62-82de-29b913dfef4b', }, cachedBalances: { '0x1': { diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 116ab183074f..f59f921e0503 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -22,6 +22,7 @@ import { NonEmptyArray } from '@metamask/controller-utils'; ///: BEGIN:ONLY_INCLUDE_IN(keyring-snaps) import { HandlerType } from '@metamask/snaps-utils'; ///: END:ONLY_INCLUDE_IN +import { InternalAccount } from '@metamask/keyring-api'; import { SetNameRequest, UpdateProposedNamesRequest, @@ -38,9 +39,10 @@ import { getEnvironmentType, addHexPrefix } from '../../app/scripts/lib/util'; import { getMetaMaskAccounts, getPermittedAccountsForCurrentTab, - getSelectedAddress, hasTransactionPendingApprovals, getApprovalFlows, + getInternalAccount, + getSelectedInternalAccount, getCurrentNetworkTransactions, ///: BEGIN:ONLY_INCLUDE_IN(snaps) getNotifications, @@ -365,14 +367,14 @@ export function resetAccount(): ThunkAction< } export function removeAccount( - address: string, + accountId: string, ): ThunkAction { return async (dispatch: MetaMaskReduxDispatch) => { dispatch(showLoadingIndication()); try { await new Promise((resolve, reject) => { - callBackgroundMethod('removeAccount', [address], (error, account) => { + callBackgroundMethod('removeAccount', [accountId], (error, account) => { if (error) { reject(error); return; @@ -388,7 +390,7 @@ export function removeAccount( dispatch(hideLoadingIndication()); } - log.info(`Account removed: ${address}`); + log.info(`Account removed: ${accountId}`); dispatch(showAccountsPage()); }; } @@ -427,31 +429,36 @@ export function importNewAccount( }; } -export function addNewAccount(): ThunkAction< - void, - MetaMaskReduxState, - unknown, - AnyAction -> { +export function addNewAccount( + accountName: string, +): ThunkAction { log.debug(`background.addNewAccount`); return async (dispatch, getState) => { - const oldIdentities = getState().metamask.identities; + const oldAccounts = getState().metamask.internalAccounts.accounts; + const oldHdAccounts = Object.values(oldAccounts).filter( + (account) => account.metadata.keyring.type === 'HD Key Tree', + ); + dispatch(showLoadingIndication()); - let addedAccountAddress; + let newAccount; try { - addedAccountAddress = await submitRequestToBackground('addNewAccount', [ - Object.keys(oldIdentities).length, - ]); + const { accounts }: { accounts: InternalAccount[] } = + await submitRequestToBackground('addNewAccount', [ + oldHdAccounts.length, + ]); + newAccount = accounts.find((account) => !oldAccounts[account.id]); } catch (error) { dispatch(displayWarning(error)); throw error; } finally { dispatch(hideLoadingIndication()); } - await forceUpdateMetamaskState(dispatch); - return addedAccountAddress; + if (accountName && newAccount) { + dispatch(setAccountLabel(newAccount.id, accountName)); + } + return newAccount; }; } @@ -1183,7 +1190,7 @@ export function removeSnap( }, })) as unknown as any[]; for (const account of accounts) { - dispatch(removeAccount(account.address.toLowerCase())); + dispatch(removeAccount(account.id)); } } ///: END:ONLY_INCLUDE_IN @@ -1537,18 +1544,21 @@ export function updateMetamaskState( const providerConfig = getProviderConfig(state); const { metamask: currentState } = state; - const { currentLocale, selectedAddress } = currentState; + const { + currentLocale, + internalAccounts: { selectedAccount: selectedAccountId }, + } = currentState; const { currentLocale: newLocale, - selectedAddress: newSelectedAddress, providerConfig: newProviderConfig, + internalAccounts: { selectedAccount: newSelectedAccountId }, } = newState; if (currentLocale && newLocale && currentLocale !== newLocale) { dispatch(updateCurrentLocale(newLocale)); } - if (selectedAddress !== newSelectedAddress) { + if (selectedAccountId !== newSelectedAccountId) { dispatch({ type: actionConstants.SELECTED_ADDRESS_CHANGED }); } @@ -1560,8 +1570,8 @@ export function updateMetamaskState( getMetaMaskAccounts({ metamask: newState }); const oldAccounts: { [address: string]: Record } = getMetaMaskAccounts({ metamask: currentState }); - const newSelectedAccount = newAccounts[newSelectedAddress]; - const oldSelectedAccount = newAccounts[selectedAddress]; + const newSelectedAccount = newAccounts[newSelectedAccountId]; + const oldSelectedAccount = newAccounts[selectedAccountId]; // dispatch an ACCOUNT_CHANGED for any account whose balance or other // properties changed in this update Object.entries(oldAccounts).forEach(([address, oldAccount]) => { @@ -1665,19 +1675,19 @@ export function lockMetamask(): ThunkAction< }; } -async function _setSelectedAddress(address: string): Promise { - log.debug(`background.setSelectedAddress`); - await submitRequestToBackground('setSelectedAddress', [address]); +async function _setSelectedInternalAccount(accountId: string): Promise { + log.debug(`background.setSelectedInternalAccount`); + await submitRequestToBackground('setSelectedInternalAccount', [accountId]); } -export function setSelectedAddress( - address: string, +export function setSelectedAccount( + accountId: string, ): ThunkAction { return async (dispatch: MetaMaskReduxDispatch) => { dispatch(showLoadingIndication()); - log.debug(`background.setSelectedAddress`); + log.debug(`background.setSelectedInternalAccount`); try { - await _setSelectedAddress(address); + await _setSelectedInternalAccount(accountId); } catch (error) { dispatch(displayWarning(error)); return; @@ -1687,32 +1697,36 @@ export function setSelectedAddress( }; } -export function setSelectedAccount( - address: string, +export function setSelectedInternalAccount( + accountId: string, ): ThunkAction { return async (dispatch, getState) => { dispatch(showLoadingIndication()); - log.debug(`background.setSelectedAddress`); + log.debug(`background.setSelectedInternalAccount`); const state = getState(); const unconnectedAccountAccountAlertIsEnabled = getUnconnectedAccountAlertEnabledness(state); const activeTabOrigin = state.activeTab.origin; - const selectedAddress = getSelectedAddress(state); + const selectedAccount = getSelectedInternalAccount(state); + const accountToBeSet = getInternalAccount(state, accountId); const permittedAccountsForCurrentTab = getPermittedAccountsForCurrentTab(state); + + // TODO: ACCOUNTS_CONTROLLER change to account id const currentTabIsConnectedToPreviousAddress = Boolean(activeTabOrigin) && - permittedAccountsForCurrentTab.includes(selectedAddress); + permittedAccountsForCurrentTab.includes(selectedAccount.address); + // TODO: ACCOUNTS_CONTROLLER change to account id const currentTabIsConnectedToNextAddress = Boolean(activeTabOrigin) && - permittedAccountsForCurrentTab.includes(address); + permittedAccountsForCurrentTab.includes(accountToBeSet.address); const switchingToUnconnectedAddress = currentTabIsConnectedToPreviousAddress && !currentTabIsConnectedToNextAddress; try { - await _setSelectedAddress(address); + await _setSelectedInternalAccount(accountId); await forceUpdateMetamaskState(dispatch); } catch (error) { dispatch(displayWarning(error)); @@ -2666,7 +2680,7 @@ export function showPrivateKey(key: string): PayloadAction { } export function setAccountLabel( - account: string, + accountId: string, label: string, ): ThunkAction, MetaMaskReduxState, unknown, AnyAction> { return (dispatch: MetaMaskReduxDispatch) => { @@ -2674,7 +2688,7 @@ export function setAccountLabel( log.debug(`background.setAccountLabel`); return new Promise((resolve, reject) => { - callBackgroundMethod('setAccountLabel', [account, label], (err) => { + callBackgroundMethod('setAccountLabel', [accountId, label], (err) => { dispatch(hideLoadingIndication()); if (err) { @@ -2685,9 +2699,9 @@ export function setAccountLabel( dispatch({ type: actionConstants.SET_ACCOUNT_LABEL, - value: { account, label }, + value: { accountId, label }, }); - resolve(account); + resolve(accountId); }); }); }; @@ -2886,10 +2900,10 @@ export function toggleNetworkMenu() { }; } -export function setAccountDetailsAddress(address: string) { +export function setAccountDetailsAccountId(accountId: string) { return { - type: actionConstants.SET_ACCOUNT_DETAILS_ADDRESS, - payload: address, + type: actionConstants.SET_ACCOUNT_DETAILS_ACCOUNT_ID, + payload: accountId, }; } @@ -3926,7 +3940,7 @@ export function getNextNonce(): ThunkAction< AnyAction > { return async (dispatch, getState) => { - const address = getState().metamask.selectedAddress; + const { address } = getSelectedInternalAccount(getState()); let nextNonce; try { nextNonce = await submitRequestToBackground('getNextNonce', [ diff --git a/ui/store/store.ts b/ui/store/store.ts index 739271df9b8b..426664d81d17 100644 --- a/ui/store/store.ts +++ b/ui/store/store.ts @@ -3,6 +3,7 @@ import { configureStore as baseConfigureStore } from '@reduxjs/toolkit'; import devtoolsEnhancer from 'remote-redux-devtools'; import { ApprovalControllerState } from '@metamask/approval-controller'; import { GasEstimateType, GasFeeEstimates } from '@metamask/gas-fee-controller'; +import { InternalAccount } from '@metamask/keyring-api'; import rootReducer from '../ducks'; import { LedgerTransportTypes } from '../../shared/constants/hardware-wallets'; import { TransactionMeta } from '../../shared/constants/transaction'; @@ -55,11 +56,6 @@ interface TemporaryBackgroundState { }; transactions: TransactionMeta[]; selectedAddress: string; - identities: { - [address: string]: { - balance: string; - }; - }; ledgerTransportType: LedgerTransportTypes; unapprovedDecryptMsgs: MessagesIndexedById; unapprovedMsgs: MessagesIndexedById; @@ -83,6 +79,12 @@ interface TemporaryBackgroundState { ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) custodyAccountDetails?: { [key: string]: any }; ///: END:ONLY_INCLUDE_IN + internalAccounts: { + accounts: { + [key: string]: InternalAccount; + }; + selectedAccount: string; + }; } type RootReducerReturnType = ReturnType; diff --git a/yarn.lock b/yarn.lock index 2df099c77d1f..b030a1ec5f10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3029,12 +3029,12 @@ __metadata: linkType: hard "@jest/expect@npm:^29.1.2": - version: 29.1.2 - resolution: "@jest/expect@npm:29.1.2" + version: 29.6.2 + resolution: "@jest/expect@npm:29.6.2" dependencies: - expect: "npm:^29.1.2" - jest-snapshot: "npm:^29.1.2" - checksum: 7bb80bf176f39becdbd2ef54be615c3e7010f3230ccc419f22c97fa5b66e0fa14bb6b05019840d01cd1892e42be8158bea4c94487f05da1dd2b85a4ac097dd15 + expect: "npm:^29.6.2" + jest-snapshot: "npm:^29.6.2" + checksum: 2beed96e3d24945a72aa2ae4843c99f4c631564569258fdde6746fae3efcbfbff96dbf92ed28d6531299bd12b2766075bacaaae88ebf84b99316e77151b43a9f languageName: node linkType: hard @@ -3772,6 +3772,27 @@ __metadata: languageName: node linkType: hard +"@metamask/accounts-controller@npm:^1.0.0": + version: 1.0.0 + resolution: "@metamask/accounts-controller@npm:1.0.0" + dependencies: + "@metamask/base-controller": "npm:^3.2.1" + "@metamask/eth-snap-keyring": "npm:^0.2.2" + "@metamask/keyring-api": "npm:^0.2.5" + "@metamask/snaps-utils": "npm:^1.0.1" + "@metamask/utils": "npm:^6.2.0" + deepmerge: "npm:^4.2.2" + eth-rpc-errors: "npm:^4.0.2" + ethereumjs-util: "npm:^7.0.10" + immer: "npm:^9.0.6" + nanoid: "npm:^3.1.31" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/keyring-controller": ^7.5.0 + checksum: 9a3b172921a39d313f4688f21b9ad62df4321ecc22b6361e8186fe3fcd61fcd3403cda82389057623db2c3087850fa0e7b2f0c65797b548a4243080e60d4cc0b + languageName: node + linkType: hard + "@metamask/address-book-controller@npm:^3.0.0": version: 3.0.0 resolution: "@metamask/address-book-controller@npm:3.0.0" @@ -4192,19 +4213,19 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-snap-keyring@npm:^0.2.2": - version: 0.2.2 - resolution: "@metamask/eth-snap-keyring@npm:0.2.2" +"@metamask/eth-snap-keyring@npm:^0.2.2, @metamask/eth-snap-keyring@npm:^0.2.3": + version: 0.2.3 + resolution: "@metamask/eth-snap-keyring@npm:0.2.3" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/eth-sig-util": "npm:^7.0.0" - "@metamask/keyring-api": "npm:^0.2.3" - "@metamask/snaps-controllers": "npm:^0.38.2-flask.1" + "@metamask/keyring-api": "npm:^0.2.7" + "@metamask/snaps-controllers": "npm:^2.0.0" "@metamask/utils": "npm:^8.1.0" "@types/uuid": "npm:^9.0.1" superstruct: "npm:^1.0.3" uuid: "npm:^9.0.0" - checksum: d5ae1da2428ed5a80bb5508235b80f1b812174036387c13c780398e179ed9aae07b048cbe87e93f71e74b8a520b6b2915b979a30da1a2293b97045a9343d79f9 + checksum: 6a6909c34267d78fb3d0b8a4d450667c139862c5e39a982e4cbe613c536d437d701d117f5da4a606209f1acd41d1ca4db9dadcd13df23e82a75fdc1bb9ad74c3 languageName: node linkType: hard @@ -4306,6 +4327,20 @@ __metadata: languageName: node linkType: hard +"@metamask/key-tree@npm:^7.1.1": + version: 7.1.1 + resolution: "@metamask/key-tree@npm:7.1.1" + dependencies: + "@metamask/scure-bip39": "npm:^2.1.0" + "@metamask/utils": "npm:^6.0.1" + "@noble/ed25519": "npm:^1.6.0" + "@noble/hashes": "npm:^1.0.0" + "@noble/secp256k1": "npm:^1.5.5" + "@scure/base": "npm:^1.0.0" + checksum: 7e48c3c2c88cf0c451c9d45b70af5bbad585d0d224dff11a5e0e80430e7d3abe584fa0d42875486c1f3144d6b366ff08b13be7638c4c47cb26ee67775699d017 + languageName: node + linkType: hard + "@metamask/key-tree@npm:^9.0.0": version: 9.0.0 resolution: "@metamask/key-tree@npm:9.0.0" @@ -4320,19 +4355,19 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^0.2.3": - version: 0.2.4 - resolution: "@metamask/keyring-api@npm:0.2.4" +"@metamask/keyring-api@npm:^0.2.5, @metamask/keyring-api@npm:^0.2.7": + version: 0.2.7 + resolution: "@metamask/keyring-api@npm:0.2.7" dependencies: - "@metamask/providers": "npm:^12.0.0" - "@metamask/rpc-methods": "npm:^0.38.1-flask.1" - "@metamask/snaps-controllers": "npm:^0.38.2-flask.1" - "@metamask/snaps-utils": "npm:^0.38.2-flask.1" + "@metamask/providers": "npm:^13.0.0" + "@metamask/rpc-methods": "npm:^2.0.0" + "@metamask/snaps-controllers": "npm:^2.0.0" + "@metamask/snaps-utils": "npm:^2.0.0" "@metamask/utils": "npm:^8.1.0" "@types/uuid": "npm:^9.0.1" superstruct: "npm:^1.0.3" uuid: "npm:^9.0.0" - checksum: d61e2bf7252135e5bb099bb10886fcbd4c25392e3bfe9beb267b57f97e653b687e727fd67726492723837b66c40cd226e2539c5a80ebfbe1e56ebf9e93f93728 + checksum: e5c607d4b23fcdb75a32fe17a9a25f46acc0c11a41289f444006fddf334b6f75bac6fcc9b9110ababc69388c64d5cbe8a291ac6a73e821c6e9844735f937ba09 languageName: node linkType: hard @@ -4599,7 +4634,7 @@ __metadata: languageName: node linkType: hard -"@metamask/post-message-stream@npm:^6.0.0, @metamask/post-message-stream@npm:^6.1.2, @metamask/post-message-stream@npm:^6.2.0": +"@metamask/post-message-stream@npm:^6.0.0, @metamask/post-message-stream@npm:^6.2.0": version: 6.2.0 resolution: "@metamask/post-message-stream@npm:6.2.0" dependencies: @@ -4642,6 +4677,26 @@ __metadata: languageName: node linkType: hard +"@metamask/providers@npm:^10.2.1": + version: 10.2.1 + resolution: "@metamask/providers@npm:10.2.1" + dependencies: + "@metamask/object-multiplex": "npm:^1.1.0" + "@metamask/safe-event-emitter": "npm:^2.0.0" + "@types/chrome": "npm:^0.0.136" + detect-browser: "npm:^5.2.0" + eth-rpc-errors: "npm:^4.0.2" + extension-port-stream: "npm:^2.0.1" + fast-deep-equal: "npm:^2.0.1" + is-stream: "npm:^2.0.0" + json-rpc-engine: "npm:^6.1.0" + json-rpc-middleware-stream: "npm:^4.2.1" + pump: "npm:^3.0.0" + webextension-polyfill-ts: "npm:^0.25.0" + checksum: b8784ee9ae3f740c43dc8079754886be15249aa1b4e65dd969a5ddb067745c068a45bb329b6b343f34d7629002d771a74a873599dad89f140413ff2a95cdbffb + languageName: node + linkType: hard + "@metamask/providers@npm:^11.1.0, @metamask/providers@npm:^11.1.1": version: 11.1.2 resolution: "@metamask/providers@npm:11.1.2" @@ -4661,9 +4716,9 @@ __metadata: languageName: node linkType: hard -"@metamask/providers@npm:^12.0.0": - version: 12.0.0 - resolution: "@metamask/providers@npm:12.0.0" +"@metamask/providers@npm:^13.0.0": + version: 13.0.0 + resolution: "@metamask/providers@npm:13.0.0" dependencies: "@metamask/json-rpc-engine": "npm:^7.1.1" "@metamask/object-multiplex": "npm:^1.1.0" @@ -4675,9 +4730,8 @@ __metadata: fast-deep-equal: "npm:^3.1.3" is-stream: "npm:^2.0.0" json-rpc-middleware-stream: "npm:^4.2.1" - pump: "npm:^3.0.0" webextension-polyfill: "npm:^0.10.0" - checksum: 8c3895593a71de6e165276f00069b57f83b5bb6991b6bb9444ae556d0ceb3252d56b546eb136b19b854b919c4368bf30b37c993da8904ea8ddf200323759e715 + checksum: c6fe1936741a2b782960f0a12148602754fabe55b75707fd80716d5a42ca4f672d838d1ba36873e96f4afdc5360247e3b737c95b7ca532a74e135218cb65033a languageName: node linkType: hard @@ -4712,23 +4766,6 @@ __metadata: languageName: node linkType: hard -"@metamask/rpc-methods@npm:^0.38.1-flask.1": - version: 0.38.1-flask.1 - resolution: "@metamask/rpc-methods@npm:0.38.1-flask.1" - dependencies: - "@metamask/key-tree": "npm:^9.0.0" - "@metamask/permission-controller": "npm:^4.1.0" - "@metamask/snaps-ui": "npm:^0.37.4-flask.1" - "@metamask/snaps-utils": "npm:^0.38.2-flask.1" - "@metamask/types": "npm:^1.1.0" - "@metamask/utils": "npm:^6.0.1" - "@noble/hashes": "npm:^1.3.1" - eth-rpc-errors: "npm:^4.0.3" - superstruct: "npm:^1.0.3" - checksum: b28adc2fe7e08a58f7760ffbd67bd365f2bed8e7b34857c83ed125d9f7204dbca6edfa5d8776dafbdf4a7862d3ef828079afbba857f0d4eca1ba273f655687da - languageName: node - linkType: hard - "@metamask/rpc-methods@npm:^2.0.0": version: 2.0.0 resolution: "@metamask/rpc-methods@npm:2.0.0" @@ -4845,36 +4882,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^0.38.2-flask.1": - version: 0.38.2-flask.1 - resolution: "@metamask/snaps-controllers@npm:0.38.2-flask.1" - dependencies: - "@metamask/approval-controller": "npm:^3.5.0" - "@metamask/base-controller": "npm:^3.2.0" - "@metamask/object-multiplex": "npm:^1.2.0" - "@metamask/permission-controller": "npm:^4.1.0" - "@metamask/post-message-stream": "npm:^6.1.2" - "@metamask/rpc-methods": "npm:^0.38.1-flask.1" - "@metamask/snaps-execution-environments": "npm:^0.38.2-flask.1" - "@metamask/snaps-registry": "npm:^1.2.1" - "@metamask/snaps-utils": "npm:^0.38.2-flask.1" - "@metamask/utils": "npm:^6.0.1" - "@xstate/fsm": "npm:^2.0.0" - concat-stream: "npm:^2.0.0" - eth-rpc-errors: "npm:^4.0.3" - gunzip-maybe: "npm:^1.4.2" - immer: "npm:^9.0.6" - json-rpc-engine: "npm:^6.1.0" - json-rpc-middleware-stream: "npm:^4.2.0" - nanoid: "npm:^3.1.31" - pump: "npm:^3.0.0" - readable-web-to-node-stream: "npm:^3.0.2" - tar-stream: "npm:^2.2.0" - checksum: 62f555c24b5200f082e796f8cc53a326dc35a34bd94fc98f66bef1160f2f3b78a9c71e2d2548687d043817df982433f794da4a012ef3230c4604f7ce2781159a - languageName: node - linkType: hard - -"@metamask/snaps-controllers@npm:^2.0.1": +"@metamask/snaps-controllers@npm:^2.0.0, @metamask/snaps-controllers@npm:^2.0.1": version: 2.0.1 resolution: "@metamask/snaps-controllers@npm:2.0.1" dependencies: @@ -4902,25 +4910,6 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-execution-environments@npm:^0.38.2-flask.1": - version: 0.38.2-flask.1 - resolution: "@metamask/snaps-execution-environments@npm:0.38.2-flask.1" - dependencies: - "@metamask/object-multiplex": "npm:^1.2.0" - "@metamask/post-message-stream": "npm:^6.1.2" - "@metamask/providers": "npm:^11.1.1" - "@metamask/rpc-methods": "npm:^0.38.1-flask.1" - "@metamask/snaps-utils": "npm:^0.38.2-flask.1" - "@metamask/utils": "npm:^6.0.1" - eth-rpc-errors: "npm:^4.0.3" - json-rpc-engine: "npm:^6.1.0" - nanoid: "npm:^3.1.31" - pump: "npm:^3.0.0" - superstruct: "npm:^1.0.3" - checksum: ae8f8991d5911cb4eff0145ab136549467f5e904f3530653dbb678f52a71f0a2bc17646b57ae0aceb9848db56e1fd110cef5c24916f247ac8d3e9eaac2cee6f3 - languageName: node - linkType: hard - "@metamask/snaps-execution-environments@npm:^2.0.1": version: 2.0.1 resolution: "@metamask/snaps-execution-environments@npm:2.0.1" @@ -4961,13 +4950,13 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-ui@npm:^0.37.4-flask.1": - version: 0.37.4-flask.1 - resolution: "@metamask/snaps-ui@npm:0.37.4-flask.1" +"@metamask/snaps-ui@npm:^1.0.2": + version: 1.0.2 + resolution: "@metamask/snaps-ui@npm:1.0.2" dependencies: "@metamask/utils": "npm:^6.0.1" superstruct: "npm:^1.0.3" - checksum: e57ca1e375d0c7860f198143789226552cb449a654e97b639f90a0cb577f9d46387c7c65b5dacc05b7fa613ed8499021df286590b6c633b6f5be8578d4d9f6a9 + checksum: 2c2eeb252f30670f4598338ea40fe0b430b054b606f341ed89e0cd1734395d132a861eb7677935e5a6fa0b85996c3c1f4eb773c02678cc736cc5f13433eb402a languageName: node linkType: hard @@ -4981,32 +4970,31 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^0.38.2-flask.1": - version: 0.38.2-flask.1 - resolution: "@metamask/snaps-utils@npm:0.38.2-flask.1" +"@metamask/snaps-utils@npm:^1.0.1": + version: 1.0.2 + resolution: "@metamask/snaps-utils@npm:1.0.2" dependencies: - "@babel/core": "npm:^7.20.12" + "@babel/core": "npm:^7.18.6" "@babel/types": "npm:^7.18.7" - "@metamask/base-controller": "npm:^3.2.0" - "@metamask/key-tree": "npm:^9.0.0" - "@metamask/permission-controller": "npm:^4.1.0" + "@metamask/base-controller": "npm:^3.0.0" + "@metamask/key-tree": "npm:^7.1.1" + "@metamask/permission-controller": "npm:^4.0.0" + "@metamask/providers": "npm:^10.2.1" "@metamask/snaps-registry": "npm:^1.2.1" - "@metamask/snaps-ui": "npm:^0.37.4-flask.1" + "@metamask/snaps-ui": "npm:^1.0.2" "@metamask/utils": "npm:^6.0.1" - "@noble/hashes": "npm:^1.3.1" + "@noble/hashes": "npm:^1.1.3" "@scure/base": "npm:^1.1.1" - chalk: "npm:^4.1.2" cron-parser: "npm:^4.5.0" eth-rpc-errors: "npm:^4.0.3" fast-deep-equal: "npm:^3.1.3" fast-json-stable-stringify: "npm:^2.1.0" - is-svg: "npm:^4.4.0" rfdc: "npm:^1.3.0" semver: "npm:^7.5.4" ses: "npm:^0.18.7" superstruct: "npm:^1.0.3" validate-npm-package-name: "npm:^5.0.0" - checksum: 9f488976851c7bf6c1bebaa12a2542c651bb44a7df9901ee89b6e2aed0d610a9417c1994a7aa840fb321fb66dd6c2b2148035e9205f005aab22d29d47427c957 + checksum: 0f8663c1ef2894b06829a507e0ea0a724615ac256476b28c21cd060531101e0428fd0c7563aeafff795751c4641043ec50445d54138f5514bfd211723b20a80f languageName: node linkType: hard @@ -5214,7 +5202,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:^1.0.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1": +"@noble/hashes@npm:^1.0.0, @noble/hashes@npm:^1.1.3, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1": version: 1.3.2 resolution: "@noble/hashes@npm:1.3.2" checksum: 685f59d2d44d88e738114b71011d343a9f7dce9dfb0a121f1489132f9247baa60bc985e5ec6f3213d114fbd1e1168e7294644e46cbd0ce2eba37994f28eeb51b @@ -7581,6 +7569,16 @@ __metadata: languageName: node linkType: hard +"@types/chrome@npm:^0.0.136": + version: 0.0.136 + resolution: "@types/chrome@npm:0.0.136" + dependencies: + "@types/filesystem": "npm:*" + "@types/har-format": "npm:*" + checksum: 4de30c5bd3eec7aba4c110985779ba179a4a433a68ef4d5e96289d8aca4318cf9c206f0c9fced020e1a498e32f0fc4942d9209424c66905e7b43983b38b680c0 + languageName: node + linkType: hard + "@types/color-name@npm:^1.1.1": version: 1.1.1 resolution: "@types/color-name@npm:1.1.1" @@ -7736,6 +7734,22 @@ __metadata: languageName: node linkType: hard +"@types/filesystem@npm:*": + version: 0.0.30 + resolution: "@types/filesystem@npm:0.0.30" + dependencies: + "@types/filewriter": "npm:*" + checksum: ccbce5af7b2d0a58d23b05a6f2c19321983918e581a682baeea22233ebd637ebe6b0e890c7d0860c04990a1a3367818cff6f312e10b37f474dceeeb599b44a14 + languageName: node + linkType: hard + +"@types/filewriter@npm:*": + version: 0.0.29 + resolution: "@types/filewriter@npm:0.0.29" + checksum: bbad0faade291f375f670fff7e95a9eab6bd67315c978f31ce3bb047be418fcc2d9c56b6eeebd1599d7128f0faa43ae3f5f709e6f53d2130eb022fbe32d63c1e + languageName: node + linkType: hard + "@types/find-cache-dir@npm:^3.2.1": version: 3.2.1 resolution: "@types/find-cache-dir@npm:3.2.1" @@ -7831,6 +7845,13 @@ __metadata: languageName: node linkType: hard +"@types/har-format@npm:*": + version: 1.2.5 + resolution: "@types/har-format@npm:1.2.5" + checksum: 33206883b6b59f5638eaaeae1e728cd2877a075d2fdc5435f9d3ddaef4ac463d446c5a7ff6b44abc8080b428a4bdaff10dc7c0d7e83363774c12c0f77b2cc73d + languageName: node + linkType: hard + "@types/hast@npm:^2.0.0": version: 2.3.1 resolution: "@types/hast@npm:2.3.1" @@ -15969,18 +15990,6 @@ __metadata: languageName: node linkType: hard -"eth-sig-util@npm:^3.0.0": - version: 3.0.1 - resolution: "eth-sig-util@npm:3.0.1" - dependencies: - ethereumjs-abi: "npm:^0.6.8" - ethereumjs-util: "npm:^5.1.1" - tweetnacl: "npm:^1.0.3" - tweetnacl-util: "npm:^0.15.0" - checksum: 2f6a68b72569528c310f429136284596e28aac9de4874ef62af6ae76340193991c02cc88b50388ac8c5d3dc120c635d62e59eb7d6e49df51e75d20d9e45c70dc - languageName: node - linkType: hard - "ethereum-bloom-filters@npm:^1.0.6": version: 1.0.7 resolution: "ethereum-bloom-filters@npm:1.0.7" @@ -16044,7 +16053,7 @@ __metadata: languageName: node linkType: hard -"ethereumjs-abi@npm:0.6.8, ethereumjs-abi@npm:^0.6.4, ethereumjs-abi@npm:^0.6.8": +"ethereumjs-abi@npm:0.6.8, ethereumjs-abi@npm:^0.6.4": version: 0.6.8 resolution: "ethereumjs-abi@npm:0.6.8" dependencies: @@ -16561,7 +16570,7 @@ __metadata: languageName: node linkType: hard -"expect@npm:^29.0.0, expect@npm:^29.1.2, expect@npm:^29.6.2": +"expect@npm:^29.0.0, expect@npm:^29.6.2": version: 29.6.2 resolution: "expect@npm:29.6.2" dependencies: @@ -16661,7 +16670,7 @@ __metadata: languageName: node linkType: hard -"extension-port-stream@npm:^2.0.0, extension-port-stream@npm:^2.1.1": +"extension-port-stream@npm:^2.0.0, extension-port-stream@npm:^2.0.1, extension-port-stream@npm:^2.1.1": version: 2.1.1 resolution: "extension-port-stream@npm:2.1.1" dependencies: @@ -16767,6 +16776,13 @@ __metadata: languageName: node linkType: hard +"fast-deep-equal@npm:^2.0.1": + version: 2.0.1 + resolution: "fast-deep-equal@npm:2.0.1" + checksum: b701835a87985e0ec4925bdf1f0c1e7eb56309b5d12d534d5b4b69d95a54d65bb16861c081781ead55f73f12d6c60ba668713391ee7fbf6b0567026f579b7b0b + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -21631,7 +21647,7 @@ __metadata: languageName: node linkType: hard -"jest-snapshot@npm:^29.1.2": +"jest-snapshot@npm:^29.1.2, jest-snapshot@npm:^29.6.2": version: 29.6.2 resolution: "jest-snapshot@npm:29.6.2" dependencies: @@ -24021,6 +24037,7 @@ __metadata: "@metamask-institutional/rpc-allowlist": "npm:^1.0.0" "@metamask-institutional/sdk": "npm:^0.1.18" "@metamask-institutional/transaction-update": "npm:^0.1.27" + "@metamask/accounts-controller": "npm:^1.0.0" "@metamask/address-book-controller": "npm:^3.0.0" "@metamask/announcement-controller": "npm:^4.0.0" "@metamask/approval-controller": "npm:^3.4.0" @@ -24040,7 +24057,7 @@ __metadata: "@metamask/eth-json-rpc-middleware": "npm:^11.0.0" "@metamask/eth-keyring-controller": "npm:^10.0.1" "@metamask/eth-ledger-bridge-keyring": "npm:^0.15.0" - "@metamask/eth-snap-keyring": "npm:^0.2.2" + "@metamask/eth-snap-keyring": "npm:^0.2.3" "@metamask/eth-token-tracker": "npm:^4.0.0" "@metamask/eth-trezor-keyring": "npm:^1.1.0" "@metamask/etherscan-link": "npm:^2.2.0" @@ -24049,6 +24066,7 @@ __metadata: "@metamask/gas-fee-controller": "npm:^6.0.1" "@metamask/jazzicon": "npm:^2.0.0" "@metamask/key-tree": "npm:^9.0.0" + "@metamask/keyring-api": "npm:^0.2.7" "@metamask/keyring-controller": "npm:^8.0.0" "@metamask/logging-controller": "npm:^1.0.1" "@metamask/logo": "npm:^3.1.1" @@ -24189,7 +24207,6 @@ __metadata: eth-method-registry: "npm:^2.0.0" eth-query: "npm:^2.1.2" eth-rpc-errors: "npm:^4.0.2" - eth-sig-util: "npm:^3.0.0" ethereum-ens-network-map: "npm:^1.0.2" ethereumjs-abi: "npm:^0.6.4" ethereumjs-util: "npm:^7.0.10" @@ -34497,6 +34514,15 @@ __metadata: languageName: node linkType: hard +"webextension-polyfill-ts@npm:^0.25.0": + version: 0.25.0 + resolution: "webextension-polyfill-ts@npm:0.25.0" + dependencies: + webextension-polyfill: "npm:^0.7.0" + checksum: 33260014ffda174348ec2f8271dd4312f5ba6286fdc6f014b87194361fda7d0b10a4b168a7eb2a62525785cc28ef4080ac5cba20179041ba642e039bb49aee0e + languageName: node + linkType: hard + "webextension-polyfill@npm:>=0.10.0 <1.0, webextension-polyfill@npm:^0.10.0": version: 0.10.0 resolution: "webextension-polyfill@npm:0.10.0" @@ -34504,6 +34530,13 @@ __metadata: languageName: node linkType: hard +"webextension-polyfill@npm:^0.7.0": + version: 0.7.0 + resolution: "webextension-polyfill@npm:0.7.0" + checksum: 693a4d89705284e668ad501afe44a6f99dac6b5259ed6a57c559e6e8da827dfd449755ff367ee6c55cd4af7dead0fd7eb70b2b8ac938d191e6082f3fb7c211b6 + languageName: node + linkType: hard + "webextension-polyfill@npm:^0.8.0": version: 0.8.0 resolution: "webextension-polyfill@npm:0.8.0"