diff --git a/.yarnrc.yml b/.yarnrc.yml index 252333917781..f4d8fc7fa471 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -114,15 +114,9 @@ npmAuditIgnoreAdvisories: # upon old versions of ethereumjs-utils. - 'ethereum-cryptography (deprecation)' - # Currently only dependent on deprecated @metamask/types as it is brought in - # by @metamask/keyring-api. Updating the dependency in keyring-api will - # remove this. - - '@metamask/types (deprecation)' - - # @metamask/keyring-api also depends on @metamask/snaps-ui which is - # deprecated. Replacing that dependency with @metamask/snaps-sdk will remove - # this. - - '@metamask/snaps-ui (deprecation)' + # Currently in use for the network list drag and drop functionality. + # Maintenance has stopped and the project will be archived in 2025. + - 'react-beautiful-dnd (deprecation)' npmRegistries: 'https://npm.pkg.github.com': diff --git a/shared/lib/trace.ts b/shared/lib/trace.ts index 5ca256371502..ab1deefd1cc5 100644 --- a/shared/lib/trace.ts +++ b/shared/lib/trace.ts @@ -32,6 +32,11 @@ const ID_DEFAULT = 'default'; const OP_DEFAULT = 'custom'; const tracesByKey: Map = new Map(); +const durationsByName: { [name: string]: number } = {}; + +if (process.env.IN_TEST && globalThis.stateHooks) { + globalThis.stateHooks.getCustomTraces = () => durationsByName; +} type PendingTrace = { end: (timestamp?: number) => void; @@ -155,9 +160,8 @@ export function endTrace(request: EndTraceRequest) { const { request: pendingRequest, startTime } = pendingTrace; const endTime = timestamp ?? getPerformanceTimestamp(); - const duration = endTime - startTime; - log('Finished trace', name, id, duration, { request: pendingRequest }); + logTrace(pendingRequest, startTime, endTime); } function traceCallback(request: TraceRequest, fn: TraceCallback): T { @@ -181,9 +185,7 @@ function traceCallback(request: TraceRequest, fn: TraceCallback): T { }, () => { const end = Date.now(); - const duration = end - start; - - log('Finished trace', name, duration, { error, request }); + logTrace(request, start, end, error); }, ) as T; }; @@ -242,6 +244,22 @@ function startSpan( }); } +function logTrace( + request: TraceRequest, + startTime: number, + endTime: number, + error?: unknown, +) { + const duration = endTime - startTime; + const { name } = request; + + if (process.env.IN_TEST) { + durationsByName[name] = duration; + } + + log('Finished trace', name, duration, { request, error }); +} + function getTraceId(request: TraceRequest) { return request.id ?? ID_DEFAULT; } diff --git a/test/e2e/benchmark.js b/test/e2e/benchmark.js index 738d766f8555..1f24a960d9eb 100755 --- a/test/e2e/benchmark.js +++ b/test/e2e/benchmark.js @@ -17,6 +17,16 @@ const FixtureBuilder = require('./fixture-builder'); const DEFAULT_NUM_SAMPLES = 20; const ALL_PAGES = Object.values(PAGES); +const CUSTOM_TRACES = { + backgroundConnect: 'Background Connect', + firstReactRender: 'First Render', + getState: 'Get State', + initialActions: 'Initial Actions', + loadScripts: 'Load Scripts', + setupStore: 'Setup Store', + uiStartup: 'UI Startup', +}; + async function measurePage(pageName) { let metrics; await withFixtures( @@ -32,6 +42,7 @@ async function measurePage(pageName) { await driver.findElement('[data-testid="account-menu-icon"]'); await driver.navigate(pageName); await driver.delay(1000); + metrics = await driver.collectMetrics(); }, ); @@ -79,7 +90,7 @@ async function profilePageLoad(pages, numSamples, retries) { runResults.push(result); } - if (runResults.some((result) => result.navigation.lenth > 1)) { + if (runResults.some((result) => result.navigation.length > 1)) { throw new Error(`Multiple navigations not supported`); } else if ( runResults.some((result) => result.navigation[0].type !== 'navigate') @@ -107,6 +118,10 @@ async function profilePageLoad(pages, numSamples, retries) { ), }; + for (const [key, name] of Object.entries(CUSTOM_TRACES)) { + result[key] = runResults.map((metrics) => metrics[name]); + } + results[pageName] = { min: minResult(result), max: maxResult(result), diff --git a/test/e2e/tests/network/multi-rpc.spec.ts b/test/e2e/tests/network/multi-rpc.spec.ts index c9fa95f986e9..af2ef47e93fb 100644 --- a/test/e2e/tests/network/multi-rpc.spec.ts +++ b/test/e2e/tests/network/multi-rpc.spec.ts @@ -396,12 +396,10 @@ describe('MultiRpc:', function (this: Suite) { await driver.delay(regularDelayMs); // go to advanced settigns - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Manage default settings', }); - await driver.delay(regularDelayMs); - await driver.clickElement({ text: 'General', }); @@ -420,23 +418,18 @@ describe('MultiRpc:', function (this: Suite) { tag: 'button', }); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Save', tag: 'button', }); - await driver.delay(regularDelayMs); - await driver.waitForSelector('[data-testid="category-back-button"]'); await driver.clickElement('[data-testid="category-back-button"]'); - await driver.waitForSelector( - '[data-testid="privacy-settings-back-button"]', - ); await driver.clickElement( '[data-testid="privacy-settings-back-button"]', ); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Done', tag: 'button', }); @@ -446,7 +439,7 @@ describe('MultiRpc:', function (this: Suite) { tag: 'button', }); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Done', tag: 'button', }); @@ -461,7 +454,17 @@ describe('MultiRpc:', function (this: Suite) { '“Arbitrum One” was successfully edited!', ); // Ensures popover backround doesn't kill test - await driver.delay(regularDelayMs); + await driver.assertElementNotPresent('.popover-bg'); + + // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#27870) + // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed + await driver.clickElementSafe({ tag: 'h6', text: 'Got it' }); + + await driver.assertElementNotPresent({ + tag: 'h6', + text: 'Got it', + }); + await driver.clickElement('[data-testid="network-display"]'); const arbitrumRpcUsed = await driver.findElement({ diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index a03a0d1cbd04..fb8aed3d28a6 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -1313,7 +1313,10 @@ function collectMetrics() { }); }); - return results; + return { + ...results, + ...window.stateHooks.getCustomTraces(), + }; } module.exports = { Driver, PAGES }; diff --git a/test/jest/mocks.ts b/test/jest/mocks.ts index ed89b487e3ab..be1120429290 100644 --- a/test/jest/mocks.ts +++ b/test/jest/mocks.ts @@ -180,12 +180,14 @@ export function createMockInternalAccount({ address = MOCK_DEFAULT_ADDRESS, type = EthAccountType.Eoa, keyringType = KeyringTypes.hd, + lastSelected = 0, snapOptions = undefined, }: { name?: string; address?: string; type?: string; keyringType?: string; + lastSelected?: number; snapOptions?: { enabled: boolean; name: string; @@ -228,6 +230,7 @@ export function createMockInternalAccount({ type: keyringType, }, snap: snapOptions, + lastSelected, }, options: {}, methods, diff --git a/types/global.d.ts b/types/global.d.ts index 95fb6c98547a..8078a3998bde 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -239,6 +239,7 @@ type HttpProvider = { }; type StateHooks = { + getCustomTraces?: () => { [name: string]: number }; getCleanAppState?: () => Promise; getLogs?: () => any[]; getMostRecentPersistedState?: () => any; diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts index 3bf65d98d19c..4db61d568f4a 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts @@ -1,3 +1,4 @@ +import { InternalAccount } from '@metamask/keyring-api'; import type { CurrencyDisplayProps } from '../../ui/currency-display/currency-display.component'; import type { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; @@ -5,6 +6,7 @@ export type UserPrefrencedCurrencyDisplayProps = OverridingUnion< CurrencyDisplayProps, { type?: PRIMARY | SECONDARY; + account?: InternalAccount; currency?: string; showEthLogo?: boolean; ethNumberOfDecimals?: string | number; diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index 4b5492091288..613b731d0a16 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -1,5 +1,6 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; import { EtherDenomination } from '../../../../shared/constants/common'; import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; import CurrencyDisplay from '../../ui/currency-display'; @@ -10,13 +11,14 @@ import { getMultichainCurrentNetwork, } from '../../../selectors/multichain'; import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; +import { getSelectedEvmInternalAccount } from '../../../selectors'; /* eslint-disable jsdoc/require-param-name */ // eslint-disable-next-line jsdoc/require-param /** @param {PropTypes.InferProps>} */ export default function UserPreferencedCurrencyDisplay({ 'data-testid': dataTestId, - account, + account: multichainAccount, ethNumberOfDecimals, fiatNumberOfDecimals, numberOfDecimals: propsNumberOfDecimals, @@ -28,6 +30,15 @@ export default function UserPreferencedCurrencyDisplay({ shouldCheckShowNativeToken, ...restProps }) { + // NOTE: When displaying currencies, we need the actual account to detect whether we're in a + // multichain world or EVM-only world. + // To preserve the original behavior of this component, we default to the lastly selected + // EVM accounts (when used in an EVM-only context). + // The caller has to pass the account in a multichain context to properly display the currency + // here (e.g for Bitcoin). + const evmAccount = useSelector(getSelectedEvmInternalAccount); + const account = multichainAccount ?? evmAccount; + const currentNetwork = useMultichainSelector( getMultichainCurrentNetwork, account, diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index c369ef0e89fd..2de787ef23c0 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -117,6 +117,7 @@ export const CoinOverview = ({ ///: END:ONLY_INCLUDE_IF + const account = useSelector(getSelectedAccount); const showNativeTokenAsMainBalanceRoute = getSpecificSettingsRoute( t, t('general'), @@ -254,6 +255,7 @@ export const CoinOverview = ({ {balanceToDisplay ? ( ({ setShowTestNetworks: () => mockSetShowTestNetworks, setActiveNetwork: () => mockSetActiveNetwork, toggleNetworkMenu: () => mockToggleNetworkMenu, + updateCustomNonce: () => mockUpdateCustomNonce, + setNextNonce: () => mockSetNextNonce, setNetworkClientIdForDomain: (network, id) => mockSetNetworkClientIdForDomain(network, id), })); @@ -206,6 +210,8 @@ describe('NetworkListMenu', () => { fireEvent.click(getByText(MAINNET_DISPLAY_NAME)); expect(mockToggleNetworkMenu).toHaveBeenCalled(); expect(mockSetActiveNetwork).toHaveBeenCalled(); + expect(mockUpdateCustomNonce).toHaveBeenCalled(); + expect(mockSetNextNonce).toHaveBeenCalled(); }); it('shows the correct selected network when networks share the same chain ID', () => { diff --git a/ui/components/multichain/network-list-menu/network-list-menu.tsx b/ui/components/multichain/network-list-menu/network-list-menu.tsx index 6dc4457cceb5..5376dc17859e 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.tsx +++ b/ui/components/multichain/network-list-menu/network-list-menu.tsx @@ -26,6 +26,8 @@ import { setEditedNetwork, grantPermittedChain, showPermittedNetworkToast, + updateCustomNonce, + setNextNonce, } from '../../../store/actions'; import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP, @@ -277,6 +279,8 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { network.rpcEndpoints[network.defaultRpcEndpointIndex]; dispatch(setActiveNetwork(networkClientId)); dispatch(toggleNetworkMenu()); + dispatch(updateCustomNonce('')); + dispatch(setNextNonce('')); if (permittedAccountAddresses.length > 0) { grantPermittedChain(selectedTabOrigin, network.chainId); diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index efbd781f943f..cf8348243238 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -6,6 +6,7 @@ import { captureMessage } from '@sentry/browser'; import { TransactionType } from '@metamask/transaction-controller'; import { createProjectLogger } from '@metamask/utils'; +import { CHAIN_IDS } from '../../../shared/constants/network'; import { addToken, addTransactionAndWaitForPublish, @@ -1284,6 +1285,21 @@ export const signAndSendTransactions = ( }, }, ); + if ( + [ + CHAIN_IDS.LINEA_MAINNET, + CHAIN_IDS.LINEA_GOERLI, + CHAIN_IDS.LINEA_SEPOLIA, + ].includes(chainId) + ) { + debugLog( + 'Delaying submitting trade tx to make Linea confirmation more likely', + ); + const waitPromise = new Promise((resolve) => + setTimeout(resolve, 5000), + ); + await waitPromise; + } } catch (e) { debugLog('Approve transaction failed', e); await dispatch(setSwapsErrorKey(SWAP_FAILED_ERROR)); diff --git a/ui/helpers/utils/util.js b/ui/helpers/utils/util.js index fe01bd15f5b1..eafc8e31bfe5 100644 --- a/ui/helpers/utils/util.js +++ b/ui/helpers/utils/util.js @@ -303,6 +303,25 @@ export function getAccountByAddress(accounts = [], targetAddress) { return accounts.find(({ address }) => address === targetAddress); } +/** + * Sort the given list of account their selecting order (descending). Meaning the + * first account of the sorted list will be the last selected account. + * + * @param {import('@metamask/keyring-api').InternalAccount[]} accounts - The internal accounts list. + * @returns {import('@metamask/keyring-api').InternalAccount[]} The sorted internal account list. + */ +export function sortSelectedInternalAccounts(accounts) { + // This logic comes from the `AccountsController`: + // TODO: Expose a free function from this controller and use it here + return accounts.sort((accountA, accountB) => { + // Sort by `.lastSelected` in descending order + return ( + (accountB.metadata.lastSelected ?? 0) - + (accountA.metadata.lastSelected ?? 0) + ); + }); +} + /** * Strips the following schemes from URL strings: * - http diff --git a/ui/helpers/utils/util.test.js b/ui/helpers/utils/util.test.js index dd2282efa531..d12a57675343 100644 --- a/ui/helpers/utils/util.test.js +++ b/ui/helpers/utils/util.test.js @@ -4,6 +4,7 @@ import { CHAIN_IDS } from '../../../shared/constants/network'; import { addHexPrefixToObjectValues } from '../../../shared/lib/swaps-utils'; import { toPrecisionWithoutTrailingZeros } from '../../../shared/lib/transactions-controller-utils'; import { MinPermissionAbstractionDisplayCount } from '../../../shared/constants/permissions'; +import { createMockInternalAccount } from '../../../test/jest/mocks'; import * as util from './util'; describe('util', () => { @@ -1259,4 +1260,52 @@ describe('util', () => { expect(result).toBe(0); }); }); + + describe('sortSelectedInternalAccounts', () => { + const account1 = createMockInternalAccount({ lastSelected: 1 }); + const account2 = createMockInternalAccount({ lastSelected: 2 }); + const account3 = createMockInternalAccount({ lastSelected: 3 }); + // We use a big "gap" here to make sure we're not only sorting with sequential indexes + const accountWithBigSelectedIndexGap = createMockInternalAccount({ + lastSelected: 108912379837, + }); + // We wanna make sure that negative indexes are also being considered properly + const accountWithNegativeSelectedIndex = createMockInternalAccount({ + lastSelected: -1, + }); + + const orderedAccounts = [account3, account2, account1]; + + it.each([ + { accounts: [account1, account2, account3] }, + { accounts: [account2, account3, account1] }, + { accounts: [account3, account2, account1] }, + ])('sorts accounts by descending order: $accounts', ({ accounts }) => { + const sortedAccount = util.sortSelectedInternalAccounts(accounts); + expect(sortedAccount).toStrictEqual(orderedAccounts); + }); + + it('sorts accounts with bigger gap', () => { + const accounts = [account1, accountWithBigSelectedIndexGap, account3]; + const sortedAccount = util.sortSelectedInternalAccounts(accounts); + expect(sortedAccount.length).toBeGreaterThan(0); + expect(sortedAccount).toHaveLength(accounts.length); + expect(sortedAccount[0]).toStrictEqual(accountWithBigSelectedIndexGap); + }); + + it('sorts accounts with negative `lastSelected` index', () => { + const accounts = [account1, accountWithNegativeSelectedIndex, account3]; + const sortedAccount = util.sortSelectedInternalAccounts(accounts); + expect(sortedAccount.length).toBeGreaterThan(0); // Required since we using `length - 1` + expect(sortedAccount).toHaveLength(accounts.length); + expect(sortedAccount[sortedAccount.length - 1]).toStrictEqual( + accountWithNegativeSelectedIndex, + ); + }); + + it('succeed with no accounts', () => { + const sortedAccount = util.sortSelectedInternalAccounts([]); + expect(sortedAccount).toStrictEqual([]); + }); + }); }); diff --git a/ui/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/pages/swaps/awaiting-swap/awaiting-swap.js index e7f47bc3f006..660f7ef4fcae 100644 --- a/ui/pages/swaps/awaiting-swap/awaiting-swap.js +++ b/ui/pages/swaps/awaiting-swap/awaiting-swap.js @@ -97,6 +97,9 @@ export default function AwaitingSwap({ const [trackedQuotesExpiredEvent, setTrackedQuotesExpiredEvent] = useState(false); + const destinationTokenSymbol = + usedQuote?.destinationTokenInfo?.symbol || swapMetaData?.token_to; + let feeinUnformattedFiat; if (usedQuote && swapsGasPrice) { @@ -107,7 +110,7 @@ export default function AwaitingSwap({ currentCurrency, conversionRate: usdConversionRate, tradeValue: usedQuote?.trade?.value, - sourceSymbol: swapMetaData?.token_from, + sourceSymbol: usedQuote?.sourceTokenInfo?.symbol, sourceAmount: usedQuote.sourceAmount, chainId, }); @@ -123,13 +126,14 @@ export default function AwaitingSwap({ const currentSmartTransactionsEnabled = useSelector( getCurrentSmartTransactionsEnabled, ); + const swapSlippage = swapMetaData?.slippage || usedQuote?.slippage; const sensitiveProperties = { - token_from: swapMetaData?.token_from, + token_from: swapMetaData?.token_from || usedQuote?.sourceTokenInfo?.symbol, token_from_amount: swapMetaData?.token_from_amount, - token_to: swapMetaData?.token_to, + token_to: destinationTokenSymbol, request_type: fetchParams?.balanceError ? 'Quote' : 'Order', - slippage: swapMetaData?.slippage, - custom_slippage: swapMetaData?.slippage === 2, + slippage: swapSlippage, + custom_slippage: swapSlippage === 2, gas_fees: feeinUnformattedFiat, is_hardware_wallet: hardwareWalletUsed, hardware_wallet_type: hardwareWalletType, @@ -137,7 +141,6 @@ export default function AwaitingSwap({ current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: smartTransactionsOptInStatus, }; - const baseNetworkUrl = rpcPrefs.blockExplorerUrl ?? SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? @@ -234,7 +237,7 @@ export default function AwaitingSwap({ className="awaiting-swap__amount-and-symbol" data-testid="awaiting-swap-amount-and-symbol" > - {swapMetaData?.token_to} + {destinationTokenSymbol} , ]); content = blockExplorerUrl && ( @@ -252,7 +255,7 @@ export default function AwaitingSwap({ key="swapTokenAvailable-2" className="awaiting-swap__amount-and-symbol" > - {`${tokensReceived || ''} ${swapMetaData?.token_to}`} + {`${tokensReceived || ''} ${destinationTokenSymbol}`} , ]); content = blockExplorerUrl && ( @@ -317,7 +320,7 @@ export default function AwaitingSwap({ } else if (errorKey) { await dispatch(navigateBackToBuildQuote(history)); } else if ( - isSwapsDefaultTokenSymbol(swapMetaData?.token_to, chainId) || + isSwapsDefaultTokenSymbol(destinationTokenSymbol, chainId) || swapComplete ) { history.push(DEFAULT_ROUTE); diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 1148e8d86468..b676da209046 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -231,8 +231,11 @@ export function getMultichainProviderConfig( return getMultichainNetwork(state, account).network; } -export function getMultichainCurrentNetwork(state: MultichainState) { - return getMultichainProviderConfig(state); +export function getMultichainCurrentNetwork( + state: MultichainState, + account?: InternalAccount, +) { + return getMultichainProviderConfig(state, account); } export function getMultichainNativeCurrency( diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 2059c3a4678d..09c062012731 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -67,6 +67,7 @@ import { shortenAddress, getAccountByAddress, getURLHostName, + sortSelectedInternalAccounts, } from '../helpers/utils/util'; import { @@ -388,6 +389,23 @@ export function getInternalAccount(state, accountId) { return state.metamask.internalAccounts.accounts[accountId]; } +export const getEvmInternalAccounts = createSelector( + getInternalAccounts, + (accounts) => { + return accounts.filter((account) => isEvmAccountType(account.type)); + }, +); + +export const getSelectedEvmInternalAccount = createSelector( + getEvmInternalAccounts, + (accounts) => { + // We should always have 1 EVM account (if not, it would be `undefined`, same + // as `getSelectedInternalAccount` selector. + const [evmAccountSelected] = sortSelectedInternalAccounts(accounts); + return evmAccountSelected; + }, +); + /** * Returns an array of internal accounts sorted by keyring. * diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 81a4b2532743..f150dc498ed0 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -1,6 +1,10 @@ import { deepClone } from '@metamask/snaps-utils'; import { ApprovalType } from '@metamask/controller-utils'; -import { EthAccountType, EthMethod } from '@metamask/keyring-api'; +import { + BtcAccountType, + EthAccountType, + EthMethod, +} from '@metamask/keyring-api'; import { TransactionStatus } from '@metamask/transaction-controller'; import mockState from '../../test/data/mock-state.json'; import { KeyringType } from '../../shared/constants/keyring'; @@ -36,6 +40,21 @@ const modifyStateWithHWKeyring = (keyring) => { return modifiedState; }; +const mockAccountsState = (accounts) => { + const accountsMap = accounts.reduce((map, account) => { + map[account.id] = account; + return map; + }, {}); + + return { + metamask: { + internalAccounts: { + accounts: accountsMap, + }, + }, + }; +}; + describe('Selectors', () => { describe('#getSelectedAddress', () => { it('returns undefined if selectedAddress is undefined', () => { @@ -2092,4 +2111,99 @@ describe('#getConnectedSitesList', () => { ).toStrictEqual('INITIALIZED'); }); }); + + describe('getEvmInternalAccounts', () => { + const account1 = createMockInternalAccount({ + keyringType: KeyringType.hd, + }); + const account2 = createMockInternalAccount({ + type: EthAccountType.Erc4337, + keyringType: KeyringType.snap, + }); + const account3 = createMockInternalAccount({ + keyringType: KeyringType.imported, + }); + const account4 = createMockInternalAccount({ + keyringType: KeyringType.ledger, + }); + const account5 = createMockInternalAccount({ + keyringType: KeyringType.trezor, + }); + const nonEvmAccount1 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + }); + const nonEvmAccount2 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + }); + + const evmAccounts = [account1, account2, account3, account4, account5]; + + it('returns all EVM accounts when only EVM accounts are present', () => { + const state = mockAccountsState(evmAccounts); + expect(selectors.getEvmInternalAccounts(state)).toStrictEqual( + evmAccounts, + ); + }); + + it('only returns EVM accounts when there are non-EVM accounts', () => { + const state = mockAccountsState([ + ...evmAccounts, + nonEvmAccount1, + nonEvmAccount2, + ]); + expect(selectors.getEvmInternalAccounts(state)).toStrictEqual( + evmAccounts, + ); + }); + + it('returns an empty array when there are no EVM accounts', () => { + const state = mockAccountsState([nonEvmAccount1, nonEvmAccount2]); + expect(selectors.getEvmInternalAccounts(state)).toStrictEqual([]); + }); + }); + + describe('getSelectedEvmInternalAccount', () => { + const account1 = createMockInternalAccount({ + lastSelected: 1, + }); + const account2 = createMockInternalAccount({ + lastSelected: 2, + }); + const account3 = createMockInternalAccount({ + lastSelected: 3, + }); + const nonEvmAccount1 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + lastSelected: 4, + }); + const nonEvmAccount2 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + lastSelected: 5, + }); + + it('returns the last selected EVM account', () => { + const state = mockAccountsState([account1, account2, account3]); + expect(selectors.getSelectedEvmInternalAccount(state)).toBe(account3); + }); + + it('returns the last selected EVM account when there are non-EVM accounts', () => { + const state = mockAccountsState([ + account1, + account2, + account3, + nonEvmAccount1, + nonEvmAccount2, + ]); + expect(selectors.getSelectedEvmInternalAccount(state)).toBe(account3); + }); + + it('returns `undefined` if there are no EVM accounts', () => { + const state = mockAccountsState([nonEvmAccount1, nonEvmAccount2]); + expect(selectors.getSelectedEvmInternalAccount(state)).toBe(undefined); + }); + }); }); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 91453590791c..9c5ab7ebb45e 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4020,7 +4020,7 @@ export function resolvePendingApproval( // Before closing the current window, check if any additional confirmations // are added as a result of this confirmation being accepted - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask,build-mmi) const { pendingApprovals } = await forceUpdateMetamaskState(_dispatch); if (Object.values(pendingApprovals).length === 0) { _dispatch(closeCurrentNotificationWindow());