From bdb5ec71415d76038d5fd9cd77bca5b7c81ef070 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:12:29 -0400 Subject: [PATCH 1/8] feat: add batch sell metrics --- packages/bridge-controller/CHANGELOG.md | 4 + .../src/bridge-controller.test.ts | 118 ++++++++++++++++++ .../src/bridge-controller.ts | 40 ++++-- packages/bridge-controller/src/index.ts | 5 + .../src/utils/metrics/constants.ts | 19 +++ .../src/utils/metrics/types.ts | 66 ++++++++-- 6 files changed, 233 insertions(+), 19 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index c463a6abc4..0d24121f62 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 + +- Add Batch Sell token and quote page analytics event types + ### Changed - Bump `@metamask/multichain-network-controller` from `^3.1.4` to `^3.2.0` ([#9264](https://github.com/MetaMask/core/pull/9264)) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 1987be7d91..6e3abf49b1 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -13,6 +13,7 @@ import type { MessengerEvents, MockAnyNamespace, } from '@metamask/messenger'; +import type { CaipAssetType } from '@metamask/utils'; import nock from 'nock'; import { flushPromises } from '../../../tests/helpers'; @@ -52,6 +53,8 @@ import { import * as featureFlagUtils from './utils/feature-flags'; import * as fetchUtils from './utils/fetch'; import { + BatchSellMetricsEventName, + BatchSellMetricsLocation, InputAmountPreset, MetaMetricsSwapsEventSource, MetricsActionType, @@ -3032,6 +3035,121 @@ describe('BridgeController', function () { }); }); + it('should track Batch Sell token page events with default chain fallback', async () => { + await withController(async ({ rootMessenger }) => { + rootMessenger.call( + 'BridgeController:trackUnifiedSwapBridgeEvent', + BatchSellMetricsEventName.BatchSellTokenPageViewed, + { + location: BatchSellMetricsLocation.TradeMenu, + feature_id: FeatureId.BATCH_SELL, + }, + ); + rootMessenger.call( + 'BridgeController:trackUnifiedSwapBridgeEvent', + BatchSellMetricsEventName.BatchSellTokenPageSubmitted, + { + location: BatchSellMetricsLocation.AssetPicker, + feature_id: FeatureId.BATCH_SELL, + }, + ); + + expect(trackMetaMetricsFn).toHaveBeenNthCalledWith( + 1, + BatchSellMetricsEventName.BatchSellTokenPageViewed, + { + chain_id: formatChainIdToCaip(ChainId.ETH), + location: BatchSellMetricsLocation.TradeMenu, + feature_id: FeatureId.BATCH_SELL, + action_type: MetricsActionType.SWAPBRIDGE_V1, + }, + ); + expect(trackMetaMetricsFn).toHaveBeenNthCalledWith( + 2, + BatchSellMetricsEventName.BatchSellTokenPageSubmitted, + { + chain_id: formatChainIdToCaip(ChainId.ETH), + location: BatchSellMetricsLocation.AssetPicker, + feature_id: FeatureId.BATCH_SELL, + action_type: MetricsActionType.SWAPBRIDGE_V1, + }, + ); + }); + }); + + it('should track Batch Sell quote page events with selected token metadata', async () => { + await withController(async ({ rootMessenger }) => { + await rootMessenger.call( + 'BridgeController:updateBridgeQuoteRequestParams', + { + walletAddress: '0x123', + srcChainId: ChainId.OPTIMISM, + }, + { + 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.BATCH_SELL, + }, + ); + jest.clearAllMocks(); + + const selectedTokenAddressList = [ + 'eip155:10/erc20:0x1111111111111111111111111111111111111111', + 'eip155:10/erc20:0x2222222222222222222222222222222222222222', + ] satisfies CaipAssetType[]; + const properties = { + selected_token_address_list: selectedTokenAddressList, + target_token_symbol: 'USDC', + location: BatchSellMetricsLocation.Deeplink, + slider_percentages: [25, 75], + slippage_percentages: [0.5, 1], + feature_id: FeatureId.BATCH_SELL, + }; + const expectedProperties = { + chain_id: formatChainIdToCaip(ChainId.OPTIMISM), + selected_tokens_count: selectedTokenAddressList.length, + ...properties, + action_type: MetricsActionType.SWAPBRIDGE_V1, + }; + + rootMessenger.call( + 'BridgeController:trackUnifiedSwapBridgeEvent', + BatchSellMetricsEventName.BatchSellQuotePageViewed, + properties, + ); + rootMessenger.call( + 'BridgeController:trackUnifiedSwapBridgeEvent', + BatchSellMetricsEventName.BatchSellQuotesReviewed, + properties, + ); + rootMessenger.call( + 'BridgeController:trackUnifiedSwapBridgeEvent', + BatchSellMetricsEventName.BatchSellQuotePageSubmitted, + properties, + ); + + expect(trackMetaMetricsFn).toHaveBeenNthCalledWith( + 1, + BatchSellMetricsEventName.BatchSellQuotePageViewed, + expectedProperties, + ); + expect(trackMetaMetricsFn).toHaveBeenNthCalledWith( + 2, + BatchSellMetricsEventName.BatchSellQuotesReviewed, + expectedProperties, + ); + expect(trackMetaMetricsFn).toHaveBeenNthCalledWith( + 3, + BatchSellMetricsEventName.BatchSellQuotePageSubmitted, + expectedProperties, + ); + }); + }); + it('should track the FiatCryptoToggleClicked event', async () => { await withController(async ({ rootMessenger, controller }) => { jest.spyOn(console, 'warn').mockImplementationOnce(jest.fn()); diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index da3ec1c4b2..978f7c0b8d 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -25,7 +25,7 @@ import { ExchangeRateSourcesForLookup, selectIsAssetExchangeRateInState, } from './selectors'; -import { FeatureId, RequestStatus } from './types'; +import { ChainId, FeatureId, RequestStatus } from './types'; import type { L1GasFees, GenericQuoteRequest, @@ -63,10 +63,12 @@ import { } from './utils/fetch'; import { AbortReason, + BatchSellMetricsEventName, MetaMetricsSwapsEventSource, MetricsActionType, UnifiedSwapBridgeEventName, } from './utils/metrics/constants'; +import type { BridgeControllerMetricsEventName } from './utils/metrics/constants'; import { formatProviderLabel, getAccountHardwareType, @@ -236,8 +238,7 @@ export class BridgeController extends StaticIntervalPollingController( eventName: EventName, properties: CrossChainSwapsEventProperties, @@ -279,8 +280,7 @@ export class BridgeController extends StaticIntervalPollingController( eventName: EventName, properties: CrossChainSwapsEventProperties, @@ -1165,8 +1165,7 @@ export class BridgeController extends StaticIntervalPollingController( eventName: EventName, propertiesFromClient: Pick< @@ -1184,6 +1183,11 @@ export class BridgeController extends StaticIntervalPollingController( eventName: EventName, propertiesFromClient: Pick< diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index d66ab330dd..57fc1b82e6 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -1,13 +1,18 @@ export { BridgeController } from './bridge-controller'; export { + BatchSellMetricsEventName, UnifiedSwapBridgeEventName, + BATCH_SELL_EVENT_CATEGORY, UNIFIED_SWAP_BRIDGE_EVENT_CATEGORY, + BatchSellMetricsLocation, InputAmountPreset, MetaMetricsSwapsEventSource, PollingStatus, } from './utils/metrics/constants'; +export type { BridgeControllerMetricsEventName } from './utils/metrics/constants'; + export type { AccountHardwareType, RequiredEventContextFromClient, diff --git a/packages/bridge-controller/src/utils/metrics/constants.ts b/packages/bridge-controller/src/utils/metrics/constants.ts index d44b80b524..30fcf45d27 100644 --- a/packages/bridge-controller/src/utils/metrics/constants.ts +++ b/packages/bridge-controller/src/utils/metrics/constants.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ export const UNIFIED_SWAP_BRIDGE_EVENT_CATEGORY = 'Unified SwapBridge'; +export const BATCH_SELL_EVENT_CATEGORY = 'Batch Sell'; /** * These event names map to events defined in the segment-schema: https://github.com/Consensys/segment-schema/tree/main/libraries/events/metamask-cross-chain-swaps @@ -26,6 +27,18 @@ export enum UnifiedSwapBridgeEventName { PollingStatusUpdated = `${UNIFIED_SWAP_BRIDGE_EVENT_CATEGORY} Polling Status Updated`, } +export enum BatchSellMetricsEventName { + BatchSellTokenPageViewed = `${BATCH_SELL_EVENT_CATEGORY} Token Page Viewed`, + BatchSellTokenPageSubmitted = `${BATCH_SELL_EVENT_CATEGORY} Token Page Submitted`, + BatchSellQuotePageViewed = `${BATCH_SELL_EVENT_CATEGORY} Quote Page Viewed`, + BatchSellQuotesReviewed = `${BATCH_SELL_EVENT_CATEGORY} Quotes Reviewed`, + BatchSellQuotePageSubmitted = `${BATCH_SELL_EVENT_CATEGORY} Quote Page Submitted`, +} + +export type BridgeControllerMetricsEventName = + | UnifiedSwapBridgeEventName + | BatchSellMetricsEventName; + export enum PollingStatus { MaxPollingReached = 'max_polling_reached', InvalidTransactionHash = 'invalid_transaction_hash', @@ -57,6 +70,12 @@ export enum MetaMetricsSwapsEventSource { Unknown = 'Unknown', } +export enum BatchSellMetricsLocation { + TradeMenu = 'trade_menu', + Deeplink = 'deeplink', + AssetPicker = 'asset_picker', +} + export enum InputAmountPreset { PERCENT_25 = '25%', PERCENT_50 = '50%', diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index 9e963d340e..ef01234b6a 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -9,6 +9,9 @@ import type { } from '../../types'; import type { UnifiedSwapBridgeEventName, + BatchSellMetricsEventName, + BatchSellMetricsLocation, + BridgeControllerMetricsEventName, MetaMetricsSwapsEventSource, MetricsActionType, MetricsSwapType, @@ -111,10 +114,33 @@ export type QuoteWarning = | 'quote_expired' | 'tx_alert'; +type BatchSellTokenPageEventContext = { + location: BatchSellMetricsLocation; +}; + +type BatchSellQuotePageEventContext = BatchSellTokenPageEventContext & { + selected_token_address_list: CaipAssetType[]; + target_token_symbol: string; + slider_percentages: number[]; + slippage_percentages: number[]; +}; + +type SharedEventContextFromClient = { + ab_tests?: Record; + active_ab_tests?: { key: string; value: string }[]; + feature_id: FeatureId; +}; + +type OptionalLocationContextFromClient = T extends { + location: unknown; +} + ? object + : { location?: MetaMetricsSwapsEventSource }; + /** * Properties that are required to be provided when trackUnifiedSwapBridgeEvent is called. - * This is the base type without the `location` property which is added to all events - * via the RequiredEventContextFromClient mapped type. + * Most events receive an optional location via RequiredEventContextFromClient; + * Batch Sell events define their own required location enum. */ type RequiredEventContextFromClientBase = { [UnifiedSwapBridgeEventName.ButtonClicked]: Pick< @@ -287,6 +313,11 @@ type RequiredEventContextFromClientBase = { [UnifiedSwapBridgeEventName.AssetPickerOpened]: { asset_location: 'source' | 'destination'; }; + [BatchSellMetricsEventName.BatchSellTokenPageViewed]: BatchSellTokenPageEventContext; + [BatchSellMetricsEventName.BatchSellTokenPageSubmitted]: BatchSellTokenPageEventContext; + [BatchSellMetricsEventName.BatchSellQuotePageViewed]: BatchSellQuotePageEventContext; + [BatchSellMetricsEventName.BatchSellQuotesReviewed]: BatchSellQuotePageEventContext; + [BatchSellMetricsEventName.BatchSellQuotePageSubmitted]: BatchSellQuotePageEventContext; }; /** @@ -299,12 +330,9 @@ type RequiredEventContextFromClientBase = { * Both are kept for a migration window and are treated as separate payloads. */ export type RequiredEventContextFromClient = { - [K in keyof RequiredEventContextFromClientBase]: RequiredEventContextFromClientBase[K] & { - location?: MetaMetricsSwapsEventSource; - ab_tests?: Record; - active_ab_tests?: { key: string; value: string }[]; - feature_id: FeatureId; - }; + [K in keyof RequiredEventContextFromClientBase]: RequiredEventContextFromClientBase[K] & + OptionalLocationContextFromClient & + SharedEventContextFromClient; }; /** @@ -379,6 +407,24 @@ export type EventPropertiesFromControllerState = { > & { batch_id?: string; }; + [BatchSellMetricsEventName.BatchSellTokenPageViewed]: { + chain_id: CaipChainId; + }; + [BatchSellMetricsEventName.BatchSellTokenPageSubmitted]: { + chain_id: CaipChainId; + }; + [BatchSellMetricsEventName.BatchSellQuotePageViewed]: { + chain_id: CaipChainId; + selected_tokens_count: number; + }; + [BatchSellMetricsEventName.BatchSellQuotesReviewed]: { + chain_id: CaipChainId; + selected_tokens_count: number; + }; + [BatchSellMetricsEventName.BatchSellQuotePageSubmitted]: { + chain_id: CaipChainId; + selected_tokens_count: number; + }; }; /** @@ -389,12 +435,12 @@ export type EventPropertiesFromControllerState = { * `ab_tests` and `active_ab_tests` intentionally coexist during migration. */ export type CrossChainSwapsEventProperties< - T extends UnifiedSwapBridgeEventName, + T extends BridgeControllerMetricsEventName, > = | { feature_id: FeatureId; action_type: MetricsActionType; - location: MetaMetricsSwapsEventSource; + location: MetaMetricsSwapsEventSource | BatchSellMetricsLocation; ab_tests?: Record; active_ab_tests?: { key: string; value: string }[]; } From d5fd8101f7830d3cceeac2b70e39cc712b7626f6 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:09:17 -0400 Subject: [PATCH 2/8] chore: add Unknown to batch sell location and tighten up types --- .../bridge-controller/src/bridge-controller.test.ts | 9 +++++++++ packages/bridge-controller/src/bridge-controller.ts | 11 +++++++---- packages/bridge-controller/src/index.ts | 1 + .../bridge-controller/src/utils/metrics/constants.ts | 5 +++++ packages/bridge-controller/src/utils/metrics/types.ts | 6 +++--- 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 6e3abf49b1..bec5760dda 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -2914,6 +2914,15 @@ describe('BridgeController', function () { expect(rootMessenger.call('BridgeController:getLocation')).toBe( MetaMetricsSwapsEventSource.TokenView, ); + + rootMessenger.call( + 'BridgeController:setLocation', + BatchSellMetricsLocation.AssetPicker, + ); + + expect(rootMessenger.call('BridgeController:getLocation')).toBe( + BatchSellMetricsLocation.AssetPicker, + ); }); }); }); diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 978f7c0b8d..18c5d87025 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -68,7 +68,10 @@ import { MetricsActionType, UnifiedSwapBridgeEventName, } from './utils/metrics/constants'; -import type { BridgeControllerMetricsEventName } from './utils/metrics/constants'; +import type { + BridgeControllerMetricsEventName, + BridgeControllerMetricsLocation, +} from './utils/metrics/constants'; import { formatProviderLabel, getAccountHardwareType, @@ -227,7 +230,7 @@ export class BridgeController extends StaticIntervalPollingController { + setLocation = (location: BridgeControllerMetricsLocation) => { this.#location = location; }; @@ -725,7 +728,7 @@ export class BridgeController extends StaticIntervalPollingController { + getLocation = (): BridgeControllerMetricsLocation => { return this.#location; }; diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 57fc1b82e6..d1b61848a7 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -12,6 +12,7 @@ export { } from './utils/metrics/constants'; export type { BridgeControllerMetricsEventName } from './utils/metrics/constants'; +export type { BridgeControllerMetricsLocation } from './utils/metrics/constants'; export type { AccountHardwareType, diff --git a/packages/bridge-controller/src/utils/metrics/constants.ts b/packages/bridge-controller/src/utils/metrics/constants.ts index 30fcf45d27..ff82fd0294 100644 --- a/packages/bridge-controller/src/utils/metrics/constants.ts +++ b/packages/bridge-controller/src/utils/metrics/constants.ts @@ -74,8 +74,13 @@ export enum BatchSellMetricsLocation { TradeMenu = 'trade_menu', Deeplink = 'deeplink', AssetPicker = 'asset_picker', + Unknown = 'Unknown', } +export type BridgeControllerMetricsLocation = + | MetaMetricsSwapsEventSource + | BatchSellMetricsLocation; + export enum InputAmountPreset { PERCENT_25 = '25%', PERCENT_50 = '50%', diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index ef01234b6a..80202fae7a 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -12,7 +12,7 @@ import type { BatchSellMetricsEventName, BatchSellMetricsLocation, BridgeControllerMetricsEventName, - MetaMetricsSwapsEventSource, + BridgeControllerMetricsLocation, MetricsActionType, MetricsSwapType, PollingStatus, @@ -135,7 +135,7 @@ type OptionalLocationContextFromClient = T extends { location: unknown; } ? object - : { location?: MetaMetricsSwapsEventSource }; + : { location?: BridgeControllerMetricsLocation }; /** * Properties that are required to be provided when trackUnifiedSwapBridgeEvent is called. @@ -440,7 +440,7 @@ export type CrossChainSwapsEventProperties< | { feature_id: FeatureId; action_type: MetricsActionType; - location: MetaMetricsSwapsEventSource | BatchSellMetricsLocation; + location: BridgeControllerMetricsLocation; ab_tests?: Record; active_ab_tests?: { key: string; value: string }[]; } From cee0c4e7fab6f6fd3654d9c6e6a5782267ce5f0d Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:52:01 -0400 Subject: [PATCH 3/8] chore: update batch sell metrics --- .../src/bridge-controller.test.ts | 70 +++++++----- .../src/bridge-controller.ts | 78 ++++++++++--- .../src/utils/metrics/constants.ts | 6 +- .../src/utils/metrics/types.ts | 105 ++++++++++++------ 4 files changed, 177 insertions(+), 82 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index bec5760dda..928704e439 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -3046,20 +3046,26 @@ describe('BridgeController', function () { it('should track Batch Sell token page events with default chain fallback', async () => { await withController(async ({ rootMessenger }) => { + const sourceTokenAddresses = [ + 'eip155:1/erc20:0x1111111111111111111111111111111111111111', + 'eip155:1/erc20:0x2222222222222222222222222222222222222222', + ] satisfies CaipAssetType[]; + const sourceTokenSymbols = ['LINK', 'UNI']; + rootMessenger.call( 'BridgeController:trackUnifiedSwapBridgeEvent', BatchSellMetricsEventName.BatchSellTokenPageViewed, { location: BatchSellMetricsLocation.TradeMenu, - feature_id: FeatureId.BATCH_SELL, }, ); rootMessenger.call( 'BridgeController:trackUnifiedSwapBridgeEvent', - BatchSellMetricsEventName.BatchSellTokenPageSubmitted, + BatchSellMetricsEventName.BatchSellTokenPageContinueClicked, { location: BatchSellMetricsLocation.AssetPicker, - feature_id: FeatureId.BATCH_SELL, + source_token_symbols: sourceTokenSymbols, + source_token_addresses: sourceTokenAddresses, }, ); @@ -3067,20 +3073,21 @@ describe('BridgeController', function () { 1, BatchSellMetricsEventName.BatchSellTokenPageViewed, { - chain_id: formatChainIdToCaip(ChainId.ETH), + chain_id_source: formatChainIdToCaip(ChainId.ETH), + chain_id_destination: null, location: BatchSellMetricsLocation.TradeMenu, - feature_id: FeatureId.BATCH_SELL, - action_type: MetricsActionType.SWAPBRIDGE_V1, }, ); expect(trackMetaMetricsFn).toHaveBeenNthCalledWith( 2, - BatchSellMetricsEventName.BatchSellTokenPageSubmitted, + BatchSellMetricsEventName.BatchSellTokenPageContinueClicked, { - chain_id: formatChainIdToCaip(ChainId.ETH), + chain_id_source: formatChainIdToCaip(ChainId.ETH), + chain_id_destination: null, location: BatchSellMetricsLocation.AssetPicker, - feature_id: FeatureId.BATCH_SELL, - action_type: MetricsActionType.SWAPBRIDGE_V1, + source_token_count: sourceTokenAddresses.length, + source_token_symbols: sourceTokenSymbols, + source_token_addresses: sourceTokenAddresses, }, ); }); @@ -3093,6 +3100,7 @@ describe('BridgeController', function () { { walletAddress: '0x123', srcChainId: ChainId.OPTIMISM, + destChainId: ChainId.OPTIMISM, }, { stx_enabled: false, @@ -3106,23 +3114,27 @@ describe('BridgeController', function () { ); jest.clearAllMocks(); - const selectedTokenAddressList = [ + const sourceTokenAddresses = [ 'eip155:10/erc20:0x1111111111111111111111111111111111111111', 'eip155:10/erc20:0x2222222222222222222222222222222222222222', ] satisfies CaipAssetType[]; + const destinationTokenAddress = + 'eip155:10/erc20:0x3333333333333333333333333333333333333333' satisfies CaipAssetType; const properties = { - selected_token_address_list: selectedTokenAddressList, - target_token_symbol: 'USDC', location: BatchSellMetricsLocation.Deeplink, - slider_percentages: [25, 75], - slippage_percentages: [0.5, 1], - feature_id: FeatureId.BATCH_SELL, + source_token_symbols: ['WETH', 'OP'], + source_token_addresses: sourceTokenAddresses, + destination_token_symbol: 'USDC', + destination_token_address: destinationTokenAddress, + usd_amount_source_tokens: [10, 20], + usd_amount_source_total: 30, + source_token_slippages: [0.5, 1], }; const expectedProperties = { - chain_id: formatChainIdToCaip(ChainId.OPTIMISM), - selected_tokens_count: selectedTokenAddressList.length, + chain_id_source: formatChainIdToCaip(ChainId.OPTIMISM), + chain_id_destination: formatChainIdToCaip(ChainId.OPTIMISM), + source_token_count: sourceTokenAddresses.length, ...properties, - action_type: MetricsActionType.SWAPBRIDGE_V1, }; rootMessenger.call( @@ -3132,13 +3144,17 @@ describe('BridgeController', function () { ); rootMessenger.call( 'BridgeController:trackUnifiedSwapBridgeEvent', - BatchSellMetricsEventName.BatchSellQuotesReviewed, + BatchSellMetricsEventName.BatchSellQuotePageReviewClicked, properties, ); rootMessenger.call( 'BridgeController:trackUnifiedSwapBridgeEvent', - BatchSellMetricsEventName.BatchSellQuotePageSubmitted, - properties, + BatchSellMetricsEventName.BatchSellReviewModalSubmitted, + { + ...properties, + usd_quoted_gas: 1, + usd_quoted_return: 29, + }, ); expect(trackMetaMetricsFn).toHaveBeenNthCalledWith( @@ -3148,13 +3164,17 @@ describe('BridgeController', function () { ); expect(trackMetaMetricsFn).toHaveBeenNthCalledWith( 2, - BatchSellMetricsEventName.BatchSellQuotesReviewed, + BatchSellMetricsEventName.BatchSellQuotePageReviewClicked, expectedProperties, ); expect(trackMetaMetricsFn).toHaveBeenNthCalledWith( 3, - BatchSellMetricsEventName.BatchSellQuotePageSubmitted, - expectedProperties, + BatchSellMetricsEventName.BatchSellReviewModalSubmitted, + { + ...expectedProperties, + usd_quoted_gas: 1, + usd_quoted_return: 29, + }, ); }); }); diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 18c5d87025..65e93ed15f 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -1176,7 +1176,7 @@ export class BridgeController extends StaticIntervalPollingController[EventName], quoteRequestIndex: number = 0, - ): CrossChainSwapsEventProperties => { + ) => { const clientProps = propertiesFromClient as Record; const baseProperties = { ...propertiesFromClient, @@ -1186,10 +1186,15 @@ export class BridgeController extends StaticIntervalPollingController, + ); } catch (error) { console.error( `Error tracking cross-chain swaps MetaMetrics event ${eventName}`, diff --git a/packages/bridge-controller/src/utils/metrics/constants.ts b/packages/bridge-controller/src/utils/metrics/constants.ts index ff82fd0294..af28b18fa1 100644 --- a/packages/bridge-controller/src/utils/metrics/constants.ts +++ b/packages/bridge-controller/src/utils/metrics/constants.ts @@ -29,10 +29,10 @@ export enum UnifiedSwapBridgeEventName { export enum BatchSellMetricsEventName { BatchSellTokenPageViewed = `${BATCH_SELL_EVENT_CATEGORY} Token Page Viewed`, - BatchSellTokenPageSubmitted = `${BATCH_SELL_EVENT_CATEGORY} Token Page Submitted`, + BatchSellTokenPageContinueClicked = `${BATCH_SELL_EVENT_CATEGORY} Token Page Continue Clicked`, BatchSellQuotePageViewed = `${BATCH_SELL_EVENT_CATEGORY} Quote Page Viewed`, - BatchSellQuotesReviewed = `${BATCH_SELL_EVENT_CATEGORY} Quotes Reviewed`, - BatchSellQuotePageSubmitted = `${BATCH_SELL_EVENT_CATEGORY} Quote Page Submitted`, + BatchSellQuotePageReviewClicked = `${BATCH_SELL_EVENT_CATEGORY} Quote Page Review Clicked`, + BatchSellReviewModalSubmitted = `${BATCH_SELL_EVENT_CATEGORY} Review Modal Submitted`, } export type BridgeControllerMetricsEventName = diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index 80202fae7a..711b93fe2a 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -118,13 +118,47 @@ type BatchSellTokenPageEventContext = { location: BatchSellMetricsLocation; }; -type BatchSellQuotePageEventContext = BatchSellTokenPageEventContext & { - selected_token_address_list: CaipAssetType[]; - target_token_symbol: string; - slider_percentages: number[]; - slippage_percentages: number[]; +type BatchSellSourceTokenEventContext = BatchSellTokenPageEventContext & { + source_token_symbols: string[]; + source_token_addresses: CaipAssetType[]; }; +type BatchSellQuotePageEventContext = BatchSellSourceTokenEventContext & { + destination_token_symbol: string; + destination_token_address: CaipAssetType; + usd_amount_source_tokens: number[]; + usd_amount_source_total: number; + source_token_slippages: number[]; +}; + +type BatchSellReviewModalSubmittedEventContext = + BatchSellQuotePageEventContext & + Pick; + +type BatchSellChainProperties = { + chain_id_source: CaipChainId; + chain_id_destination: CaipChainId | null; +}; + +type BatchSellTokenPageEventProperties = BatchSellChainProperties & + BatchSellTokenPageEventContext; + +type BatchSellSourceTokenEventProperties = BatchSellChainProperties & + BatchSellSourceTokenEventContext & { + source_token_count: number; + }; + +type BatchSellQuotePageEventProperties = BatchSellChainProperties & + BatchSellQuotePageEventContext & { + source_token_count: number; + }; + +type BatchSellReviewModalSubmittedEventProperties = + BatchSellChainProperties & + BatchSellReviewModalSubmittedEventContext & { + source_token_count: number; + }; + type SharedEventContextFromClient = { ab_tests?: Record; active_ab_tests?: { key: string; value: string }[]; @@ -314,10 +348,10 @@ type RequiredEventContextFromClientBase = { asset_location: 'source' | 'destination'; }; [BatchSellMetricsEventName.BatchSellTokenPageViewed]: BatchSellTokenPageEventContext; - [BatchSellMetricsEventName.BatchSellTokenPageSubmitted]: BatchSellTokenPageEventContext; + [BatchSellMetricsEventName.BatchSellTokenPageContinueClicked]: BatchSellSourceTokenEventContext; [BatchSellMetricsEventName.BatchSellQuotePageViewed]: BatchSellQuotePageEventContext; - [BatchSellMetricsEventName.BatchSellQuotesReviewed]: BatchSellQuotePageEventContext; - [BatchSellMetricsEventName.BatchSellQuotePageSubmitted]: BatchSellQuotePageEventContext; + [BatchSellMetricsEventName.BatchSellQuotePageReviewClicked]: BatchSellQuotePageEventContext; + [BatchSellMetricsEventName.BatchSellReviewModalSubmitted]: BatchSellReviewModalSubmittedEventContext; }; /** @@ -330,9 +364,13 @@ type RequiredEventContextFromClientBase = { * Both are kept for a migration window and are treated as separate payloads. */ export type RequiredEventContextFromClient = { - [K in keyof RequiredEventContextFromClientBase]: RequiredEventContextFromClientBase[K] & - OptionalLocationContextFromClient & - SharedEventContextFromClient; + [K in keyof RequiredEventContextFromClientBase]: K extends BatchSellMetricsEventName + ? RequiredEventContextFromClientBase[K] + : RequiredEventContextFromClientBase[K] & + OptionalLocationContextFromClient< + RequiredEventContextFromClientBase[K] + > & + SharedEventContextFromClient; }; /** @@ -407,34 +445,14 @@ export type EventPropertiesFromControllerState = { > & { batch_id?: string; }; - [BatchSellMetricsEventName.BatchSellTokenPageViewed]: { - chain_id: CaipChainId; - }; - [BatchSellMetricsEventName.BatchSellTokenPageSubmitted]: { - chain_id: CaipChainId; - }; - [BatchSellMetricsEventName.BatchSellQuotePageViewed]: { - chain_id: CaipChainId; - selected_tokens_count: number; - }; - [BatchSellMetricsEventName.BatchSellQuotesReviewed]: { - chain_id: CaipChainId; - selected_tokens_count: number; - }; - [BatchSellMetricsEventName.BatchSellQuotePageSubmitted]: { - chain_id: CaipChainId; - selected_tokens_count: number; - }; + [BatchSellMetricsEventName.BatchSellTokenPageViewed]: BatchSellTokenPageEventProperties; + [BatchSellMetricsEventName.BatchSellTokenPageContinueClicked]: BatchSellSourceTokenEventProperties; + [BatchSellMetricsEventName.BatchSellQuotePageViewed]: BatchSellQuotePageEventProperties; + [BatchSellMetricsEventName.BatchSellQuotePageReviewClicked]: BatchSellQuotePageEventProperties; + [BatchSellMetricsEventName.BatchSellReviewModalSubmitted]: BatchSellReviewModalSubmittedEventProperties; }; -/** - * trackUnifiedSwapBridgeEvent payload properties consist of required properties from the client - * and properties from the bridge controller - * - * `ab_tests` will be deprecated in favor of `active_ab_tests` in the future. - * `ab_tests` and `active_ab_tests` intentionally coexist during migration. - */ -export type CrossChainSwapsEventProperties< +type SharedCrossChainSwapsEventProperties< T extends BridgeControllerMetricsEventName, > = | { @@ -446,3 +464,16 @@ export type CrossChainSwapsEventProperties< } | Pick[T] | Pick[T]; + +/** + * trackUnifiedSwapBridgeEvent payload properties consist of required properties from the client + * and properties from the bridge controller + * + * `ab_tests` will be deprecated in favor of `active_ab_tests` in the future. + * `ab_tests` and `active_ab_tests` intentionally coexist during migration. + */ +export type CrossChainSwapsEventProperties< + T extends BridgeControllerMetricsEventName, +> = T extends BatchSellMetricsEventName + ? Pick[T] + : SharedCrossChainSwapsEventProperties; From 17e301a73bf7b106b31d18f74a9bc8791fb289ba Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:00:51 -0400 Subject: [PATCH 4/8] fix: use client provided chain id source and dest --- .../src/bridge-controller.test.ts | 37 ++++++++++------ .../src/bridge-controller.ts | 42 +++++++++---------- .../src/utils/metrics/types.ts | 26 ++++++------ 3 files changed, 58 insertions(+), 47 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 928704e439..81565834b8 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -3044,11 +3044,13 @@ describe('BridgeController', function () { }); }); - it('should track Batch Sell token page events with default chain fallback', async () => { + it('should track Batch Sell token page events with client chain ids', async () => { await withController(async ({ rootMessenger }) => { + const chainIdSource = formatChainIdToCaip(ChainId.POLYGON); + const chainIdDestination = formatChainIdToCaip(ChainId.BASE); const sourceTokenAddresses = [ - 'eip155:1/erc20:0x1111111111111111111111111111111111111111', - 'eip155:1/erc20:0x2222222222222222222222222222222222222222', + 'eip155:137/erc20:0x1111111111111111111111111111111111111111', + 'eip155:137/erc20:0x2222222222222222222222222222222222222222', ] satisfies CaipAssetType[]; const sourceTokenSymbols = ['LINK', 'UNI']; @@ -3056,6 +3058,8 @@ describe('BridgeController', function () { 'BridgeController:trackUnifiedSwapBridgeEvent', BatchSellMetricsEventName.BatchSellTokenPageViewed, { + chain_id_source: chainIdSource, + chain_id_destination: chainIdDestination, location: BatchSellMetricsLocation.TradeMenu, }, ); @@ -3063,6 +3067,8 @@ describe('BridgeController', function () { 'BridgeController:trackUnifiedSwapBridgeEvent', BatchSellMetricsEventName.BatchSellTokenPageContinueClicked, { + chain_id_source: chainIdSource, + chain_id_destination: chainIdDestination, location: BatchSellMetricsLocation.AssetPicker, source_token_symbols: sourceTokenSymbols, source_token_addresses: sourceTokenAddresses, @@ -3073,8 +3079,8 @@ describe('BridgeController', function () { 1, BatchSellMetricsEventName.BatchSellTokenPageViewed, { - chain_id_source: formatChainIdToCaip(ChainId.ETH), - chain_id_destination: null, + chain_id_source: chainIdSource, + chain_id_destination: chainIdDestination, location: BatchSellMetricsLocation.TradeMenu, }, ); @@ -3082,8 +3088,8 @@ describe('BridgeController', function () { 2, BatchSellMetricsEventName.BatchSellTokenPageContinueClicked, { - chain_id_source: formatChainIdToCaip(ChainId.ETH), - chain_id_destination: null, + chain_id_source: chainIdSource, + chain_id_destination: chainIdDestination, location: BatchSellMetricsLocation.AssetPicker, source_token_count: sourceTokenAddresses.length, source_token_symbols: sourceTokenSymbols, @@ -3114,13 +3120,15 @@ describe('BridgeController', function () { ); jest.clearAllMocks(); + const chainIdSource = formatChainIdToCaip(ChainId.POLYGON); + const chainIdDestination = formatChainIdToCaip(ChainId.BASE); const sourceTokenAddresses = [ - 'eip155:10/erc20:0x1111111111111111111111111111111111111111', - 'eip155:10/erc20:0x2222222222222222222222222222222222222222', + 'eip155:137/erc20:0x1111111111111111111111111111111111111111', + 'eip155:137/erc20:0x2222222222222222222222222222222222222222', ] satisfies CaipAssetType[]; const destinationTokenAddress = - 'eip155:10/erc20:0x3333333333333333333333333333333333333333' satisfies CaipAssetType; - const properties = { + 'eip155:8453/erc20:0x3333333333333333333333333333333333333333' satisfies CaipAssetType; + const sharedProperties = { location: BatchSellMetricsLocation.Deeplink, source_token_symbols: ['WETH', 'OP'], source_token_addresses: sourceTokenAddresses, @@ -3130,9 +3138,12 @@ describe('BridgeController', function () { usd_amount_source_total: 30, source_token_slippages: [0.5, 1], }; + const properties = { + chain_id_source: chainIdSource, + chain_id_destination: chainIdDestination, + ...sharedProperties, + }; const expectedProperties = { - chain_id_source: formatChainIdToCaip(ChainId.OPTIMISM), - chain_id_destination: formatChainIdToCaip(ChainId.OPTIMISM), source_token_count: sourceTokenAddresses.length, ...properties, }; diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 65e93ed15f..e52af2ab3c 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -25,7 +25,7 @@ import { ExchangeRateSourcesForLookup, selectIsAssetExchangeRateInState, } from './selectors'; -import { ChainId, FeatureId, RequestStatus } from './types'; +import { FeatureId, RequestStatus } from './types'; import type { L1GasFees, GenericQuoteRequest, @@ -1186,14 +1186,14 @@ export class BridgeController extends StaticIntervalPollingController; const batchSellBaseProperties = { - chain_id_source: formatChainIdToCaip( - batchSellQuoteRequest?.srcChainId ?? ChainId.ETH, - ), - chain_id_destination: batchSellQuoteRequest?.destChainId - ? formatChainIdToCaip(batchSellQuoteRequest.destChainId) - : null, + chain_id_source: batchSellClientChainProperties.chain_id_source, + chain_id_destination: batchSellClientChainProperties.chain_id_destination, location: clientProps?.location ?? this.#location, }; const quoteRequest = this.state.quoteRequest[quoteRequestIndex]; @@ -1219,33 +1219,33 @@ export class BridgeController extends StaticIntervalPollingController; -type BatchSellChainProperties = { - chain_id_source: CaipChainId; - chain_id_destination: CaipChainId | null; -}; - type BatchSellTokenPageEventProperties = BatchSellChainProperties & BatchSellTokenPageEventContext; type BatchSellSourceTokenEventProperties = BatchSellChainProperties & BatchSellSourceTokenEventContext & { - source_token_count: number; - }; + source_token_count: number; +}; type BatchSellQuotePageEventProperties = BatchSellChainProperties & BatchSellQuotePageEventContext & { - source_token_count: number; - }; + source_token_count: number; +}; type BatchSellReviewModalSubmittedEventProperties = BatchSellChainProperties & - BatchSellReviewModalSubmittedEventContext & { - source_token_count: number; - }; + BatchSellReviewModalSubmittedEventContext & { + source_token_count: number; + }; type SharedEventContextFromClient = { ab_tests?: Record; From 6e8c113e9a0eee746feee2597e9d737e3579b865 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:01:53 -0400 Subject: [PATCH 5/8] fix: sent amount usd for batch sell being incorrect --- .../bridge-controller/src/selectors.test.ts | 2 ++ packages/bridge-controller/src/selectors.ts | 22 +++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/bridge-controller/src/selectors.test.ts b/packages/bridge-controller/src/selectors.test.ts index 3c8c9f15ce..84f4b29a96 100644 --- a/packages/bridge-controller/src/selectors.test.ts +++ b/packages/bridge-controller/src/selectors.test.ts @@ -1980,6 +1980,8 @@ describe('Bridge Selectors', () => { "90ae8e69-f03a-4cf6-bab7-ed4e3431eb37", ] `); + expect(recommendedQuotes.map((quote) => quote?.sentAmount.usd)) + .toStrictEqual(['18', '140']); }); it('should return metadata when quotes are empty', () => { diff --git a/packages/bridge-controller/src/selectors.ts b/packages/bridge-controller/src/selectors.ts index 49f8b231cf..ad04ff3796 100644 --- a/packages/bridge-controller/src/selectors.ts +++ b/packages/bridge-controller/src/selectors.ts @@ -329,16 +329,7 @@ const selectBridgeQuotesWithMetadata = createBridgeSelector( [ ({ quotes }) => quotes, selectBridgeFeesPerGas, - createBridgeSelector( - [ - (state) => state, - ({ quoteRequest: [{ srcChainId, srcTokenAddress }] }) => - srcTokenAddress - ? formatAddressToAssetId(srcTokenAddress, srcChainId) - : undefined, - ], - selectExchangeRateByAssetId, - ), + (state) => state, createBridgeSelector( [ (state) => state, @@ -363,11 +354,20 @@ const selectBridgeQuotesWithMetadata = createBridgeSelector( ( quotes, bridgeFeesPerGas, - srcTokenExchangeRate, + exchangeRateSources, destTokenExchangeRate, nativeExchangeRate, ) => { const newQuotes = quotes.map((quote) => { + const sourceAssetId = + formatAddressToAssetId( + quote.quote.srcAsset.address, + quote.quote.srcChainId, + ) ?? quote.quote.srcAsset.assetId; + const srcTokenExchangeRate = selectExchangeRateByAssetId( + exchangeRateSources, + sourceAssetId, + ); const sentAmount = calcSentAmount(quote.quote, srcTokenExchangeRate); const toTokenAmount = calcToAmount( quote.quote.destTokenAmount, From e073ad1f032c61346821e7fd1f4f667f1f786dd9 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:12:11 -0400 Subject: [PATCH 6/8] fix: lint --- .../src/bridge-controller.ts | 31 +++++++------------ .../bridge-controller/src/selectors.test.ts | 5 +-- .../src/utils/metrics/types.ts | 11 +++---- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index e52af2ab3c..0b37efac21 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -230,7 +230,8 @@ export class BridgeController extends StaticIntervalPollingController( + trackMetaMetricsFn: ( eventName: EventName, properties: CrossChainSwapsEventProperties, ) => void; @@ -1186,11 +1185,10 @@ export class BridgeController extends StaticIntervalPollingController; + const batchSellClientChainProperties = propertiesFromClient as Pick< + RequiredEventContextFromClient[BatchSellMetricsEventName.BatchSellTokenPageViewed], + 'chain_id_source' | 'chain_id_destination' + >; const batchSellBaseProperties = { chain_id_source: batchSellClientChainProperties.chain_id_source, chain_id_destination: batchSellClientChainProperties.chain_id_destination, @@ -1223,8 +1221,7 @@ export class BridgeController extends StaticIntervalPollingController { "90ae8e69-f03a-4cf6-bab7-ed4e3431eb37", ] `); - expect(recommendedQuotes.map((quote) => quote?.sentAmount.usd)) - .toStrictEqual(['18', '140']); + expect( + recommendedQuotes.map((quote) => quote?.sentAmount.usd), + ).toStrictEqual(['18', '140']); }); it('should return metadata when quotes are empty', () => { diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index 08e4ee3fcc..82bf1b54e8 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -145,16 +145,15 @@ type BatchSellTokenPageEventProperties = BatchSellChainProperties & type BatchSellSourceTokenEventProperties = BatchSellChainProperties & BatchSellSourceTokenEventContext & { - source_token_count: number; -}; + source_token_count: number; + }; type BatchSellQuotePageEventProperties = BatchSellChainProperties & BatchSellQuotePageEventContext & { - source_token_count: number; -}; + source_token_count: number; + }; -type BatchSellReviewModalSubmittedEventProperties = - BatchSellChainProperties & +type BatchSellReviewModalSubmittedEventProperties = BatchSellChainProperties & BatchSellReviewModalSubmittedEventContext & { source_token_count: number; }; From 9646ddce75c1a0dcc8901c36a410d8fe586e7ab8 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:16:44 -0400 Subject: [PATCH 7/8] chore: changelog --- packages/bridge-controller/CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 0d24121f62..f391d8a096 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -9,7 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add Batch Sell token and quote page analytics event types +- Add Batch Sell analytics event types ([#9272](https://github.com/MetaMask/core/pull/9272)) + - `Batch Sell Token Page Viewed` + - `Batch Sell Token Page Continue Clicked` + - `Batch Sell Quote Page Viewed` + - `Batch Sell Quote Page Review Clicked` + - `Batch Sell Review Modal Submitted` ### Changed From 97015b3a5a22e9dd6e66a27aa35c42a66d3a118b Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:17:41 -0400 Subject: [PATCH 8/8] chore: changelog --- packages/bridge-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index f391d8a096..8c1c1f1a8d 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/multichain-network-controller` from `^3.1.4` to `^3.2.0` ([#9264](https://github.com/MetaMask/core/pull/9264)) +### Fixed + +- Fix Batch Sell-only `sentAmount.usd` metadata calculations to use each quote's source token exchange rate ([#9272](https://github.com/MetaMask/core/pull/9272)) + ## [77.0.0] ### Added