diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index e67eb4cb00..56d510f43f 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **BREAKING**: Add persisted input primary denomination state and `Unified SwapBridge Fiat Crypto Toggle Clicked` analytics event support ([#9147](https://github.com/MetaMask/core/pull/9147)) + ## [75.2.1] ### Changed diff --git a/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.batch.test.ts.snap b/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.batch.test.ts.snap index 0ee671c741..b1d5c0c009 100644 --- a/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.batch.test.ts.snap +++ b/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.batch.test.ts.snap @@ -10,6 +10,7 @@ exports[`BridgeController BatchSell (multiple quote requests) SSE fetch quotes s }, "batchSellTrades": null, "batchSellTradesLoadingStatus": 0, + "inputPrimaryDenomination": "token_amount", "minimumBalanceForRentExemptionInLamports": "0", "quoteFetchError": null, "quoteRequest": [ @@ -112,6 +113,7 @@ exports[`BridgeController BatchSell (multiple quote requests) SSE fetch quotes s "custom_slippage": true, "feature_id": "batch_sell", "has_sufficient_funds": true, + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "security_warnings": [], diff --git a/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap b/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap index 76019e0e68..af4e3ba7e7 100644 --- a/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap +++ b/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap @@ -76,6 +76,7 @@ exports[`BridgeController SSE should replace all stale quotes after a refresh an "custom_slippage": true, "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "security_warnings": [], @@ -105,6 +106,7 @@ exports[`BridgeController SSE should reset and refetch quotes after quote reques "custom_slippage": true, "feature_id": "unified_swap_bridge", "has_sufficient_funds": false, + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "security_warnings": [], @@ -134,6 +136,7 @@ exports[`BridgeController SSE should reset quotes list if quote refresh fails 2` "custom_slippage": true, "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "security_warnings": [], @@ -188,6 +191,7 @@ exports[`BridgeController SSE should rethrow error from server 1`] = ` }, "batchSellTrades": null, "batchSellTradesLoadingStatus": null, + "inputPrimaryDenomination": "token_amount", "minimumBalanceForRentExemptionInLamports": "0", "quoteFetchError": null, "quoteRequest": [ @@ -266,6 +270,7 @@ exports[`BridgeController SSE should rethrow error from server 3`] = ` "custom_slippage": true, "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "security_warnings": [], @@ -320,6 +325,7 @@ exports[`BridgeController SSE should trigger quote polling if request is valid 1 }, "batchSellTrades": null, "batchSellTradesLoadingStatus": null, + "inputPrimaryDenomination": "token_amount", "minimumBalanceForRentExemptionInLamports": "0", "quoteFetchError": null, "quoteRequest": [ @@ -398,6 +404,7 @@ exports[`BridgeController SSE should trigger quote polling if request is valid 2 "custom_slippage": true, "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "security_warnings": [], diff --git a/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap b/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap index 3b2eceb75a..883571bd07 100644 --- a/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap +++ b/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap @@ -10,6 +10,7 @@ exports[`BridgeController should handle errors from fetchBridgeQuotes 1`] = ` }, "batchSellTrades": null, "batchSellTradesLoadingStatus": null, + "inputPrimaryDenomination": "token_amount", "minimumBalanceForRentExemptionInLamports": "0", "quoteFetchError": null, "quoteRequest": [ @@ -43,6 +44,7 @@ exports[`BridgeController should handle errors from fetchBridgeQuotes 2`] = ` }, "batchSellTrades": null, "batchSellTradesLoadingStatus": null, + "inputPrimaryDenomination": "token_amount", "minimumBalanceForRentExemptionInLamports": "0", "quoteFetchError": null, "quoteRequest": [ @@ -345,6 +347,29 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t ] `; +exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should track the FiatCryptoToggleClicked event 1`] = ` +[ + [ + "Unified SwapBridge Fiat Crypto Toggle Clicked", + { + "action_type": "swapbridge-v1", + "chain_id_destination": "eip155:1", + "chain_id_source": "eip155:1", + "feature_id": "quick_buy_follow_trading", + "location": "Main View", + "new_primary_denomination": "fiat_value", + "previous_primary_denomination": "token_amount", + "swap_type": "single_chain", + "token_address_destination": "eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7", + "token_address_source": "eip155:1/slip44:60", + "token_security_type_destination": null, + "token_symbol_destination": "USDC", + "token_symbol_source": "ETH", + }, + ], +] +`; + exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should track the InputSourceDestinationFlipped event 1`] = ` [ [ @@ -379,6 +404,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "chain_id_source": "eip155:1", "custom_slippage": false, "feature_id": "quick_buy_token_details", + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "slippage_limit": undefined, @@ -454,6 +480,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "gas_included_7702": false, "has_gas_included_quote": false, "initial_load_time_all_quotes": 0, + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "price_impact": 0, @@ -530,6 +557,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should only poll once i "custom_slippage": true, "feature_id": "unified_swap_bridge", "has_sufficient_funds": false, + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "security_warnings": [], @@ -560,6 +588,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should only poll once i "gas_included_7702": false, "has_gas_included_quote": false, "initial_load_time_all_quotes": 11000, + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "price_impact": 0, @@ -854,6 +883,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "assetExchangeRates": {}, "batchSellTrades": null, "batchSellTradesLoadingStatus": null, + "inputPrimaryDenomination": "token_amount", "minimumBalanceForRentExemptionInLamports": "0", "quoteFetchError": null, "quoteRequest": [ @@ -891,6 +921,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po }, "batchSellTrades": null, "batchSellTradesLoadingStatus": null, + "inputPrimaryDenomination": "token_amount", "minimumBalanceForRentExemptionInLamports": "0", "quoteFetchError": null, "quoteRequest": [ @@ -968,6 +999,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "custom_slippage": true, "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "security_warnings": [], @@ -993,6 +1025,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "custom_slippage": true, "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "security_warnings": [], @@ -1018,6 +1051,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "custom_slippage": true, "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "security_warnings": [], @@ -1069,6 +1103,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "custom_slippage": true, "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, + "input_primary_denomination": "token_amount", "is_hardware_wallet": false, "location": "Main View", "security_warnings": [], diff --git a/packages/bridge-controller/src/bridge-controller-method-action-types.ts b/packages/bridge-controller/src/bridge-controller-method-action-types.ts index 12a7527abd..3b0e41fcda 100644 --- a/packages/bridge-controller/src/bridge-controller-method-action-types.ts +++ b/packages/bridge-controller/src/bridge-controller-method-action-types.ts @@ -25,6 +25,11 @@ export type BridgeControllerSetLocationAction = { handler: BridgeController['setLocation']; }; +export type BridgeControllerSetInputPrimaryDenominationAction = { + type: `BridgeController:setInputPrimaryDenomination`; + handler: BridgeController['setInputPrimaryDenomination']; +}; + export type BridgeControllerResetStateAction = { type: `BridgeController:resetState`; handler: BridgeController['resetState']; @@ -53,6 +58,7 @@ export type BridgeControllerMethodActions = | BridgeControllerFetchQuotesAction | BridgeControllerStopPollingForQuotesAction | BridgeControllerSetLocationAction + | BridgeControllerSetInputPrimaryDenominationAction | BridgeControllerResetStateAction | BridgeControllerSetChainIntervalLengthAction | BridgeControllerTrackUnifiedSwapBridgeEventAction diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 3264079bce..126f66ce2b 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -3013,6 +3013,56 @@ describe('BridgeController', function () { }); }); + it('should track the FiatCryptoToggleClicked event', async () => { + await withController(async ({ rootMessenger, controller }) => { + jest.spyOn(console, 'warn').mockImplementationOnce(jest.fn()); + await rootMessenger.call( + 'BridgeController:updateBridgeQuoteRequestParams', + { + walletAddress: '0x123', + }, + { + stx_enabled: false, + security_warnings: [], + token_symbol_source: 'ETH', + token_symbol_destination: 'USDC', + usd_amount_source: 100, + token_security_type_destination: null, + feature_id: FeatureId.QUICK_BUY_FOLLOW_TRADING, + }, + ); + rootMessenger.call( + 'BridgeController:setInputPrimaryDenomination', + 'fiat_value', + ); + expect(controller.state.inputPrimaryDenomination).toBe('fiat_value'); + jest.clearAllMocks(); + rootMessenger.call( + 'BridgeController:trackUnifiedSwapBridgeEvent', + UnifiedSwapBridgeEventName.FiatCryptoToggleClicked, + { + location: MetaMetricsSwapsEventSource.MainView, + previous_primary_denomination: 'token_amount', + new_primary_denomination: 'fiat_value', + token_symbol_source: 'ETH', + token_symbol_destination: 'USDC', + chain_id_source: formatChainIdToCaip(ChainId.ETH), + chain_id_destination: formatChainIdToCaip(ChainId.ETH), + token_address_source: formatAddressToAssetId('', ChainId.ETH), + token_address_destination: formatAddressToAssetId( + ETH_USDT_ADDRESS, + ChainId.ETH, + ), + token_security_type_destination: null, + swap_type: MetricsSwapType.SINGLE, + feature_id: FeatureId.QUICK_BUY_FOLLOW_TRADING, + }, + ); + expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); + expect(trackMetaMetricsFn.mock.calls).toMatchSnapshot(); + }); + }); + it('should track InputChanged with an enum quick amount preset label', async () => { await withController(async ({ rootMessenger }) => { // Ignore console.warn for this test bc there will be expected asset rate fetching warnings @@ -4342,6 +4392,7 @@ describe('BridgeController', function () { "assetExchangeRates": {}, "batchSellTrades": null, "batchSellTradesLoadingStatus": null, + "inputPrimaryDenomination": "token_amount", "minimumBalanceForRentExemptionInLamports": "0", "quoteFetchError": null, "quoteRequest": [ @@ -4370,7 +4421,11 @@ describe('BridgeController', function () { bridgeController.metadata, 'persist', ), - ).toMatchInlineSnapshot(`{}`); + ).toMatchInlineSnapshot(` + { + "inputPrimaryDenomination": "token_amount", + } + `); }); }); @@ -4387,6 +4442,7 @@ describe('BridgeController', function () { "assetExchangeRates": {}, "batchSellTrades": null, "batchSellTradesLoadingStatus": null, + "inputPrimaryDenomination": "token_amount", "minimumBalanceForRentExemptionInLamports": "0", "quoteFetchError": null, "quoteRequest": [ diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 3c9f49378e..da33641ba9 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -35,6 +35,7 @@ import type { BridgeControllerState, BridgeControllerMessenger, FetchFunction, + InputPrimaryDenomination, } from './types'; import { getAssetIdsForToken, toExchangeRates } from './utils/assets'; import { hasSufficientBalance } from './utils/balance'; @@ -156,6 +157,12 @@ const metadata: StateMetadata = { includeInDebugSnapshot: false, usedInUi: true, }, + inputPrimaryDenomination: { + includeInStateLogs: true, + persist: true, + includeInDebugSnapshot: false, + usedInUi: true, + }, quoteStreamComplete: { includeInStateLogs: true, persist: false, @@ -195,6 +202,7 @@ const MESSENGER_EXPOSED_METHODS = [ 'updateBatchSellTrades', 'stopPollingForQuotes', 'setLocation', + 'setInputPrimaryDenomination', 'resetState', 'setChainIntervalLength', 'trackUnifiedSwapBridgeEvent', @@ -711,6 +719,14 @@ export class BridgeController extends StaticIntervalPollingController { + this.update((state) => { + state.inputPrimaryDenomination = inputPrimaryDenomination; + }); + }; + resetState = ( reason = AbortReason.ResetState, quoteRequestIndex: number | null = null, @@ -1155,6 +1171,9 @@ export class BridgeController extends StaticIntervalPollingController & { + Pick & + InputPrimaryDenominationData & { warnings: QuoteWarning[]; best_quote_provider: QuoteFetchData['best_quote_provider']; price_impact: QuoteFetchData['price_impact']; @@ -174,7 +196,7 @@ type RequiredEventContextFromClientBase = { > & { action_type: MetricsActionType; batch_id?: string; - }; + } & InputPrimaryDenominationData; [UnifiedSwapBridgeEventName.Completed]: TradeData & Pick & Omit & @@ -187,7 +209,7 @@ type RequiredEventContextFromClientBase = { quoted_vs_used_gas_ratio: number; action_type: MetricsActionType; batch_id?: string; - }; + } & InputPrimaryDenominationData; [UnifiedSwapBridgeEventName.Failed]: ( | // Tx failed before confirmation (Pick< @@ -224,7 +246,7 @@ type RequiredEventContextFromClientBase = { [UnifiedSwapBridgeEventName.StatusValidationFailed]: { failures: string[]; refresh_count: number; - }; + } & Partial; // Emitted by clients [UnifiedSwapBridgeEventName.AllQuotesOpened]: Pick< TradeData, @@ -294,22 +316,25 @@ export type EventPropertiesFromControllerState = { Omit< RequestMetadata, 'stx_enabled' | 'usd_amount_source' | 'security_warnings' - >; + > & + InputPrimaryDenominationData; [UnifiedSwapBridgeEventName.InputChanged]: { input: InputKeys; input_value: string; }; + [UnifiedSwapBridgeEventName.FiatCryptoToggleClicked]: RequestParams & + Pick; [UnifiedSwapBridgeEventName.InputSourceDestinationSwitched]: RequestParams; [UnifiedSwapBridgeEventName.QuotesRequested]: RequestParams & RequestMetadata & { has_sufficient_funds: boolean; - }; + } & InputPrimaryDenominationData; [UnifiedSwapBridgeEventName.QuotesReceived]: RequestParams & RequestMetadata & QuoteFetchData & TradeData & { refresh_count: number; // starts from 0 - }; + } & InputPrimaryDenominationData; [UnifiedSwapBridgeEventName.QuotesError]: RequestParams & RequestMetadata & { has_sufficient_funds: boolean; diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 3ee7de6c1e..45028e8104 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add input primary denomination to submitted bridge history and post-submit analytics ([#9147](https://github.com/MetaMask/core/pull/9147)) + ## [72.1.1] ### Changed diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 4469adae67..a1e1b1adb9 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -6,6 +6,7 @@ import { Trade, FeatureId, BatchSellTradesResponse, + InputPrimaryDenomination, } from '@metamask/bridge-controller'; import { isNonEvmChainId, @@ -949,6 +950,7 @@ export class BridgeStatusController extends StaticIntervalPollingController; activeAbTests?: { key: string; value: string }[]; tokenSecurityTypeDestination?: string | null; + inputPrimaryDenomination?: InputPrimaryDenomination; }, ): Promise => { let tradeTxMeta!: TransactionMeta; @@ -1028,6 +1030,7 @@ export class BridgeStatusController extends StaticIntervalPollingController => { /** * If there are multiple quote responses, we assume that they all originate from the same src chain @@ -1099,7 +1103,12 @@ export class BridgeStatusController extends StaticIntervalPollingController; activeAbTests?: { key: string; value: string }[]; tokenSecurityTypeDestination?: string | null; + inputPrimaryDenomination?: InputPrimaryDenomination; isStxEnabled?: boolean; quotesReceivedContext?: RequiredEventContextFromClient[UnifiedSwapBridgeEventName.QuotesReceived]; }): Promise => { @@ -1189,6 +1201,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { }); expect(txHistoryItem.tokenSecurityTypeDestination).toBeNull(); }); + + it('persists inputPrimaryDenomination when provided', () => { + const txHistoryItem = getInitialHistoryItem({ + ...baseArgs, + inputPrimaryDenomination: 'fiat_value', + }); + expect(txHistoryItem.inputPrimaryDenomination).toBe('fiat_value'); + }); }); }); diff --git a/packages/bridge-status-controller/src/utils/history.ts b/packages/bridge-status-controller/src/utils/history.ts index f44092d7e1..4c20f2134a 100644 --- a/packages/bridge-status-controller/src/utils/history.ts +++ b/packages/bridge-status-controller/src/utils/history.ts @@ -206,6 +206,7 @@ export const getInitialHistoryItem = ( originalTransactionId, actionId, tokenSecurityTypeDestination, + inputPrimaryDenomination, batchSellData, quoteIds, } = args; @@ -256,6 +257,9 @@ export const getInitialHistoryItem = ( ...(tokenSecurityTypeDestination !== undefined && { tokenSecurityTypeDestination, }), + ...(inputPrimaryDenomination !== undefined && { + inputPrimaryDenomination, + }), }; if (batchSellData) {