From 47aef8dde60f35885e737ea9df78702fb709fb11 Mon Sep 17 00:00:00 2001 From: Bryan Fullam Date: Thu, 4 Dec 2025 21:36:31 -0800 Subject: [PATCH 1/3] fix: correct swaps network ordering cp:7.61.0 (#23686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixes the network ordering to include Tron between Solana and Base ## **Changelog** CHANGELOG entry: Updated swaps network picker ordering ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/23687 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Adds Tron to bridge networks, positions it after Solana in popularity ordering, and includes TRX in source network balance sorting and tests. > > - **Bridge UI**: > - Update `BridgeDestNetworksBar.tsx` `ChainPopularity` to include `TrxScope.Mainnet` after `SolScope.Mainnet` and shift subsequent rankings. > - **Sorting/Logic**: > - Extend `useSortedSourceNetworks` to compute/sort by Tron (`TrxScope.Mainnet`) fiat totals alongside Solana and Bitcoin. > - **Mocks/State**: > - Add Tron account, native asset (`tron:728126428/slip44:195`), balances, metadata, rates, and controller entries in `_mocks_/initialState.ts`. > - **Tests/Snapshots**: > - Update selectors and snapshots to display/select Tron across source/dest network and token selectors; adjust expectations to include `TrxScope.Mainnet`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ed48008b55e58ff28576633a1d2826b42c040faf. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../UI/Bridge/_mocks_/initialState.ts | 59 +++- .../BridgeDestNetworkSelector.test.tsx.snap | 106 ++++++ .../components/BridgeDestNetworksBar.tsx | 21 +- .../BridgeDestTokenSelector.test.tsx.snap | 54 +++ .../BridgeSourceNetworkSelector.test.tsx | 3 +- .../BridgeSourceNetworkSelector.test.tsx.snap | 193 +++++++++++ .../BridgeSourceTokenSelector.test.tsx.snap | 311 ++++++++++++++++++ .../useSortedSourceNetworks.test.ts.snap | 11 + .../useSortedSourceNetworks.ts | 14 +- 9 files changed, 759 insertions(+), 13 deletions(-) diff --git a/app/components/UI/Bridge/_mocks_/initialState.ts b/app/components/UI/Bridge/_mocks_/initialState.ts index 75f1770530fa..69972a0c4421 100644 --- a/app/components/UI/Bridge/_mocks_/initialState.ts +++ b/app/components/UI/Bridge/_mocks_/initialState.ts @@ -7,6 +7,8 @@ import { SolAccountType, BtcScope, BtcAccountType, + TrxScope, + TrxAccountType, } from '@metamask/keyring-api'; import { AccountWalletType, AccountGroupType } from '@metamask/account-api'; import { ethers } from 'ethers'; @@ -53,6 +55,11 @@ export const solanaToken2Address = export const btcNativeTokenAddress = 'bip122:000000000019d6689c085ae165831e93/slip44:0' as CaipAssetId; +// Tron account and tokens +export const trxAccountId = 'trxAccountId'; +export const trxAccountAddress = 'TN3W4Bb1JVHPiWJVm7d9q9qHGXSdoMrMrE'; +export const trxNativeTokenAddress = 'tron:728126428/slip44:195' as CaipAssetId; + export const initialState = { engine: { backgroundState: { @@ -100,6 +107,11 @@ export const initialState = { isActiveDest: true, isGaslessSwapEnabled: false, }, + [TrxScope.Mainnet]: { + isActiveSrc: true, + isActiveDest: true, + isGaslessSwapEnabled: false, + }, }, bip44DefaultPairs: { bip122: { @@ -259,6 +271,12 @@ export const initialState = { 'bip122:000000000019d6689c085ae165831e93/slip44:0' as const, isEvm: false as const, }, + [TrxScope.Mainnet]: { + chainId: TrxScope.Mainnet, + name: 'Tron', + nativeCurrency: 'tron:728126428/slip44:195' as const, + isEvm: false as const, + }, }, }, MultichainBalancesController: { @@ -279,12 +297,19 @@ export const initialState = { unit: 'BTC', }, }, + [trxAccountId]: { + [trxNativeTokenAddress]: { + amount: '500', + unit: 'TRX', + }, + }, }, }, MultichainAssetsController: { accountsAssets: { [solanaAccountId]: [solanaNativeTokenAddress, solanaToken2Address], [btcAccountId]: [btcNativeTokenAddress], + [trxAccountId]: [trxNativeTokenAddress], }, assetsMetadata: { [btcNativeTokenAddress]: { @@ -348,6 +373,19 @@ export const initialState = { }, ], }, + [trxNativeTokenAddress]: { + name: 'Tron', + symbol: 'TRX', + iconUrl: 'https://tron.network/static/images/logo.png', + fungible: true as const, + units: [ + { + name: 'Tron', + symbol: 'TRX', + decimals: 6, + }, + ], + }, }, }, MultichainAssetsRatesController: { @@ -364,6 +402,10 @@ export const initialState = { rate: '100000', // 1 BTC = 100000 USD conversionTime: 0, }, + [trxNativeTokenAddress]: { + rate: '0.10', // 1 TRX = 0.10 USD + conversionTime: 0, + }, }, }, AccountsController: { @@ -400,6 +442,16 @@ export const initialState = { lastSelected: 0, }, }, + [trxAccountId]: { + id: trxAccountId, + address: trxAccountAddress, + name: 'Account 4', + type: TrxAccountType.Eoa, + scopes: [TrxScope.Mainnet], + metadata: { + lastSelected: 0, + }, + }, }, }, }, @@ -428,7 +480,12 @@ export const initialState = { groupIndex: 0, }, }, - accounts: [evmAccountId, solanaAccountId, btcAccountId], + accounts: [ + evmAccountId, + solanaAccountId, + btcAccountId, + trxAccountId, + ], }, }, }, diff --git a/app/components/UI/Bridge/components/BridgeDestNetworkSelector/__snapshots__/BridgeDestNetworkSelector.test.tsx.snap b/app/components/UI/Bridge/components/BridgeDestNetworkSelector/__snapshots__/BridgeDestNetworkSelector.test.tsx.snap index 5b6873b749fb..f8c8134cee07 100644 --- a/app/components/UI/Bridge/components/BridgeDestNetworkSelector/__snapshots__/BridgeDestNetworkSelector.test.tsx.snap +++ b/app/components/UI/Bridge/components/BridgeDestNetworkSelector/__snapshots__/BridgeDestNetworkSelector.test.tsx.snap @@ -758,6 +758,112 @@ exports[`BridgeDestNetworkSelector renders with initial state and displays netwo + + + + + + + + + + + Tron + + + + + + + diff --git a/app/components/UI/Bridge/components/BridgeDestNetworksBar.tsx b/app/components/UI/Bridge/components/BridgeDestNetworksBar.tsx index 00ee2dcce22b..47d9eb33df73 100644 --- a/app/components/UI/Bridge/components/BridgeDestNetworksBar.tsx +++ b/app/components/UI/Bridge/components/BridgeDestNetworksBar.tsx @@ -30,7 +30,7 @@ import { selectChainId } from '../../../../selectors/networkController'; // Using ScrollView from react-native-gesture-handler to fix scroll issues with the bottom sheet import { ScrollView } from 'react-native-gesture-handler'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) -import { BtcScope, SolScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; import { BridgeViewMode } from '../types'; ///: END:ONLY_INCLUDE_IF const createStyles = (params: { theme: Theme }) => { @@ -69,16 +69,17 @@ export const ChainPopularity: Record = { ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) [BtcScope.Mainnet]: 3, [SolScope.Mainnet]: 4, + [TrxScope.Mainnet]: 5, ///: END:ONLY_INCLUDE_IF - [CHAIN_IDS.BASE]: 5, - [CHAIN_IDS.ARBITRUM]: 6, - [CHAIN_IDS.LINEA_MAINNET]: 7, - [CHAIN_IDS.POLYGON]: 8, - [CHAIN_IDS.AVALANCHE]: 9, - [CHAIN_IDS.OPTIMISM]: 10, - [CHAIN_IDS.ZKSYNC_ERA]: 11, - [NETWORKS_CHAIN_ID.SEI]: 12, - [NETWORKS_CHAIN_ID.MONAD]: 13, + [CHAIN_IDS.BASE]: 6, + [CHAIN_IDS.ARBITRUM]: 7, + [CHAIN_IDS.LINEA_MAINNET]: 8, + [CHAIN_IDS.POLYGON]: 9, + [CHAIN_IDS.AVALANCHE]: 10, + [CHAIN_IDS.OPTIMISM]: 11, + [CHAIN_IDS.ZKSYNC_ERA]: 12, + [NETWORKS_CHAIN_ID.SEI]: 13, + [NETWORKS_CHAIN_ID.MONAD]: 14, }; export const BridgeDestNetworksBar = () => { diff --git a/app/components/UI/Bridge/components/BridgeDestTokenSelector/__snapshots__/BridgeDestTokenSelector.test.tsx.snap b/app/components/UI/Bridge/components/BridgeDestTokenSelector/__snapshots__/BridgeDestTokenSelector.test.tsx.snap index 642a448224a2..bd5d4722251c 100644 --- a/app/components/UI/Bridge/components/BridgeDestTokenSelector/__snapshots__/BridgeDestTokenSelector.test.tsx.snap +++ b/app/components/UI/Bridge/components/BridgeDestTokenSelector/__snapshots__/BridgeDestTokenSelector.test.tsx.snap @@ -722,6 +722,60 @@ exports[`BridgeDestTokenSelector renders with initial state and displays tokens + + + + Tron + + + { optimismChainId, SolScope.Mainnet, BtcScope.Mainnet, + TrxScope.Mainnet, ]); // Should navigate back diff --git a/app/components/UI/Bridge/components/BridgeSourceNetworkSelector/__snapshots__/BridgeSourceNetworkSelector.test.tsx.snap b/app/components/UI/Bridge/components/BridgeSourceNetworkSelector/__snapshots__/BridgeSourceNetworkSelector.test.tsx.snap index 2b10f897f802..050d3b8463e0 100644 --- a/app/components/UI/Bridge/components/BridgeSourceNetworkSelector/__snapshots__/BridgeSourceNetworkSelector.test.tsx.snap +++ b/app/components/UI/Bridge/components/BridgeSourceNetworkSelector/__snapshots__/BridgeSourceNetworkSelector.test.tsx.snap @@ -1364,6 +1364,199 @@ exports[`BridgeSourceNetworkSelector renders with initial state and displays net + + + + + + + + + + + + + + + + + Tron + + + + + + $50 + + + + + + diff --git a/app/components/UI/Bridge/components/BridgeSourceTokenSelector/__snapshots__/BridgeSourceTokenSelector.test.tsx.snap b/app/components/UI/Bridge/components/BridgeSourceTokenSelector/__snapshots__/BridgeSourceTokenSelector.test.tsx.snap index ed92091f927c..f1e1649c0758 100644 --- a/app/components/UI/Bridge/components/BridgeSourceTokenSelector/__snapshots__/BridgeSourceTokenSelector.test.tsx.snap +++ b/app/components/UI/Bridge/components/BridgeSourceTokenSelector/__snapshots__/BridgeSourceTokenSelector.test.tsx.snap @@ -751,6 +751,28 @@ exports[`BridgeSourceTokenSelector renders with initial state and displays token /> + + +1 + + + + + + + + + + + + + + + + + + + + + + + TRX + + + + Tron + + + + + 50 USD + + + 500 TRX + + + + + + diff --git a/app/components/UI/Bridge/hooks/useSortedSourceNetworks/__snapshots__/useSortedSourceNetworks.test.ts.snap b/app/components/UI/Bridge/hooks/useSortedSourceNetworks/__snapshots__/useSortedSourceNetworks.test.ts.snap index 61f87b8e0809..57ded41b0715 100644 --- a/app/components/UI/Bridge/hooks/useSortedSourceNetworks/__snapshots__/useSortedSourceNetworks.test.ts.snap +++ b/app/components/UI/Bridge/hooks/useSortedSourceNetworks/__snapshots__/useSortedSourceNetworks.test.ts.snap @@ -48,5 +48,16 @@ exports[`useSortedSourceNetworks should sort networks by total fiat value in des "ticker": "BTC", "totalFiatValue": 1500, }, + { + "chainId": "tron:728126428", + "decimals": 6, + "imageSource": 1, + "isEvm": false, + "isTestnet": false, + "name": "Tron", + "nativeCurrency": "tron:728126428/slip44:195", + "ticker": "TRX", + "totalFiatValue": 50, + }, ] `; diff --git a/app/components/UI/Bridge/hooks/useSortedSourceNetworks/useSortedSourceNetworks.ts b/app/components/UI/Bridge/hooks/useSortedSourceNetworks/useSortedSourceNetworks.ts index 32edaf73e61c..807c3efee068 100644 --- a/app/components/UI/Bridge/hooks/useSortedSourceNetworks/useSortedSourceNetworks.ts +++ b/app/components/UI/Bridge/hooks/useSortedSourceNetworks/useSortedSourceNetworks.ts @@ -7,7 +7,7 @@ import { selectLastSelectedEvmAccount } from '../../../../../selectors/accountsC import { InternalAccount } from '@metamask/keyring-internal-api'; import { isNonEvmChainId } from '@metamask/bridge-controller'; import { useTokensWithBalance } from '../useTokensWithBalance'; -import { BtcScope, SolScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; export const useSortedSourceNetworks = () => { const enabledSourceChains = useSelector(selectEnabledSourceChains); @@ -68,6 +68,15 @@ export const useSortedSourceNetworks = () => { 0, ); + // Calculate total fiat value for Tron (native + tokens) + const trxTokensWithBalance = useTokensWithBalance({ + chainIds: [TrxScope.Mainnet], + }); + const trxFiatTotal = trxTokensWithBalance.reduce( + (sum, token) => sum + (token.tokenFiatAmount ?? 0), + 0, + ); + // Sort networks by total fiat value in descending order const sortedSourceNetworks = useMemo( () => @@ -79,6 +88,8 @@ export const useSortedSourceNetworks = () => { totalFiatValue = solFiatTotal; } else if (chain.chainId === BtcScope.Mainnet) { totalFiatValue = btcFiatTotal; + } else if (chain.chainId === TrxScope.Mainnet) { + totalFiatValue = trxFiatTotal; } else { totalFiatValue = getEvmChainTotalFiatValue(chain.chainId); } @@ -94,6 +105,7 @@ export const useSortedSourceNetworks = () => { getEvmChainTotalFiatValue, solFiatTotal, btcFiatTotal, + trxFiatTotal, ], ); From e47bcb558f136a0950c940555930708874490b2d Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 5 Dec 2025 17:15:49 +0800 Subject: [PATCH 2/3] feat: add new metrics key to transactions cp-7.61.0 (#23585) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds `account_hardware_type` to transaction events when the account is a hardware account. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MUL-1259?atlOrigin=eyJpIjoiNGE0MjcyYTY0YjE0NGEyNGEwNWVjZGYwYWMzYjFkMWEiLCJwIjoiaiJ9 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** No visual changes. ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Adds `account_hardware_type` to transaction metrics and standardizes QR account type to "QR Hardware", updating analytics usage and tests. > > - **Analytics/Transactions**: > - Add `account_hardware_type` to `generateDefaultTransactionMetrics` in `transaction-controller/utils.ts`; populated when `isHardwareAccount(from)` is true, else `null`. > - Update tests in `transaction-controller/utils.test.ts` to expect `account_hardware_type`, add hardware wallet scenarios, and refactor address mocks. > - **Address utilities**: > - Change `getAddressAccountType` to return `"QR Hardware"` for `qr` keyrings in `app/util/address/index.ts` and update related tests. > - **UI Analytics**: > - In `EditAccountName.tsx`, map `account_type` to `hardware` when type is `"QR Hardware"` for the ACCOUNT_RENAMED event. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8e4d25d61a4f5df08ea5648df64dd8cfd605f3b5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Charly Chevalier --- .../Views/EditAccountName/EditAccountName.tsx | 2 +- .../transaction-controller/utils.test.ts | 94 ++++++++++++++++++- .../transaction-controller/utils.ts | 16 +++- app/util/address/index.test.ts | 4 +- app/util/address/index.ts | 2 +- 5 files changed, 109 insertions(+), 9 deletions(-) diff --git a/app/components/Views/EditAccountName/EditAccountName.tsx b/app/components/Views/EditAccountName/EditAccountName.tsx index 2db10a9589cb..28dd266673a6 100644 --- a/app/components/Views/EditAccountName/EditAccountName.tsx +++ b/app/components/Views/EditAccountName/EditAccountName.tsx @@ -114,7 +114,7 @@ const EditAccountName = () => { const analyticsProperties = async () => { const accountType = getAddressAccountType(selectedAccount?.address); const account_type = - accountType === 'QR' ? 'hardware' : accountType; + accountType === 'QR Hardware' ? 'hardware' : accountType; return { account_type, chain_id: getDecimalChainId(chainId) }; }; const analyticsProps = await analyticsProperties(); diff --git a/app/core/Engine/controllers/transaction-controller/utils.test.ts b/app/core/Engine/controllers/transaction-controller/utils.test.ts index 8065a760ea10..54c4ab27659a 100644 --- a/app/core/Engine/controllers/transaction-controller/utils.test.ts +++ b/app/core/Engine/controllers/transaction-controller/utils.test.ts @@ -46,9 +46,16 @@ jest.mock('../../../Analytics/MetricsEventBuilder', () => ({ }, })); +const mockGetAddressAccountType = jest.fn().mockReturnValue('MetaMask'); +const mockIsValidHexAddress = jest.fn().mockReturnValue(true); +const mockIsHardwareAccount = jest.fn().mockReturnValue(false); + jest.mock('../../../../util/address', () => ({ - getAddressAccountType: jest.fn().mockReturnValue('MetaMask'), - isValidHexAddress: jest.fn().mockReturnValue(true), + ...jest.requireActual('../../../../util/address'), + getAddressAccountType: (...args: unknown[]) => + mockGetAddressAccountType(...args), + isValidHexAddress: (...args: unknown[]) => mockIsValidHexAddress(...args), + isHardwareAccount: (...args: unknown[]) => mockIsHardwareAccount(...args), })); jest.mock('../../../../util/rpc-domain-utils', () => ({ @@ -295,6 +302,10 @@ describe('generateDefaultTransactionMetrics', () => { mockEventBuilder, ); + mockGetAddressAccountType.mockReturnValue('MetaMask'); + mockIsValidHexAddress.mockReturnValue(true); + mockIsHardwareAccount.mockReturnValue(false); + mockNativeBalance('0x1', FROM_ADDRESS_MOCK, '0x0000000000000000000'); }); @@ -309,6 +320,7 @@ describe('generateDefaultTransactionMetrics', () => { metametricsEvent: mockMetametricsEvent, properties: expect.objectContaining({ account_eip7702_upgraded: undefined, + account_hardware_type: null, account_type: 'MetaMask', additional_property: 'test_value', chain_id: '0x1', @@ -347,6 +359,7 @@ describe('generateDefaultTransactionMetrics', () => { metametricsEvent: mockMetametricsEvent, properties: expect.objectContaining({ account_eip7702_upgraded: undefined, + account_hardware_type: null, account_type: 'MetaMask', chain_id: '0x1', dapp_host_name: 'N/A', @@ -770,6 +783,7 @@ describe('generateDefaultTransactionMetrics', () => { expect(metrics.properties).toEqual( expect.objectContaining({ account_eip7702_upgraded: undefined, + account_hardware_type: null, account_type: 'MetaMask', api_method: 'wallet_sendCalls', batch_transaction_count: 2, @@ -808,6 +822,7 @@ describe('generateDefaultTransactionMetrics', () => { expect(metrics.properties).toEqual( expect.objectContaining({ account_eip7702_upgraded: undefined, + account_hardware_type: null, account_type: 'MetaMask', chain_id: '0xaa36a7', dapp_host_name: 'metamask', @@ -848,6 +863,7 @@ describe('generateDefaultTransactionMetrics', () => { expect(metrics.properties).toEqual( expect.objectContaining({ account_eip7702_upgraded: undefined, + account_hardware_type: null, account_type: 'MetaMask', chain_id: '0xaa36a7', dapp_host_name: 'metamask', @@ -884,6 +900,7 @@ describe('generateDefaultTransactionMetrics', () => { expect.objectContaining({ account_eip7702_upgraded: '0x63c0c19a282a1b52b07dd5a65b58948a07dae32b', + account_hardware_type: null, account_type: 'MetaMask', api_method: 'wallet_sendCalls', batch_transaction_count: 2, @@ -908,4 +925,77 @@ describe('generateDefaultTransactionMetrics', () => { ); }); }); + + describe('hardware wallet metrics', () => { + it('sets account_hardware_type to Ledger for Ledger hardware wallet', async () => { + mockIsHardwareAccount.mockReturnValue(true); + mockGetAddressAccountType.mockReturnValue('Ledger'); + + const result = await generateDefaultTransactionMetrics( + mockMetametricsEvent, + mockTransactionMeta as TransactionMeta, + mockEventHandlerRequest as TransactionEventHandlerRequest, + ); + + expect(result.properties.account_type).toBe('Ledger'); + expect(result.properties.account_hardware_type).toBe('Ledger'); + }); + + it('sets account_hardware_type to QR Hardware for QR hardware wallet', async () => { + mockIsHardwareAccount.mockReturnValue(true); + mockGetAddressAccountType.mockReturnValue('QR Hardware'); + + const result = await generateDefaultTransactionMetrics( + mockMetametricsEvent, + mockTransactionMeta as TransactionMeta, + mockEventHandlerRequest as TransactionEventHandlerRequest, + ); + + expect(result.properties.account_type).toBe('QR Hardware'); + expect(result.properties.account_hardware_type).toBe('QR Hardware'); + }); + + it('sets account_hardware_type to null for non-hardware wallet', async () => { + mockIsHardwareAccount.mockReturnValue(false); + mockGetAddressAccountType.mockReturnValue('MetaMask'); + + const result = await generateDefaultTransactionMetrics( + mockMetametricsEvent, + mockTransactionMeta as TransactionMeta, + mockEventHandlerRequest as TransactionEventHandlerRequest, + ); + + expect(result.properties.account_type).toBe('MetaMask'); + expect(result.properties.account_hardware_type).toBeNull(); + }); + + it('sets account_type to unknown when from address is invalid', async () => { + mockIsValidHexAddress.mockReturnValue(false); + + const result = await generateDefaultTransactionMetrics( + mockMetametricsEvent, + mockTransactionMeta as TransactionMeta, + mockEventHandlerRequest as TransactionEventHandlerRequest, + ); + + expect(result.properties.account_type).toBe('unknown'); + expect(result.properties.account_hardware_type).toBeNull(); + }); + + it('defaults to unknown account_type when getAddressAccountType throws', async () => { + mockIsValidHexAddress.mockReturnValue(true); + mockGetAddressAccountType.mockImplementation(() => { + throw new Error('Wallet locked'); + }); + + const result = await generateDefaultTransactionMetrics( + mockMetametricsEvent, + mockTransactionMeta as TransactionMeta, + mockEventHandlerRequest as TransactionEventHandlerRequest, + ); + + expect(result.properties.account_type).toBe('unknown'); + expect(result.properties.account_hardware_type).toBeNull(); + }); + }); }); diff --git a/app/core/Engine/controllers/transaction-controller/utils.ts b/app/core/Engine/controllers/transaction-controller/utils.ts index 50538959ca08..13b544718ca5 100644 --- a/app/core/Engine/controllers/transaction-controller/utils.ts +++ b/app/core/Engine/controllers/transaction-controller/utils.ts @@ -27,6 +27,7 @@ import type { } from './types'; import { getAddressAccountType, + isHardwareAccount, isValidHexAddress, } from '../../../../util/address'; import { hasTransactionType } from '../../../../components/Views/confirmations/utils/transaction'; @@ -192,12 +193,20 @@ export async function generateDefaultTransactionMetrics( const { from } = txParams || {}; let accountType = 'unknown'; + let accountHardwareType: string | null = null; // Fails if wallet locked try { - accountType = isValidHexAddress(from) - ? getAddressAccountType(from) - : accountType; + if (isValidHexAddress(from)) { + // Get account type based on the keyring associated with + // this address. + accountType = getAddressAccountType(from); + + // Also populate this one for HW accounts. + if (isHardwareAccount(from)) { + accountHardwareType = accountType; + } + } } catch { // Intentionally empty } @@ -212,6 +221,7 @@ export async function generateDefaultTransactionMetrics( ...batchProperties, ...gasFeeProperties, account_type: accountType, + account_hardware_type: accountHardwareType, chain_id: chainId, dapp_host_name: origin ?? 'N/A', error: error?.message, diff --git a/app/util/address/index.test.ts b/app/util/address/index.test.ts index 66af057d267b..1d26437ac649 100644 --- a/app/util/address/index.test.ts +++ b/app/util/address/index.test.ts @@ -517,8 +517,8 @@ describe('getAddressAccountType', () => { 'Invalid address: undefined', ); }); - it('should return QR if address is from a keyring type qr', () => { - expect(getAddressAccountType(mockQrKeyringAddress)).toBe('QR'); + it('should return QR Hardware if address is from a keyring type qr', () => { + expect(getAddressAccountType(mockQrKeyringAddress)).toBe('QR Hardware'); }); it('should return imported if address is from a keyring type simple', () => { expect(getAddressAccountType(mockSimpleKeyringAddress)).toBe('Imported'); diff --git a/app/util/address/index.ts b/app/util/address/index.ts index 7b1e2351462f..39eed9903a54 100644 --- a/app/util/address/index.ts +++ b/app/util/address/index.ts @@ -462,7 +462,7 @@ export function getAddressAccountType(address: string) { if (targetKeyring) { switch (targetKeyring.type) { case ExtendedKeyringTypes.qr: - return 'QR'; + return 'QR Hardware'; case ExtendedKeyringTypes.simple: return 'Imported'; case ExtendedKeyringTypes.ledger: From 934aff09960ba15bc8258c2ada2df96bd6accfbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20=C5=81ucka?= <5708018+PatrykLucka@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:51:58 +0100 Subject: [PATCH 3/3] refactor: update network switching metrics to include chain ID and custom network status (#23613) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds additional properties to the NETWORK_SWITCHED analytics event to improve network switching tracking. The changes include: 1. Adding from_network and to_network as chain IDs (hex format) instead of network names 2. Adding custom_network boolean flag to indicate if the target network is a custom (non-popular) network Removing the selectedNetworkName parameter from useSwitchNetworks hook as it's no longer needed These changes apply to network switching from: - Network Selector modal - Network Permissions Connected component (dApp permission flow) - RPC method calls (wallet_switchEthereumChain) ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TMCU-213 ## **Manual testing steps** ```gherkin Feature: Network Switched Analytics Event Scenario: user switches network from Network Selector Given user is on any screen with network selector accessible When user opens the network selector and switches to a different network Then the NETWORK_SWITCHED event should contain chain_id, from_network, to_network, and custom_network properties Scenario: user switches network via dApp permission Given user is connected to a dApp with network permissions When user switches network through the network permissions modal Then the NETWORK_SWITCHED event should contain chain_id, from_network (hex), to_network (hex), and custom_network (boolean) Scenario: dApp requests network switch via RPC Given user is connected to a dApp When the dApp calls wallet_switchEthereumChain Then the NETWORK_SWITCHED event should include from_network, to_network, and custom_network properties ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Unifies NETWORK_SWITCHED analytics to report from/to chain IDs and a custom_network flag across UI flows and RPC, with tests updated accordingly. > > - **Analytics/metrics** > - Standardize `MetaMetricsEvents.NETWORK_SWITCHED` properties across network switching flows: > - `from_network` and `to_network` now use chain IDs (hex) instead of names. > - Add `custom_network` boolean based on `POPULAR_NETWORK_CHAIN_IDS`. > - Ensure `chain_id` reflects the target chain (decimal where applicable). > - Apply changes in: > - `app/components/Views/AccountPermissions/.../NetworkPermissionsConnected.tsx` (remove `selectProviderConfig`; track using `evmChainId` → `targetChainId`). > - `app/components/Views/NetworkSelector/useSwitchNetworks.ts` (remove `selectedNetworkName`; track with `selectedChainId` → `chainId`/built-ins; add popularity check). > - `app/core/RPCMethods/lib/ethereum-chain-utils.js` (include `from_network`, `to_network`, `custom_network`). > - **Tests** > - Update `ethereum-chain-utils.test.ts` to assert new analytics fields (`from_network`, `to_network`, `custom_network`). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit df07c0be25e78345a74fd581f815662cb198e523. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../NetworkPermissionsConnected.tsx | 11 ++++---- .../NetworkSelector/useSwitchNetworks.ts | 25 ++++++++----------- .../RPCMethods/lib/ethereum-chain-utils.js | 5 ++++ .../lib/ethereum-chain-utils.test.ts | 5 ++++ 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/app/components/Views/AccountPermissions/NetworkPermissionsConnected/NetworkPermissionsConnected.tsx b/app/components/Views/AccountPermissions/NetworkPermissionsConnected/NetworkPermissionsConnected.tsx index a9eae2f0a90b..f3b69587b07a 100644 --- a/app/components/Views/AccountPermissions/NetworkPermissionsConnected/NetworkPermissionsConnected.tsx +++ b/app/components/Views/AccountPermissions/NetworkPermissionsConnected/NetworkPermissionsConnected.tsx @@ -11,8 +11,6 @@ import { AccountPermissionsScreens } from '../AccountPermissions.types'; import { MetaMetricsEvents } from '../../../../core/Analytics'; import Routes from '../../../../constants/navigation/Routes'; import { - selectProviderConfig, - ProviderConfig, selectEvmChainId, selectEvmNetworkConfigurationsByChainId, } from '../../../../selectors/networkController'; @@ -48,6 +46,7 @@ import { getCaip25Caveat } from '../../../../core/Permissions'; import { getPermittedEthChainIds } from '@metamask/chain-agnostic-permission'; import { toHex } from '@metamask/controller-utils'; import { parseCaipChainId } from '@metamask/utils'; +import { POPULAR_NETWORK_CHAIN_IDS } from '../../../../constants/popular-networks'; // Needs to be updated to handle non-evm const NetworkPermissionsConnected = ({ @@ -59,7 +58,6 @@ const NetworkPermissionsConnected = ({ const { navigate } = useNavigation(); const { trackEvent, createEventBuilder } = useMetrics(); - const providerConfig: ProviderConfig = useSelector(selectProviderConfig); const evmChainId = useSelector(selectEvmChainId); const evmCaipChainId = `eip155:${parseInt(evmChainId, 16)}`; @@ -141,12 +139,15 @@ const NetworkPermissionsConnected = ({ const theNetworkName = handleNetworkSwitch(reference); if (theNetworkName) { + const targetChainId = toHex(reference); trackEvent( createEventBuilder(MetaMetricsEvents.NETWORK_SWITCHED) .addProperties({ chain_id: reference, - from_network: providerConfig?.nickname || theNetworkName, - to_network: theNetworkName, + from_network: evmChainId, + to_network: targetChainId, + custom_network: + !POPULAR_NETWORK_CHAIN_IDS.has(targetChainId), }) .build(), ); diff --git a/app/components/Views/NetworkSelector/useSwitchNetworks.ts b/app/components/Views/NetworkSelector/useSwitchNetworks.ts index c651e65185c7..a7816bdf43a5 100644 --- a/app/components/Views/NetworkSelector/useSwitchNetworks.ts +++ b/app/components/Views/NetworkSelector/useSwitchNetworks.ts @@ -66,7 +66,6 @@ export function useSwitchNetworks({ domainIsConnectedDapp = false, origin = '', selectedChainId, - selectedNetworkName, dismissModal, closeRpcModal, parentSpan, @@ -109,12 +108,8 @@ export function useSwitchNetworks({ const { MultichainNetworkController, SelectedNetworkController } = Engine.context; - const { - name: nickname, - chainId, - rpcEndpoints, - defaultRpcEndpointIndex, - } = networkConfiguration; + const { chainId, rpcEndpoints, defaultRpcEndpointIndex } = + networkConfiguration; const networkConfigurationId = rpcEndpoints[defaultRpcEndpointIndex].networkClientId; @@ -154,8 +149,9 @@ export function useSwitchNetworks({ createEventBuilder(MetaMetricsEvents.NETWORK_SWITCHED) .addProperties({ chain_id: getDecimalChainId(chainId), - from_network: selectedNetworkName, - to_network: nickname, + from_network: selectedChainId, + to_network: chainId, + custom_network: !POPULAR_NETWORK_CHAIN_IDS.has(chainId), }) .build(), ); @@ -163,7 +159,7 @@ export function useSwitchNetworks({ [ domainIsConnectedDapp, origin, - selectedNetworkName, + selectedChainId, trackEvent, createEventBuilder, parentSpan, @@ -225,12 +221,14 @@ export function useSwitchNetworks({ endTrace({ name: TraceName.SwitchBuiltInNetwork }); endTrace({ name: TraceName.NetworkSwitch }); + const toChainId = BUILT_IN_NETWORKS[type].chainId; trackEvent( createEventBuilder(MetaMetricsEvents.NETWORK_SWITCHED) .addProperties({ - chain_id: getDecimalChainId(selectedChainId), - from_network: selectedNetworkName, - to_network: type, + chain_id: getDecimalChainId(toChainId), + from_network: selectedChainId, + to_network: toChainId, + custom_network: !POPULAR_NETWORK_CHAIN_IDS.has(toChainId), }) .build(), ); @@ -241,7 +239,6 @@ export function useSwitchNetworks({ networkConfigurations, setTokenNetworkFilter, selectedChainId, - selectedNetworkName, trackEvent, createEventBuilder, parentSpan, diff --git a/app/core/RPCMethods/lib/ethereum-chain-utils.js b/app/core/RPCMethods/lib/ethereum-chain-utils.js index 9a6349b2df09..00ac9d3a1299 100644 --- a/app/core/RPCMethods/lib/ethereum-chain-utils.js +++ b/app/core/RPCMethods/lib/ethereum-chain-utils.js @@ -15,6 +15,7 @@ import { MetaMetrics, MetaMetricsEvents } from '../../../core/Analytics'; import { MetricsEventBuilder } from '../../../core/Analytics/MetricsEventBuilder'; import Engine from '../../Engine'; import { isSnapId } from '@metamask/snaps-utils'; +import { POPULAR_NETWORK_CHAIN_IDS } from '../../../constants/popular-networks'; const EVM_NATIVE_TOKEN_DECIMALS = 18; @@ -278,10 +279,14 @@ export async function switchToNetwork({ networkClientId, ); + const fromChainId = hooks.fromNetworkConfiguration?.chainId; const analyticsParams = { chain_id: getDecimalChainId(chainId), source: 'Custom Network API', symbol: nativeCurrency || 'ETH', + from_network: fromChainId, + to_network: chainId, + custom_network: !POPULAR_NETWORK_CHAIN_IDS.has(chainId), ...analytics, }; diff --git a/app/core/RPCMethods/lib/ethereum-chain-utils.test.ts b/app/core/RPCMethods/lib/ethereum-chain-utils.test.ts index b879fe1532ee..6504ced6ad46 100644 --- a/app/core/RPCMethods/lib/ethereum-chain-utils.test.ts +++ b/app/core/RPCMethods/lib/ethereum-chain-utils.test.ts @@ -39,12 +39,14 @@ describe('switchToNetwork', () => { build: jest.fn().mockReturnValue(mockMetricsBuilderBuild), }); + const fromChainId = '0x89'; const mockHooks = { getCaveat: jest .fn() .mockReturnValue({ value: getDefaultCaip25CaveatValue() }), requestPermittedChainsPermissionIncrementalForOrigin: jest.fn(), hasApprovalRequestsForOrigin: jest.fn(), + fromNetworkConfiguration: { chainId: fromChainId }, }; const chainId = '0x1'; @@ -82,6 +84,9 @@ describe('switchToNetwork', () => { chain_id: '1', source: 'Custom Network API', symbol: 'ETH', + from_network: fromChainId, + to_network: chainId, + custom_network: false, test: 'test', }); expect(mockTrackEvent).toHaveBeenCalledWith(mockMetricsBuilderBuild);