diff --git a/app/scripts/lib/transaction/util.test.ts b/app/scripts/lib/transaction/util.test.ts index ad5b87925073..16077e0f08ed 100644 --- a/app/scripts/lib/transaction/util.test.ts +++ b/app/scripts/lib/transaction/util.test.ts @@ -18,6 +18,7 @@ import { } from '../../../../shared/constants/security-provider'; import { SecurityAlertResponse } from '../ppom/types'; import { flushPromises } from '../../../../test/lib/timer-helpers'; +import { createMockInternalAccount } from '../../../../test/jest/mocks'; import { AddDappTransactionRequest, AddTransactionOptions, @@ -40,6 +41,9 @@ jest.mock('uuid', () => { const SECURITY_ALERT_ID_MOCK = '123'; const INTERNAL_ACCOUNT_ADDRESS = '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b'; +const INTERNAL_ACCOUNT = createMockInternalAccount({ + address: INTERNAL_ACCOUNT_ADDRESS, +}); const TRANSACTION_PARAMS_MOCK: TransactionParams = { from: '0x1', @@ -484,7 +488,7 @@ describe('Transaction Utils', () => { ...sendRequest, securityAlertsEnabled: false, chainId: '0x1', - internalAccounts: [{ address: INTERNAL_ACCOUNT_ADDRESS }], + internalAccounts: [INTERNAL_ACCOUNT], }); expect( diff --git a/app/scripts/migrations/105.test.ts b/app/scripts/migrations/105.test.ts index 944606043075..168fe8dd0916 100644 --- a/app/scripts/migrations/105.test.ts +++ b/app/scripts/migrations/105.test.ts @@ -72,6 +72,7 @@ function expectedInternalAccount( type: 'HD Key Tree', }, lastSelected: lastSelected ? expect.any(Number) : undefined, + importTime: 0, }, options: {}, methods: ETH_EOA_METHODS, diff --git a/app/scripts/migrations/105.ts b/app/scripts/migrations/105.ts index d4f5f2985215..a54b3e6457a7 100644 --- a/app/scripts/migrations/105.ts +++ b/app/scripts/migrations/105.ts @@ -97,6 +97,7 @@ function createInternalAccountsForAccountsController( metadata: { name: identity.name, lastSelected: identity.lastSelected ?? undefined, + importTime: 0, keyring: { // This is default HD Key Tree type because the keyring is encrypted // during migration, the type will get updated when the during the diff --git a/app/scripts/migrations/126.test.ts b/app/scripts/migrations/126.test.ts new file mode 100644 index 000000000000..a581b15d28d9 --- /dev/null +++ b/app/scripts/migrations/126.test.ts @@ -0,0 +1,69 @@ +import { AccountsControllerState } from '@metamask/accounts-controller'; +import { createMockInternalAccount } from '../../../test/jest/mocks'; +import { migrate, version } from './126'; + +const oldVersion = 125; + +const mockInternalAccount = createMockInternalAccount(); +const mockAccountsControllerState: AccountsControllerState = { + internalAccounts: { + accounts: { + [mockInternalAccount.id]: mockInternalAccount, + }, + selectedAccount: mockInternalAccount.id, + }, +}; + +describe('migration #126', () => { + afterEach(() => jest.resetAllMocks()); + + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: mockAccountsControllerState, + }, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('updates selected account if it is not found in the list of accounts', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: { + ...mockAccountsControllerState, + internalAccounts: { + ...mockAccountsControllerState.internalAccounts, + selectedAccount: 'unknown id', + }, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + const { + internalAccounts: { selectedAccount }, + } = newStorage.data.AccountsController as AccountsControllerState; + expect(selectedAccount).toStrictEqual(mockInternalAccount.id); + expect(newStorage.data.AccountsController).toStrictEqual( + mockAccountsControllerState, + ); + }); + + it('does nothing if the selectedAccount is found in the list of accounts', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: mockAccountsControllerState, + }, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data.AccountsController).toStrictEqual( + mockAccountsControllerState, + ); + }); +}); diff --git a/app/scripts/migrations/126.ts b/app/scripts/migrations/126.ts new file mode 100644 index 000000000000..da7a72b19d3a --- /dev/null +++ b/app/scripts/migrations/126.ts @@ -0,0 +1,49 @@ +import { AccountsControllerState } from '@metamask/accounts-controller'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 126; + +/** + * This migration removes depreciated `Txcontroller` key if it is present in state. + * + * @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; + transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record) { + const accountsControllerState = state?.AccountsController as + | AccountsControllerState + | undefined; + + if ( + accountsControllerState && + Object.values(accountsControllerState?.internalAccounts.accounts).length > + 0 && + !accountsControllerState?.internalAccounts.accounts[ + accountsControllerState?.internalAccounts.selectedAccount + ] + ) { + accountsControllerState.internalAccounts.selectedAccount = Object.values( + accountsControllerState?.internalAccounts.accounts, + )[0].id; + } + return state; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 305343e7124f..afc1484f3085 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -142,6 +142,7 @@ const migrations = [ require('./123'), require('./124'), require('./125'), + require('./126'), ]; export default migrations; diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 3986953430dd..e386b1576ad1 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -790,10 +790,10 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/accounts-controller>@metamask/base-controller": true, + "@metamask/accounts-controller>@metamask/utils": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/utils": true, "uuid": true } }, @@ -805,6 +805,21 @@ "immer": true } }, + "@metamask/accounts-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/address-book-controller": { "packages": { "@metamask/address-book-controller>@metamask/controller-utils": true, @@ -1610,10 +1625,25 @@ "URL": true }, "packages": { + "@metamask/keyring-api>@metamask/utils": true, "@metamask/keyring-api>bech32": true, "@metamask/keyring-api>uuid": true, - "@metamask/utils": true, - "superstruct": true + "@metamask/utils>@metamask/superstruct": true + } + }, + "@metamask/keyring-api>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/keyring-api>uuid": { diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 3986953430dd..e386b1576ad1 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -790,10 +790,10 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/accounts-controller>@metamask/base-controller": true, + "@metamask/accounts-controller>@metamask/utils": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/utils": true, "uuid": true } }, @@ -805,6 +805,21 @@ "immer": true } }, + "@metamask/accounts-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/address-book-controller": { "packages": { "@metamask/address-book-controller>@metamask/controller-utils": true, @@ -1610,10 +1625,25 @@ "URL": true }, "packages": { + "@metamask/keyring-api>@metamask/utils": true, "@metamask/keyring-api>bech32": true, "@metamask/keyring-api>uuid": true, - "@metamask/utils": true, - "superstruct": true + "@metamask/utils>@metamask/superstruct": true + } + }, + "@metamask/keyring-api>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/keyring-api>uuid": { diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 3986953430dd..e386b1576ad1 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -790,10 +790,10 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/accounts-controller>@metamask/base-controller": true, + "@metamask/accounts-controller>@metamask/utils": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/utils": true, "uuid": true } }, @@ -805,6 +805,21 @@ "immer": true } }, + "@metamask/accounts-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/address-book-controller": { "packages": { "@metamask/address-book-controller>@metamask/controller-utils": true, @@ -1610,10 +1625,25 @@ "URL": true }, "packages": { + "@metamask/keyring-api>@metamask/utils": true, "@metamask/keyring-api>bech32": true, "@metamask/keyring-api>uuid": true, - "@metamask/utils": true, - "superstruct": true + "@metamask/utils>@metamask/superstruct": true + } + }, + "@metamask/keyring-api>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/keyring-api>uuid": { diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 1120b0b9947b..5c007baf3f06 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -882,10 +882,10 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@ethereumjs/tx>ethereum-cryptography": true, "@metamask/accounts-controller>@metamask/base-controller": true, + "@metamask/accounts-controller>@metamask/utils": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, "@metamask/keyring-controller": true, - "@metamask/utils": true, "uuid": true } }, @@ -897,6 +897,21 @@ "immer": true } }, + "@metamask/accounts-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/address-book-controller": { "packages": { "@metamask/address-book-controller>@metamask/controller-utils": true, @@ -1702,10 +1717,25 @@ "URL": true }, "packages": { + "@metamask/keyring-api>@metamask/utils": true, "@metamask/keyring-api>bech32": true, "@metamask/keyring-api>uuid": true, - "@metamask/utils": true, - "superstruct": true + "@metamask/utils>@metamask/superstruct": true + } + }, + "@metamask/keyring-api>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/keyring-api>uuid": { diff --git a/package.json b/package.json index 8b265e0fe5cb..04cfe6f163f6 100644 --- a/package.json +++ b/package.json @@ -297,7 +297,7 @@ "@metamask-institutional/types": "^1.1.0", "@metamask/abi-utils": "^2.0.2", "@metamask/account-watcher": "^4.1.0", - "@metamask/accounts-controller": "^17.2.0", + "@metamask/accounts-controller": "^18.1.0", "@metamask/address-book-controller": "^4.0.1", "@metamask/announcement-controller": "^6.1.0", "@metamask/approval-controller": "^7.0.0", diff --git a/ui/components/multichain/connect-accounts-modal/connect-accounts-modal-list.test.tsx b/ui/components/multichain/connect-accounts-modal/connect-accounts-modal-list.test.tsx index e365acdd36bd..090a48f30b79 100644 --- a/ui/components/multichain/connect-accounts-modal/connect-accounts-modal-list.test.tsx +++ b/ui/components/multichain/connect-accounts-modal/connect-accounts-modal-list.test.tsx @@ -1,7 +1,9 @@ import React from 'react'; +import { EthAccountType } from '@metamask/keyring-api'; import configureStore from '../../../store/store'; import mockState from '../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../test/jest/rendering'; +import { ETH_EOA_METHODS } from '../../../../shared/constants/eth-methods'; import { ConnectAccountsModalList } from './connect-accounts-modal-list'; const render = () => { @@ -21,12 +23,20 @@ const render = () => { id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', metadata: { name: 'Custody Account A', + importTime: 1631610000000, + lastSelected: 1631610000000, keyring: { type: 'Custody', }, }, - type: 'eip155:eoa', + methods: ETH_EOA_METHODS, + type: EthAccountType.Eoa, options: {}, + name: 'Custody Account A', + balance: '0x0', + keyring: { + type: 'Custody', + }, }, ], selectedAccounts: [], diff --git a/ui/components/multichain/pages/connections/connections.tsx b/ui/components/multichain/pages/connections/connections.tsx index 2d751da873e6..ae1f5b88193e 100644 --- a/ui/components/multichain/pages/connections/connections.tsx +++ b/ui/components/multichain/pages/connections/connections.tsx @@ -110,7 +110,10 @@ export const Connections = () => { ); const selectedAccount = useSelector(getSelectedAccount); const internalAccounts = useSelector(getInternalAccounts); - const mergedAccounts = mergeAccounts(connectedAccounts, internalAccounts); + const mergedAccounts = mergeAccounts( + connectedAccounts, + internalAccounts, + ) as AccountType[]; const permittedAccountsByOrigin = useSelector( getPermittedAccountsByOrigin, @@ -174,16 +177,24 @@ export const Connections = () => { index === mergedAccounts.reduce( ( - acc: number, - cur: { metadata: { lastSelected: number } }, + indexOfAccountWIthHighestLastSelected: number, + currentAccountToCompare: AccountType, // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any i: any, - ) => - cur.metadata.lastSelected > - mergedAccounts[acc].metadata.lastSelected + ) => { + const currentLastSelected = + currentAccountToCompare.metadata.lastSelected ?? 0; + const accountAtIndexLastSelected = mergedAccounts[ + indexOfAccountWIthHighestLastSelected + ].metadata.lastSelected ? i - : acc, + : indexOfAccountWIthHighestLastSelected; + + return currentLastSelected > accountAtIndexLastSelected + ? i + : indexOfAccountWIthHighestLastSelected; + }, 0, ) ); @@ -252,12 +263,10 @@ export const Connections = () => { const isSelectedAccount = selectedAccount.address === account.address; // Match the index of latestSelected Account with the index of all the accounts and set the active status - let mergedAccountsProps; - if (index === latestSelected) { - mergedAccountsProps = { ...account, isAccountActive: true }; - } else { - mergedAccountsProps = { ...account }; - } + const mergedAccountsProps = { + ...account, + isAccountActive: index === latestSelected, + }; return ( { @@ -34,6 +35,8 @@ describe('PermissionDetailsModal', () => { id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', metadata: { name: 'Really Long Name That Should Be Truncated', + importTime: 1724252448, + lastSelected: 1724252448, keyring: { type: 'HD Key Tree', }, @@ -48,6 +51,7 @@ describe('PermissionDetailsModal', () => { metadata: { name: 'Account 1', lastSelected: 1586359844192, + importTime: 1586359844192, lastActive: 1586359844192, keyring: { type: 'HD Key Tree', @@ -112,10 +116,14 @@ describe('PermissionDetailsModal', () => { id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', metadata: { name: 'mockName', + importTime: 1724256979, keyring: { type: 'HD Key Tree', }, }, + options: {}, + methods: ETH_EOA_METHODS, + type: EthAccountType.Eoa, label: '', }, isOpen: true, diff --git a/ui/components/multichain/toast/toast.test.tsx b/ui/components/multichain/toast/toast.test.tsx index d1541587694e..eb208d41e122 100644 --- a/ui/components/multichain/toast/toast.test.tsx +++ b/ui/components/multichain/toast/toast.test.tsx @@ -12,10 +12,6 @@ const mockInternalAccount = createMockInternalAccount(); const CHAOS_ACCOUNT: InternalAccount = { ...mockInternalAccount, address: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', - balance: '0x152387ad22c3f0', - keyring: { - type: 'HD Key Tree', - }, }; const onActionClick = jest.fn(); diff --git a/ui/hooks/useMultichainSelector.test.ts b/ui/hooks/useMultichainSelector.test.ts index e4469a9f05db..0f66030ae100 100644 --- a/ui/hooks/useMultichainSelector.test.ts +++ b/ui/hooks/useMultichainSelector.test.ts @@ -60,6 +60,7 @@ describe('useMultichainSelector', () => { }); it('uses selectedAccount if account is not provided', () => { + // @ts-expect-error: intentionally testing without account const { result } = renderUseMultichainHook(getMultichainIsEvm, null); expect(result.current).toBe(true); diff --git a/ui/pages/remove-snap-account/snap-account-card.tsx b/ui/pages/remove-snap-account/snap-account-card.tsx index 117aa44c94e9..510d721b87c8 100644 --- a/ui/pages/remove-snap-account/snap-account-card.tsx +++ b/ui/pages/remove-snap-account/snap-account-card.tsx @@ -8,6 +8,7 @@ import { BlockSize, BorderRadius } from '../../helpers/constants/design-system'; import { Box } from '../../components/component-library'; import { AccountListItem } from '../../components/multichain'; import { mergeAccounts } from '../../components/multichain/account-list-menu/account-list-menu'; +import { MergedInternalAccount } from '../../selectors/selectors.types'; // Wrapper component of AccountListItem with proper styling and auto populating information for the selected account export const SnapAccountCard = ({ @@ -24,7 +25,7 @@ export const SnapAccountCard = ({ const account = mergedAccounts.find( (internalAccount: { address: string }) => internalAccount.address === address, - ); + ) as MergedInternalAccount; return ( =15 <18" - checksum: 10/0c8546a4e980c70a7d4a6dcc470fa4968ca36d3e05f8fa8974e980c2b02616ada8ed416aa49f2801f883c08455453b2386d393e3b41c853d2dd45c226f8c360f + checksum: 10/15711ddaa0007794cc23f9c02f6cfbee85aa1cf79a46468a0398404c295eef1511555ce6bd60a691081d33864d288ea8b309ee9ac9c4d6f277ab22e4d97cb76e languageName: node linkType: hard @@ -26518,7 +26540,7 @@ __metadata: "@metamask-institutional/types": "npm:^1.1.0" "@metamask/abi-utils": "npm:^2.0.2" "@metamask/account-watcher": "npm:^4.1.0" - "@metamask/accounts-controller": "npm:^17.2.0" + "@metamask/accounts-controller": "npm:^18.1.0" "@metamask/address-book-controller": "npm:^4.0.1" "@metamask/announcement-controller": "npm:^6.1.0" "@metamask/api-specs": "npm:^0.9.3"