diff --git a/app/components/UI/Bridge/_mocks_/initialState.ts b/app/components/UI/Bridge/_mocks_/initialState.ts index 75f1770530f..69972a0c442 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 5b6873b749f..f8c8134cee0 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 00ee2dcce22..47d9eb33df7 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 642a448224a..bd5d4722251 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 2b10f897f80..050d3b8463e 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 ed92091f927..f1e1649c075 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 61f87b8e080..57ded41b071 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 32edaf73e61..807c3efee06 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, ], ); diff --git a/app/components/Views/AccountPermissions/NetworkPermissionsConnected/NetworkPermissionsConnected.tsx b/app/components/Views/AccountPermissions/NetworkPermissionsConnected/NetworkPermissionsConnected.tsx index a9eae2f0a90..f3b69587b07 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/EditAccountName/EditAccountName.tsx b/app/components/Views/EditAccountName/EditAccountName.tsx index 2db10a9589c..28dd266673a 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/components/Views/NetworkSelector/useSwitchNetworks.ts b/app/components/Views/NetworkSelector/useSwitchNetworks.ts index c651e65185c..a7816bdf43a 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/Engine/controllers/transaction-controller/utils.test.ts b/app/core/Engine/controllers/transaction-controller/utils.test.ts index 8065a760ea1..54c4ab27659 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 50538959ca0..13b544718ca 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/core/RPCMethods/lib/ethereum-chain-utils.js b/app/core/RPCMethods/lib/ethereum-chain-utils.js index 9a6349b2df0..00ac9d3a129 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 b879fe1532e..6504ced6ad4 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); diff --git a/app/util/address/index.test.ts b/app/util/address/index.test.ts index 66af057d267..1d26437ac64 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 7b1e2351462..39eed9903a5 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: