diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index b2b5805fca2..61bf9611cee 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -2665,7 +2665,7 @@ describe('BridgeController', function () { it('should track the PageViewed event', () => { bridgeController.trackUnifiedSwapBridgeEvent( UnifiedSwapBridgeEventName.PageViewed, - { abc: 1 }, + {}, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -2881,6 +2881,7 @@ describe('BridgeController', function () { chain_id_destination: formatChainIdToCaip(1), custom_slippage: false, is_hardware_wallet: false, + account_hardware_type: null, slippage_limit: 0.5, usd_quoted_gas: 1, gas_included: false, @@ -2920,6 +2921,7 @@ describe('BridgeController', function () { usd_amount_source: 100, stx_enabled: false, is_hardware_wallet: false, + account_hardware_type: null, swap_type: MetricsSwapType.CROSSCHAIN, provider: 'provider_bridge', price_impact: 6, @@ -2962,6 +2964,7 @@ describe('BridgeController', function () { usd_amount_source: 100, stx_enabled: false, is_hardware_wallet: false, + account_hardware_type: null, swap_type: MetricsSwapType.CROSSCHAIN, chain_id_destination: formatChainIdToCaip(ChainId.SOLANA), token_symbol_destination: 'USDC', @@ -3002,6 +3005,7 @@ describe('BridgeController', function () { { error_message: 'Failed to submit tx', is_hardware_wallet: false, + account_hardware_type: null, usd_quoted_gas: 1, gas_included: false, gas_included_7702: false, diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 42ec4f0e95f..e8873b01876 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -67,10 +67,10 @@ import { } from './utils/metrics/constants'; import { formatProviderLabel, + getAccountHardwareType, getRequestParams, getSwapTypeFromQuote, isCustomSlippage, - isHardwareWallet, toInputChangedPropertyKey, toInputChangedPropertyValue, } from './utils/metrics/properties'; @@ -913,15 +913,18 @@ export class BridgeController extends StaticIntervalPollingController => { + const accountHardwareType = getAccountHardwareType( + this.#getMultichainSelectedAccount(), + ); + return { slippage_limit: this.state.quoteRequest.slippage, swap_type: getSwapTypeFromQuote(this.state.quoteRequest), custom_slippage: isCustomSlippage(this.state.quoteRequest.slippage), + account_hardware_type: accountHardwareType, + is_hardware_wallet: accountHardwareType !== null, }; }; @@ -959,9 +962,14 @@ export class BridgeController extends StaticIntervalPollingController { }); }); + describe('getAccountHardwareType', () => { + it('returns null for non-hardware accounts', () => { + expect( + getAccountHardwareType({ + metadata: { + keyring: { + type: 'HD Key Tree', + }, + }, + } as never), + ).toBeNull(); + expect(isHardwareWallet(undefined)).toBe(false); + }); + + it.each([ + ['Ledger Hardware', 'Ledger'], + ['Trezor Hardware', 'Trezor'], + ['QR Hardware Wallet Device', 'QR Hardware'], + ['Lattice Hardware', 'Lattice'], + ] as const)('maps %s to %s', (keyringType, expected) => { + const account = { + metadata: { + keyring: { + type: keyringType, + }, + }, + } as never; + + expect(getAccountHardwareType(account)).toBe(expected); + expect(isHardwareWallet(account)).toBe(true); + }); + }); + describe('getRequestParams', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/bridge-controller/src/utils/metrics/properties.ts b/packages/bridge-controller/src/utils/metrics/properties.ts index 8f3ee9afe4d..0a145244167 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.ts @@ -2,6 +2,7 @@ import type { AccountsControllerState } from '@metamask/accounts-controller'; import { MetricsSwapType } from './constants'; import type { + AccountHardwareType, InputKeys, InputValues, QuoteWarning, @@ -106,10 +107,28 @@ export const getRequestParams = ({ }; }; +export const getAccountHardwareType = ( + selectedAccount?: AccountsControllerState['internalAccounts']['accounts'][string], +): AccountHardwareType => { + // Unified bridge analytics only support the schema enum values for hardware accounts. + switch (selectedAccount?.metadata?.keyring.type) { + case 'Ledger Hardware': + return 'Ledger'; + case 'Trezor Hardware': + return 'Trezor'; + case 'QR Hardware Wallet Device': + return 'QR Hardware'; + case 'Lattice Hardware': + return 'Lattice'; + default: + return null; + } +}; + export const isHardwareWallet = ( selectedAccount?: AccountsControllerState['internalAccounts']['accounts'][string], ) => { - return selectedAccount?.metadata?.keyring.type?.includes('Hardware') ?? false; + return getAccountHardwareType(selectedAccount) !== null; }; /** diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index 9d8504c106a..e8b041baacd 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -22,12 +22,20 @@ export type RequestParams = { token_address_destination: CaipAssetType | null; }; +export type AccountHardwareType = + | 'Ledger' + | 'Trezor' + | 'QR Hardware' + | 'Lattice' + | null; + export type RequestMetadata = { slippage_limit?: number; // undefined === auto custom_slippage: boolean; usd_amount_source: number; // Use quoteResponse when available stx_enabled: boolean; is_hardware_wallet: boolean; + account_hardware_type: AccountHardwareType; swap_type: MetricsSwapType; security_warnings: string[]; }; @@ -167,7 +175,10 @@ type RequiredEventContextFromClientBase = { Pick & Pick< RequestMetadata, - 'stx_enabled' | 'usd_amount_source' | 'is_hardware_wallet' + | 'stx_enabled' + | 'usd_amount_source' + | 'is_hardware_wallet' + | 'account_hardware_type' > & Pick< RequestParams, @@ -256,7 +267,11 @@ export type RequiredEventContextFromClient = { */ export type EventPropertiesFromControllerState = { [UnifiedSwapBridgeEventName.ButtonClicked]: RequestParams; - [UnifiedSwapBridgeEventName.PageViewed]: RequestParams; + [UnifiedSwapBridgeEventName.PageViewed]: RequestParams & + Omit< + RequestMetadata, + 'stx_enabled' | 'usd_amount_source' | 'security_warnings' + >; [UnifiedSwapBridgeEventName.InputChanged]: { input: InputKeys; input_value: string; diff --git a/packages/bridge-status-controller/src/bridge-status-controller.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.test.ts index 5d4469f177f..f8ee26f8c38 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -2109,7 +2109,7 @@ describe('BridgeStatusController', () => { id: 'test-snap', }, keyring: { - type: 'Hardware', + type: 'QR Hardware Wallet Device', }, }, options: { scope: 'solana-chain-id' }, diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 1b444f28f09..32eeba46493 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -13,10 +13,10 @@ import { StatusTypes, UnifiedSwapBridgeEventName, formatChainIdToCaip, + getAccountHardwareType, isCrossChain, isTronChainId, isEvmTxData, - isHardwareWallet, MetricsActionType, MetaMetricsSwapsEventSource, isBitcoinTrade, @@ -260,7 +260,13 @@ export class BridgeStatusController extends StaticIntervalPollingController { usd_amount_source: 2000, swap_type: 'crosschain', is_hardware_wallet: false, + account_hardware_type: null, stx_enabled: false, }); }); @@ -894,6 +895,29 @@ describe('metrics utils', () => { hardwareWalletAccount, ); expect(result.is_hardware_wallet).toBe(true); + expect(result.account_hardware_type).toBe('Ledger'); + }); + + it('should keep Lattice accounts as Lattice', () => { + const latticeAccount = { + id: 'test-account', + type: 'eip155:eoa' as const, + address: '0xaccount1', + options: {}, + metadata: { + name: 'Test Account', + importTime: 1234567890, + keyring: { + type: 'Lattice Hardware', + }, + }, + scopes: [], + methods: [], + }; + + const result = getRequestMetadataFromHistory(mockHistoryItem, latticeAccount); + expect(result.is_hardware_wallet).toBe(true); + expect(result.account_hardware_type).toBe('Lattice'); }); it('should handle missing pricing data', () => { @@ -999,6 +1023,7 @@ describe('metrics utils', () => { 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', custom_slippage: false, is_hardware_wallet: false, + account_hardware_type: null, swap_type: MetricsSwapType.SINGLE, security_warnings: [], price_impact: 0, diff --git a/packages/bridge-status-controller/src/utils/metrics.ts b/packages/bridge-status-controller/src/utils/metrics.ts index 462a92025aa..082d2b0be2c 100644 --- a/packages/bridge-status-controller/src/utils/metrics.ts +++ b/packages/bridge-status-controller/src/utils/metrics.ts @@ -4,19 +4,20 @@ import type { AccountsControllerState } from '@metamask/accounts-controller'; import { StatusTypes, + getAccountHardwareType, formatChainIdToHex, isEthUsdt, formatChainIdToCaip, formatProviderLabel, isCustomSlippage, getSwapType, - isHardwareWallet, formatAddressToAssetId, MetricsActionType, MetricsSwapType, MetaMetricsSwapsEventSource, } from '@metamask/bridge-controller'; import type { + AccountHardwareType, QuoteFetchData, QuoteMetadata, QuoteResponse, @@ -183,7 +184,7 @@ export const getPriceImpactFromQuote = ( export const getPreConfirmationPropertiesFromQuote = ( quoteResponse: QuoteResponse & Partial, isStxEnabledOnClient: boolean, - isHardwareAccount: boolean, + accountHardwareType: AccountHardwareType, location: MetaMetricsSwapsEventSource = MetaMetricsSwapsEventSource.MainView, abTests?: Record, ) => { @@ -195,7 +196,8 @@ export const getPreConfirmationPropertiesFromQuote = ( token_symbol_source: quote.srcAsset.symbol, chain_id_destination: formatChainIdToCaip(quote.destChainId), token_symbol_destination: quote.destAsset.symbol, - is_hardware_wallet: isHardwareAccount, + account_hardware_type: accountHardwareType, + is_hardware_wallet: accountHardwareType !== null, swap_type: getSwapType( quoteResponse.quote.srcChainId, quoteResponse.quote.destChainId, @@ -229,13 +231,15 @@ export const getRequestMetadataFromHistory = ( account?: AccountsControllerState['internalAccounts']['accounts'][string], ): RequestMetadata => { const { quote, slippagePercentage, isStxEnabled } = historyItem; + const accountHardwareType = getAccountHardwareType(account); return { slippage_limit: slippagePercentage, custom_slippage: isCustomSlippage(slippagePercentage), usd_amount_source: Number(historyItem.pricingData?.amountSentInUsd ?? 0), swap_type: getSwapType(quote.srcChainId, quote.destChainId), - is_hardware_wallet: isHardwareWallet(account), + account_hardware_type: accountHardwareType, + is_hardware_wallet: accountHardwareType !== null, stx_enabled: isStxEnabled ?? false, security_warnings: [], }; @@ -249,7 +253,10 @@ export const getRequestMetadataFromHistory = ( */ export const getEVMTxPropertiesFromTransactionMeta = ( transactionMeta: TransactionMeta, + account?: AccountsControllerState['internalAccounts']['accounts'][string], ) => { + const accountHardwareType = getAccountHardwareType(account); + return { source_transaction: [ TransactionStatus.failed, @@ -276,7 +283,8 @@ export const getEVMTxPropertiesFromTransactionMeta = ( transactionMeta.chainId, ) ?? ('' as CaipAssetType), custom_slippage: false, - is_hardware_wallet: false, + account_hardware_type: accountHardwareType, + is_hardware_wallet: accountHardwareType !== null, swap_type: transactionMeta.type && [TransactionType.swap, TransactionType.swapApproval].includes(