From 87754ed909462984b0d43d08a8f5f0ca4aeb0da6 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 2 Jun 2026 20:42:04 -0700 Subject: [PATCH 01/14] chore: update featureId type --- packages/bridge-controller/src/bridge-controller.ts | 3 +-- packages/bridge-controller/src/index.ts | 2 +- packages/bridge-controller/src/types.ts | 10 +++++++++- packages/bridge-controller/src/utils/fetch.test.ts | 4 ++-- packages/bridge-controller/src/utils/fetch.ts | 2 +- .../bridge-controller/src/utils/metrics/properties.ts | 2 ++ packages/bridge-controller/src/utils/quote.ts | 2 +- packages/bridge-controller/src/utils/validators.ts | 10 +--------- .../src/bridge-status-controller.ts | 2 +- 9 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index d8eefb0a0a..f878cb7a70 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 { RequestStatus } from './types'; +import { FeatureId, RequestStatus } from './types'; import type { L1GasFees, GenericQuoteRequest, @@ -88,7 +88,6 @@ import { } from './utils/quote'; import { appendFeesToQuotes } from './utils/quote-fees'; import { getMinimumBalanceForRentExemptionInLamports } from './utils/snaps'; -import type { FeatureId } from './utils/validators'; const metadata: StateMetadata = { quoteRequest: { diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index e1b3b447e8..273423a59d 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -83,6 +83,7 @@ export { SortOrder, ChainId, RequestStatus, + FeatureId, type TokenFeature, type QuoteStreamCompleteData, type BridgeControllerGetStateAction, @@ -93,7 +94,6 @@ export { FeeType, ActionTypes, BridgeAssetSchema, - FeatureId, TokenFeatureType, validateQuoteStreamComplete, QuoteStreamCompleteReason, diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index cd5e52ad93..f38b6b3bf5 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -35,7 +35,6 @@ import type { BridgeAssetSchema, ChainConfigurationSchema, ChainRankingSchema, - FeatureId, FeeDataSchema, IntentSchema, PlatformConfigSchema, @@ -259,6 +258,15 @@ export enum StatusTypes { COMPLETE = 'COMPLETE', } +export enum FeatureId { + PERPS = 'perps', + QUICK_BUY_FOLLOW_TRADING = 'quick_buy_follow_trading', + QUICK_BUY_TOKEN_DETAILS = 'quick_buy_token_details', + DAPP_SWAP = 'dapp_swap', + BATCH_SELL = 'batch_sell', + UNIFIED_SWAP_BRIDGE = 'unified_swap_bridge', +} + /** * These are types that components pass in. Since data is a mix of types when coming from the redux store, we need to use a generic type that can cover all the types. * Payloads with this type are transformed into QuoteRequest by fetchBridgeQuotes right before fetching quotes diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index 85dd6daa74..8836a131ed 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -4,7 +4,7 @@ import type { CaipAssetType } from '@metamask/utils'; import mockBridgeQuotesErc20Erc20 from '../../tests/mock-quotes-erc20-erc20.json'; import mockBridgeQuotesNativeErc20 from '../../tests/mock-quotes-native-erc20.json'; import { BridgeClientId, BRIDGE_PROD_API_BASE_URL } from '../constants/bridge'; -import { QuoteResponse } from '../types'; +import { QuoteResponse, FeatureId } from '../types'; import { fetchBridgeQuotes, fetchBridgeTokens, @@ -12,7 +12,7 @@ import { fetchBatchSellTrades, formatBatchSellTradesRequest, } from './fetch'; -import { BatchSellTransactionType, FeatureId } from './validators'; +import { BatchSellTransactionType } from './validators'; const mockFetchFn = jest.fn(); diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 59b7f1c7d8..9fd285701d 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -12,6 +12,7 @@ import type { QuoteStreamCompleteData, BatchSellTradesRequest, BatchSellTradesResponse, + FeatureId, } from '../types'; import { getEthUsdtResetData } from './bridge'; import { @@ -21,7 +22,6 @@ import { } from './caip-formatters'; import { fetchServerEvents } from './fetch-server-events'; import { isEvmTxData } from './trade-utils'; -import type { FeatureId } from './validators'; import { validateQuoteResponse, validateSwapsTokenObject, diff --git a/packages/bridge-controller/src/utils/metrics/properties.ts b/packages/bridge-controller/src/utils/metrics/properties.ts index a550e7190d..ce431dfcd8 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.ts @@ -22,6 +22,7 @@ import type { QuoteWarning, RequestParams, } from './types'; +import { FeatureId } from '../../types'; export const toInputChangedPropertyKey: Partial< Record @@ -182,5 +183,6 @@ export const getQuotesReceivedProperties = ( ...(hasSufficientGasForQuote !== undefined && { has_sufficient_gas_for_quote: hasSufficientGasForQuote, }), + feature_id: activeQuote?.featureId ?? FeatureId.UNIFIED_SWAP_BRIDGE, }; }; diff --git a/packages/bridge-controller/src/utils/quote.ts b/packages/bridge-controller/src/utils/quote.ts index 7d89e7d22d..ef13425670 100644 --- a/packages/bridge-controller/src/utils/quote.ts +++ b/packages/bridge-controller/src/utils/quote.ts @@ -17,8 +17,8 @@ import type { NonEvmFees, TxData, } from '../types'; +import { FeatureId } from '../types'; import { isNativeAddress, isNonEvmChainId } from './bridge'; -import { FeatureId } from './validators'; export const isValidQuoteRequest = ( partialRequest: Partial, diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 06030fb11e..1237866550 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -31,12 +31,6 @@ export enum FeeType { TX_FEE = 'txFee', } -export enum FeatureId { - PERPS = 'perps', - QUICK_BUY = 'quickBuy', - DAPP_SWAP = 'dappSwap', -} - export enum ActionTypes { BRIDGE = 'bridge', SWAP = 'swap', @@ -162,15 +156,13 @@ const GenericQuoteRequestSchema = type({ fee: optional(number()), }); -const FeatureIdSchema = enums(Object.values(FeatureId)); - /** * This is the schema for the feature flags response from the RemoteFeatureFlagController */ export const PlatformConfigSchema = type({ priceImpactThreshold: optional(PriceImpactThresholdSchema), quoteRequestOverrides: optional( - record(FeatureIdSchema, optional(GenericQuoteRequestSchema)), + record(string(), optional(GenericQuoteRequestSchema)), ), minimumVersion: string(), refreshRate: number(), diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 5f7fb8c4a3..79fb227736 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1,5 +1,5 @@ import type { StateMetadata } from '@metamask/base-controller'; -import type { +import { QuoteMetadata, RequiredEventContextFromClient, QuoteResponse, From 0d116b5904650663ee7cdb1c8d04fb261a86418f Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 2 Jun 2026 20:44:42 -0700 Subject: [PATCH 02/14] chore: add batch_id to controller metric types --- packages/bridge-controller/src/utils/metrics/types.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index 2ad785c827..13cbbe38f0 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import type { CaipAssetType, CaipChainId } from '@metamask/utils'; +import type { CaipAssetType, CaipChainId, Hex } from '@metamask/utils'; import type { SortOrder, StatusTypes } from '../../types'; import type { @@ -164,6 +164,7 @@ type RequiredEventContextFromClientBase = { | 'token_security_type_destination' > & { action_type: MetricsActionType; + batch_id: Hex; }; [UnifiedSwapBridgeEventName.Completed]: TradeData & Pick & @@ -176,6 +177,7 @@ type RequiredEventContextFromClientBase = { quote_vs_execution_ratio: number; quoted_vs_used_gas_ratio: number; action_type: MetricsActionType; + batch_id: Hex; }; [UnifiedSwapBridgeEventName.Failed]: | // Tx failed before confirmation @@ -203,6 +205,7 @@ type RequiredEventContextFromClientBase = { TradeData & { actual_time_minutes: number; error_message?: string; + batch_id: Hex; }); // Emitted by clients [UnifiedSwapBridgeEventName.AllQuotesOpened]: Pick< @@ -240,6 +243,7 @@ type RequiredEventContextFromClientBase = { }; [UnifiedSwapBridgeEventName.QuotesValidationFailed]: { failures: string[]; + // TODO optional batch_id }; [UnifiedSwapBridgeEventName.StatusValidationFailed]: { failures: string[]; @@ -261,6 +265,7 @@ type RequiredEventContextFromClientBase = { action_type: MetricsActionType; polling_status: PollingStatus; retry_attempts: number; + batch_id: Hex; }; }; From 3d8e11b94b7fa4d04f7bbaad254ace2aee58bfc5 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 2 Jun 2026 20:45:44 -0700 Subject: [PATCH 03/14] chore: export generateBatchId --- packages/transaction-controller/src/index.ts | 1 + packages/transaction-controller/src/utils/batch.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/index.ts b/packages/transaction-controller/src/index.ts index bccdc5b33a..7156185def 100644 --- a/packages/transaction-controller/src/index.ts +++ b/packages/transaction-controller/src/index.ts @@ -147,3 +147,4 @@ export type { GetAccountAddressRelationshipRequest, AccountAddressRelationshipResult, } from './api/accounts-api'; +export { generateBatchId } from './utils/batch'; diff --git a/packages/transaction-controller/src/utils/batch.ts b/packages/transaction-controller/src/utils/batch.ts index 0645744377..c5a2017460 100644 --- a/packages/transaction-controller/src/utils/batch.ts +++ b/packages/transaction-controller/src/utils/batch.ts @@ -232,7 +232,7 @@ export async function isAtomicBatchSupported( * * @returns A unique batch ID as a hexadecimal string. */ -function generateBatchId(): Hex { +export function generateBatchId(): Hex { const idString = v4(); const idBytes = new Uint8Array(parse(idString)); return bytesToHex(idBytes); From 52fb826b3b01908e34d00b3289e347f69ed0bfe1 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 2 Jun 2026 20:53:41 -0700 Subject: [PATCH 04/14] feat: add batch_id property to status events --- .../src/bridge-status-controller.batch-sell.test.ts | 9 +++++++++ .../src/bridge-status-controller.ts | 11 +++++++++++ .../src/strategy/batch-sell-strategy.ts | 2 ++ .../bridge-status-controller/src/strategy/types.ts | 6 ++++++ .../bridge-status-controller/src/utils/metrics.ts | 6 +++++- 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts index b47a325714..438fe9c74e 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts @@ -33,6 +33,12 @@ import type { import { getBatchSellHistoryItemsForTxHash } from './utils/history'; import { shouldDisable7702 } from './utils/transaction'; +const mockGenerateBatchId = jest.fn().mockReturnValue('0xBatchId1'); +jest.mock('@metamask/transaction-controller', () => ({ + ...jest.requireActual('@metamask/transaction-controller'), + generateBatchId: (): string => mockGenerateBatchId(), +})); + type AllBridgeStatusControllerActions = MessengerActions; @@ -197,6 +203,7 @@ describe('BridgeStatusController', () => { dateNowSpy.mockReturnValueOnce(1779922719705); dateNowSpy.mockReturnValueOnce(1779988819705); dateNowSpy.mockReturnValueOnce(1779988919705); + mockGenerateBatchId.mockReturnValueOnce('0xBatchId1'); }); it.each([true, false])( @@ -326,6 +333,7 @@ describe('BridgeStatusController', () => { usd_amount_source: 100, usd_quoted_gas: 0, usd_quoted_return: 0, + batch_id: '0xBatchId1', }, ], [ @@ -359,6 +367,7 @@ describe('BridgeStatusController', () => { origin: 'metamask', requireApproval: false, skipInitialGasEstimate: false, + batchId: '0xBatchId1', }); expect(transactions).toStrictEqual( diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 79fb227736..225f6b2eeb 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -24,6 +24,7 @@ import { TransactionStatus, TransactionType, TransactionController, + generateBatchId, } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; import { numberToHex } from '@metamask/utils'; @@ -459,6 +460,7 @@ export class BridgeStatusController extends StaticIntervalPollingController + quoteFeatureId === FeatureId.BATCH_SELL, + ) + ? generateBatchId() + : undefined; + const preConfirmationProperties = getPreConfirmationPropertiesFromQuote( quoteResponse, isStxEnabled, @@ -1110,6 +1119,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { const { quote } = quoteResponse; return { @@ -237,6 +239,7 @@ export const getPreConfirmationPropertiesFromQuote = ( activeAbTests.length > 0 && { active_ab_tests: activeAbTests, }), + ...(batchId && { batch_id: batchId }), }; }; @@ -369,5 +372,6 @@ export const getPollingStatusUpdatedProperties = ( action_type: MetricsActionType.SWAPBRIDGE_V1, polling_status: pollingStatus, retry_attempts: historyItem.attempts?.counter ?? 0, + ...(historyItem.batchId ? { batch_id: historyItem.batchId } : {}), }; }; From 7dbe8895063d5cbec28acea9cdab56e3b4e21687 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 2 Jun 2026 20:54:51 -0700 Subject: [PATCH 05/14] feat: require feature_id property --- packages/bridge-controller/src/utils/metrics/types.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index 13cbbe38f0..0ae7105bb0 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { CaipAssetType, CaipChainId, Hex } from '@metamask/utils'; -import type { SortOrder, StatusTypes } from '../../types'; +import type { FeatureId, SortOrder, StatusTypes } from '../../types'; import type { UnifiedSwapBridgeEventName, MetaMetricsSwapsEventSource, @@ -181,7 +181,7 @@ type RequiredEventContextFromClientBase = { }; [UnifiedSwapBridgeEventName.Failed]: | // Tx failed before confirmation - (TradeData & + (TradeData & Pick & Pick< RequestMetadata, @@ -357,6 +357,7 @@ export type CrossChainSwapsEventProperties< T extends UnifiedSwapBridgeEventName, > = | { + feature_id: FeatureId; action_type: MetricsActionType; location: MetaMetricsSwapsEventSource; ab_tests?: Record; From 2d48f12c711359fc6c65a121683d5e729c5a9641 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 2 Jun 2026 22:37:16 -0700 Subject: [PATCH 06/14] feat: add feature_id to all bridge-controller events --- .../bridge-controller.sse.batch.test.ts.snap | 5 + .../bridge-controller.sse.test.ts.snap | 18 ++ .../bridge-controller.test.ts.snap | 32 +++ .../src/bridge-controller.sse.batch.test.ts | 11 +- .../src/bridge-controller.sse.test.ts | 24 +- .../src/bridge-controller.test.ts | 219 ++++++++++++++---- .../src/bridge-controller.ts | 75 +++--- packages/bridge-controller/src/utils/fetch.ts | 3 + .../src/utils/metrics/types.ts | 9 + 9 files changed, 301 insertions(+), 95 deletions(-) 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 8c242116c1..0ee671c741 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 @@ -66,6 +66,7 @@ exports[`BridgeController BatchSell (multiple quote requests) SSE fetch quotes s "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "batch_sell", "input": "chain_source", "input_value": "eip155:10", "location": "Main View", @@ -75,6 +76,7 @@ exports[`BridgeController BatchSell (multiple quote requests) SSE fetch quotes s "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "batch_sell", "input": "chain_destination", "input_value": "eip155:137", "location": "Main View", @@ -84,6 +86,7 @@ exports[`BridgeController BatchSell (multiple quote requests) SSE fetch quotes s "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "batch_sell", "input": "token_destination", "input_value": "eip155:137/erc20:0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "location": "Main View", @@ -93,6 +96,7 @@ exports[`BridgeController BatchSell (multiple quote requests) SSE fetch quotes s "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "batch_sell", "input": "slippage", "input_value": 0.5, "location": "Main View", @@ -106,6 +110,7 @@ exports[`BridgeController BatchSell (multiple quote requests) SSE fetch quotes s "chain_id_destination": "eip155:137", "chain_id_source": "eip155:10", "custom_slippage": true, + "feature_id": "batch_sell", "has_sufficient_funds": true, "is_hardware_wallet": false, "location": "Main View", 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 76de1fc32f..76019e0e68 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 @@ -20,6 +20,7 @@ exports[`BridgeController SSE should publish validation failures 4`] = ` "lifi|trade.inputsToSign", "lifi|trade.raw_data_hex", ], + "feature_id": "unified_swap_bridge", "location": "Main View", "refresh_count": 1, "token_address_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:123d1", @@ -36,6 +37,7 @@ exports[`BridgeController SSE should publish validation failures 4`] = ` "failures": [ "unknown|unknown", ], + "feature_id": "unified_swap_bridge", "location": "Main View", "refresh_count": 1, "token_address_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:123d1", @@ -52,6 +54,7 @@ exports[`BridgeController SSE should publish validation failures 4`] = ` "failures": [ "unknown|quote", ], + "feature_id": "unified_swap_bridge", "location": "Main View", "refresh_count": 1, "token_address_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:123d1", @@ -71,6 +74,7 @@ exports[`BridgeController SSE should replace all stale quotes after a refresh an "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "eip155:1", "custom_slippage": true, + "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, "location": "Main View", @@ -99,6 +103,7 @@ exports[`BridgeController SSE should reset and refetch quotes after quote reques "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "eip155:1", "custom_slippage": true, + "feature_id": "unified_swap_bridge", "has_sufficient_funds": false, "is_hardware_wallet": false, "location": "Main View", @@ -127,6 +132,7 @@ exports[`BridgeController SSE should reset quotes list if quote refresh fails 2` "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "eip155:1", "custom_slippage": true, + "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, "location": "Main View", @@ -152,6 +158,7 @@ exports[`BridgeController SSE should reset quotes list if quote refresh fails 2` "chain_id_source": "eip155:1", "custom_slippage": true, "error_message": "Network error", + "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, "location": "Main View", @@ -213,6 +220,7 @@ exports[`BridgeController SSE should rethrow error from server 3`] = ` "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "chain_source", "input_value": "eip155:1", "location": "Main View", @@ -222,6 +230,7 @@ exports[`BridgeController SSE should rethrow error from server 3`] = ` "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "chain_destination", "input_value": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "location": "Main View", @@ -231,6 +240,7 @@ exports[`BridgeController SSE should rethrow error from server 3`] = ` "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "token_destination", "input_value": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:123d1", "location": "Main View", @@ -240,6 +250,7 @@ exports[`BridgeController SSE should rethrow error from server 3`] = ` "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "slippage", "input_value": 0.5, "location": "Main View", @@ -253,6 +264,7 @@ exports[`BridgeController SSE should rethrow error from server 3`] = ` "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "eip155:1", "custom_slippage": true, + "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, "location": "Main View", @@ -278,6 +290,7 @@ exports[`BridgeController SSE should rethrow error from server 3`] = ` "chain_id_source": "eip155:1", "custom_slippage": true, "error_message": "Bridge-api error: timeout from server", + "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, "location": "Main View", @@ -339,6 +352,7 @@ exports[`BridgeController SSE should trigger quote polling if request is valid 2 "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "chain_source", "input_value": "eip155:1", "location": "Main View", @@ -348,6 +362,7 @@ exports[`BridgeController SSE should trigger quote polling if request is valid 2 "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "chain_destination", "input_value": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "location": "Main View", @@ -357,6 +372,7 @@ exports[`BridgeController SSE should trigger quote polling if request is valid 2 "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "token_destination", "input_value": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:123d1", "location": "Main View", @@ -366,6 +382,7 @@ exports[`BridgeController SSE should trigger quote polling if request is valid 2 "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "slippage", "input_value": 0.5, "location": "Main View", @@ -379,6 +396,7 @@ exports[`BridgeController SSE should trigger quote polling if request is valid 2 "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "eip155:1", "custom_slippage": true, + "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, "location": "Main View", 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 e435d9ba2e..3b2eceb75a 100644 --- a/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap +++ b/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap @@ -79,6 +79,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent bridge-status-controller c "chain_id_source": "eip155:1", "custom_slippage": true, "destination_transaction": "PENDING", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -119,6 +120,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent bridge-status-controller c "custom_slippage": true, "destination_transaction": "PENDING", "error_message": "error_message", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "has_gas_included_quote": false, @@ -159,6 +161,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent bridge-status-controller c "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": true, "error_message": "Failed to submit tx", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "has_gas_included_quote": false, @@ -198,6 +201,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent bridge-status-controller c "failures": [ "Failed to submit tx", ], + "feature_id": "perps", "location": "Main View", "refresh_count": 0, }, @@ -215,6 +219,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent bridge-status-controller c "chain_id_destination": "eip155:1", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -245,6 +250,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "chain_id_destination": null, "chain_id_source": "eip155:1", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "has_gas_included_quote": false, "initial_load_time_all_quotes": 0, @@ -278,6 +284,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "chain_id_destination": null, "chain_id_source": "eip155:1", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "has_gas_included_quote": false, "initial_load_time_all_quotes": 0, @@ -308,6 +315,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "action_type": "swapbridge-v1", "chain_id": "1", "chain_name": "Ethereum", + "feature_id": "unified_swap_bridge", "location": "Main View", "token_contract": "0x123", "token_name": "ETH", @@ -325,6 +333,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "action_type": "swapbridge-v1", "chain_id_destination": null, "chain_id_source": "eip155:1", + "feature_id": "quick_buy_follow_trading", "location": "Main View", "token_address_destination": null, "token_address_source": "eip155:1/slip44:60", @@ -344,6 +353,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "action_type": "swapbridge-v1", "chain_id_destination": "eip155:10", "chain_id_source": "eip155:1", + "feature_id": "unified_swap_bridge", "location": "Main View", "security_warnings": [ "warning1", @@ -368,6 +378,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "chain_id_destination": null, "chain_id_source": "eip155:1", "custom_slippage": false, + "feature_id": "quick_buy_token_details", "is_hardware_wallet": false, "location": "Main View", "slippage_limit": undefined, @@ -392,6 +403,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "chain_id_destination": null, "chain_id_source": "eip155:1", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "has_gas_included_quote": false, @@ -437,6 +449,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "chain_id_destination": null, "chain_id_source": "eip155:1", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "has_gas_included_quote": false, @@ -471,6 +484,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should only poll once i "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "chain_source", "input_value": "eip155:1", "location": "Main View", @@ -480,6 +494,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should only poll once i "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "chain_destination", "input_value": "eip155:10", "location": "Main View", @@ -489,6 +504,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should only poll once i "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "token_destination", "input_value": "eip155:10/erc20:0x123", "location": "Main View", @@ -498,6 +514,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should only poll once i "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "slippage", "input_value": 0.5, "location": "Main View", @@ -511,6 +528,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should only poll once i "chain_id_destination": "eip155:10", "chain_id_source": "eip155:1", "custom_slippage": true, + "feature_id": "unified_swap_bridge", "has_sufficient_funds": false, "is_hardware_wallet": false, "location": "Main View", @@ -537,6 +555,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should only poll once i "chain_id_destination": "eip155:10", "chain_id_source": "eip155:1", "custom_slippage": true, + "feature_id": "dapp_swap", "gas_included": false, "gas_included_7702": false, "has_gas_included_quote": false, @@ -903,6 +922,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "chain_source", "input_value": "eip155:1", "location": "Main View", @@ -912,6 +932,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "chain_destination", "input_value": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "location": "Main View", @@ -921,6 +942,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "token_destination", "input_value": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:123d1", "location": "Main View", @@ -930,6 +952,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "slippage", "input_value": 0.5, "location": "Main View", @@ -943,6 +966,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "eip155:1", "custom_slippage": true, + "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, "location": "Main View", @@ -967,6 +991,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "eip155:1", "custom_slippage": true, + "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, "location": "Main View", @@ -991,6 +1016,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "eip155:1", "custom_slippage": true, + "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, "location": "Main View", @@ -1016,6 +1042,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "chain_id_source": "eip155:1", "custom_slippage": true, "error_message": "Network error", + "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, "location": "Main View", @@ -1040,6 +1067,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "eip155:1", "custom_slippage": true, + "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, "location": "Main View", @@ -1064,6 +1092,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should update the quote "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "chain_source", "input_value": "eip155:1", "location": "Main View", @@ -1073,6 +1102,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should update the quote "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "chain_destination", "input_value": "eip155:10", "location": "Main View", @@ -1082,6 +1112,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should update the quote "Unified SwapBridge Input Changed", { "action_type": "swapbridge-v1", + "feature_id": "unified_swap_bridge", "input": "slippage", "input_value": 0.5, "location": "Main View", @@ -1203,6 +1234,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams: should handle malforme "socket|quote.destAsset.address", "lifi|quote.srcAsset.decimals", ], + "feature_id": "unified_swap_bridge", "location": "Main View", "refresh_count": 0, "token_address_destination": "eip155:1/slip44:60", diff --git a/packages/bridge-controller/src/bridge-controller.sse.batch.test.ts b/packages/bridge-controller/src/bridge-controller.sse.batch.test.ts index ea2bd5dbc6..dba30e1cb4 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.batch.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.batch.test.ts @@ -20,7 +20,7 @@ import { DEFAULT_BRIDGE_CONTROLLER_STATE, } from './constants/bridge'; import * as selectors from './selectors'; -import { ChainId, RequestStatus } from './types'; +import { ChainId, RequestStatus, FeatureId } from './types'; import type { BridgeControllerMessenger, QuoteResponse } from './types'; import * as balanceUtils from './utils/balance'; import * as featureFlagUtils from './utils/feature-flags'; @@ -62,6 +62,7 @@ const quoteRequest = { resetApproval: false, }; const metricsContext = { + feature_id: FeatureId.BATCH_SELL, token_symbol_source: 'ETH', token_symbol_destination: 'USDC', usd_amount_source: 100, @@ -375,6 +376,7 @@ describe('BridgeController BatchSell (multiple quote requests) SSE', function () }, ], expect.any(AbortSignal), + FeatureId.BATCH_SELL, BridgeClientId.EXTENSION, 'AUTH_TOKEN', BRIDGE_PROD_API_BASE_URL, @@ -423,6 +425,7 @@ describe('BridgeController BatchSell (multiple quote requests) SSE', function () l1GasFeesInHexWei: '0x1', resetApproval: undefined, quoteRequestIndex: 0, + featureId: FeatureId.BATCH_SELL, })) .concat( mockBridgeQuotesErc20Erc20.map( @@ -432,6 +435,7 @@ describe('BridgeController BatchSell (multiple quote requests) SSE', function () l1GasFeesInHexWei: '0x2', resetApproval: undefined, quoteRequestIndex: 1, + featureId: FeatureId.BATCH_SELL, }) as never, ), ), @@ -749,7 +753,10 @@ describe('BridgeController BatchSell (multiple quote requests) SSE', function () 'BridgeController:updateBatchSellTrades', [], ); - rootMessenger.call('BridgeController:resetState'); + rootMessenger.call( + 'BridgeController:resetState', + FeatureId.BATCH_SELL, + ); await jest.advanceTimersByTimeAsync(1000); await flushPromises(); diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 0c10142524..b6f0058d0d 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -29,7 +29,7 @@ import { DEFAULT_BRIDGE_CONTROLLER_STATE, ETH_USDT_ADDRESS, } from './constants/bridge'; -import { ChainId, RequestStatus } from './types'; +import { ChainId, RequestStatus, FeatureId } from './types'; import type { BridgeControllerMessenger, QuoteResponse, TxData } from './types'; import * as balanceUtils from './utils/balance'; import { formatChainIdToDec } from './utils/caip-formatters'; @@ -81,6 +81,7 @@ const quoteRequest = { resetApproval: false, }; const metricsContext = { + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, token_symbol_source: 'ETH', token_symbol_destination: 'USDC', usd_amount_source: 100, @@ -285,6 +286,7 @@ describe('BridgeController SSE', function () { }, ], expect.any(AbortSignal), + FeatureId.UNIFIED_SWAP_BRIDGE, BridgeClientId.EXTENSION, 'AUTH_TOKEN', BRIDGE_PROD_API_BASE_URL, @@ -321,6 +323,7 @@ describe('BridgeController SSE', function () { ...quote, l1GasFeesInHexWei: '0x1', resetApproval: undefined, + featureId: FeatureId.UNIFIED_SWAP_BRIDGE, })), quotesRefreshCount: 1, quotesLoadingStatus: 1, @@ -450,6 +453,7 @@ describe('BridgeController SSE', function () { }, ], expect.any(AbortSignal), + FeatureId.UNIFIED_SWAP_BRIDGE, BridgeClientId.EXTENSION, 'AUTH_TOKEN', BRIDGE_PROD_API_BASE_URL, @@ -488,6 +492,7 @@ describe('BridgeController SSE', function () { ], quotes: mockUSDTQuoteResponse.map((quote) => ({ ...quote, + featureId: FeatureId.UNIFIED_SWAP_BRIDGE, resetApproval: tradeData ? { ...quote.approval, @@ -609,6 +614,7 @@ describe('BridgeController SSE', function () { }, ], expect.any(AbortSignal), + FeatureId.UNIFIED_SWAP_BRIDGE, BridgeClientId.EXTENSION, 'AUTH_TOKEN', BRIDGE_PROD_API_BASE_URL, @@ -645,6 +651,7 @@ describe('BridgeController SSE', function () { ], quotes: mockUSDTQuoteResponse.map((quote) => ({ ...quote, + featureId: FeatureId.UNIFIED_SWAP_BRIDGE, resetApproval: { ...quote.approval, data: '0x095ea7b30000000000000000000000000439e60f02a8900a951603950d8d4527f400c3f10000000000000000000000000000000000000000000000000000000000000000', @@ -701,6 +708,7 @@ describe('BridgeController SSE', function () { ...quote, l1GasFeesInHexWei: '0x1', resetApproval: undefined, + featureId: FeatureId.UNIFIED_SWAP_BRIDGE, })), ); const t1 = bridgeController.state.quotesLastFetched; @@ -724,6 +732,7 @@ describe('BridgeController SSE', function () { quotes: [mockBridgeQuotesNativeErc20Eth[0]].map((quote) => ({ ...quote, resetApproval: undefined, + featureId: FeatureId.UNIFIED_SWAP_BRIDGE, })), quotesLoadingStatus: RequestStatus.LOADING, quotesRefreshCount: 1, @@ -750,6 +759,7 @@ describe('BridgeController SSE', function () { quotes: mockBridgeQuotesNativeErc20Eth.map((quote) => ({ ...quote, resetApproval: undefined, + featureId: FeatureId.UNIFIED_SWAP_BRIDGE, })), quotesLastFetched: t2, quotesRefreshCount: 2, @@ -819,6 +829,7 @@ describe('BridgeController SSE', function () { mockBridgeQuotesNativeErc20Eth.map((quote) => ({ ...quote, resetApproval: undefined, + featureId: FeatureId.UNIFIED_SWAP_BRIDGE, })), ); const t2 = bridgeController.state.quotesLastFetched; @@ -958,6 +969,7 @@ describe('BridgeController SSE', function () { security_warnings: [], usd_amount_source: 100, token_security_type_destination: null, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); // Right after state update, before fetch has started @@ -989,6 +1001,7 @@ describe('BridgeController SSE', function () { ...mockBridgeQuotesNativeErc20[0], l1GasFeesInHexWei: '0x1', resetApproval: undefined, + featureId: FeatureId.UNIFIED_SWAP_BRIDGE, }, ], quotesRefreshCount: 0, @@ -1026,6 +1039,7 @@ describe('BridgeController SSE', function () { ...quote, l1GasFeesInHexWei: '0x1', resetApproval: undefined, + featureId: FeatureId.UNIFIED_SWAP_BRIDGE, })), assetExchangeRates, }); @@ -1142,6 +1156,7 @@ describe('BridgeController SSE', function () { security_warnings: [], usd_amount_source: 100, token_security_type_destination: 'test', + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); @@ -1168,6 +1183,7 @@ describe('BridgeController SSE', function () { [...mockBridgeQuotesNativeErc20, ...mockBridgeQuotesNativeErc20].map( (quote) => ({ ...quote, + featureId: FeatureId.UNIFIED_SWAP_BRIDGE, l1GasFeesInHexWei: '0x1', resetApproval: undefined, }), @@ -1198,6 +1214,7 @@ describe('BridgeController SSE', function () { quotes: [mockBridgeQuotesNativeErc20Eth[0]].map((quote) => ({ ...quote, resetApproval: undefined, + featureId: FeatureId.UNIFIED_SWAP_BRIDGE, })), quotesRefreshCount: 1, quoteFetchError: null, @@ -1344,6 +1361,7 @@ describe('BridgeController SSE', function () { }, ], expect.any(AbortSignal), + FeatureId.UNIFIED_SWAP_BRIDGE, BridgeClientId.EXTENSION, 'AUTH_TOKEN', BRIDGE_PROD_API_BASE_URL, @@ -1456,7 +1474,7 @@ describe('BridgeController SSE', function () { expect(bridgeController.state.tokenWarnings).toStrictEqual([mockWarning]); - bridgeController.resetState(); + bridgeController.resetState(FeatureId.UNIFIED_SWAP_BRIDGE); expect(bridgeController.state.tokenWarnings).toStrictEqual([]); }); }); @@ -1654,7 +1672,7 @@ describe('BridgeController SSE', function () { mockComplete, ); - bridgeController.resetState(); + bridgeController.resetState(FeatureId.UNIFIED_SWAP_BRIDGE); expect(bridgeController.state.quoteStreamComplete).toBeNull(); }); }); diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 07d58a7b44..2c880537b9 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -27,10 +27,17 @@ import { BridgeClientId, BRIDGE_PROD_API_BASE_URL, DEFAULT_BRIDGE_CONTROLLER_STATE, + ETH_USDT_ADDRESS, } from './constants/bridge'; import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; import * as selectors from './selectors'; -import { ChainId, RequestStatus, SortOrder, StatusTypes } from './types'; +import { + ChainId, + RequestStatus, + SortOrder, + StatusTypes, + FeatureId, +} from './types'; import type { BridgeControllerMessenger, QuoteResponse, @@ -51,7 +58,6 @@ import { MetricsSwapType, UnifiedSwapBridgeEventName, } from './utils/metrics/constants'; -import { FeatureId } from './utils/validators'; const EMPTY_INIT_STATE = DEFAULT_BRIDGE_CONTROLLER_STATE; @@ -94,6 +100,7 @@ const bridgeConfig = { }; const metricsContext = { + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, token_symbol_source: 'ETH', token_symbol_destination: 'USDC', usd_amount_source: 100, @@ -166,7 +173,10 @@ async function withController( ...options, }); if (!options.state) { - newRootMessenger.call('BridgeController:resetState'); + newRootMessenger.call( + 'BridgeController:resetState', + FeatureId.UNIFIED_SWAP_BRIDGE, + ); } return await testFunction({ controller, rootMessenger: newRootMessenger }); } @@ -623,7 +633,10 @@ describe('BridgeController', function () { srcTokenAddress: '0x2ABC', }); - rootMessenger.call('BridgeController:resetState'); + rootMessenger.call( + 'BridgeController:resetState', + FeatureId.UNIFIED_SWAP_BRIDGE, + ); expect(bridgeController.state.quoteRequest[0]).toStrictEqual({ srcTokenAddress: '0x0000000000000000000000000000000000000000', }); @@ -669,7 +682,10 @@ describe('BridgeController', function () { 'Warning', ); - rootMessenger.call('BridgeController:resetState'); + rootMessenger.call( + 'BridgeController:resetState', + FeatureId.UNIFIED_SWAP_BRIDGE, + ); expect(bridgeController.state.tokenSecurityTypeDestination).toBeNull(); }, ); @@ -927,7 +943,7 @@ describe('BridgeController', function () { 'AUTH_TOKEN', mockFetchFn, BRIDGE_PROD_API_BASE_URL, - null, + FeatureId.UNIFIED_SWAP_BRIDGE, '13.7.0', ); expect(bridgeController.state.quotesLastFetched).toBeCloseTo( @@ -1036,6 +1052,7 @@ describe('BridgeController', function () { security_warnings: [], usd_amount_source: 100, token_security_type_destination: null, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); await flushPromises(); @@ -1530,7 +1547,7 @@ describe('BridgeController', function () { 'AUTH_TOKEN', mockFetchFn, BRIDGE_PROD_API_BASE_URL, - null, + FeatureId.UNIFIED_SWAP_BRIDGE, '13.7.0', ); expect(bridgeController.state.quotesLastFetched).toBeCloseTo( @@ -1588,6 +1605,7 @@ describe('BridgeController', function () { best_quote_provider: 'provider_bridge2', can_submit: true, usd_balance_source: 0, + feature_id: FeatureId.DAPP_SWAP, }, ); @@ -1932,7 +1950,6 @@ describe('BridgeController', function () { }); it('updateBridgeQuoteRequestParams should include auth token as Authentication header', async function () { - jest.useFakeTimers(); await withController( async ({ controller: bridgeController, rootMessenger }) => { const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); @@ -1957,26 +1974,23 @@ describe('BridgeController', function () { .spyOn(selectors, 'selectIsAssetExchangeRateInState') .mockReturnValue(true); + jest + .spyOn(balanceUtils, 'hasSufficientBalance') + .mockResolvedValue(true); const fetchBridgeQuotesSpy = jest .spyOn(fetchUtils, 'fetchBridgeQuotes') - .mockImplementationOnce(async () => { - return await new Promise((resolve) => { - return setTimeout(() => { - resolve({ - quotes: mockBridgeQuotesNativeErc20Eth as never, - validationFailures: [], - }); - }, 5000); - }); + .mockResolvedValueOnce({ + quotes: mockBridgeQuotesNativeErc20Eth as never, + validationFailures: [], }); const quoteParams = { srcChainId: '0x1', destChainId: '0xa', srcTokenAddress: '0x0000000000000000000000000000000000000000', - destTokenAddress: '0x123', + destTokenAddress: ETH_USDT_ADDRESS, srcTokenAmount: '1000000000000000000', - walletAddress: '0x123', + walletAddress: ETH_USDT_ADDRESS, slippage: 0.5, }; @@ -1989,6 +2003,7 @@ describe('BridgeController', function () { await advanceToNthTimerThenFlush(); expect(startPollingSpy).toHaveBeenCalledTimes(1); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); expect(fetchBridgeQuotesSpy.mock.calls[0][3]).toBe('AUTH_TOKEN'); }, ); @@ -2155,7 +2170,7 @@ describe('BridgeController', function () { 'AUTH_TOKEN', mockFetchFn, BRIDGE_PROD_API_BASE_URL, - null, + FeatureId.UNIFIED_SWAP_BRIDGE, '13.7.0', ); expect(bridgeController.state.quotesLastFetched).toBeCloseTo( @@ -2296,7 +2311,10 @@ describe('BridgeController', function () { expect(bridgeController.state.quotes).toStrictEqual([]); // Verify state is reset - rootMessenger.call('BridgeController:resetState'); + rootMessenger.call( + 'BridgeController:resetState', + FeatureId.UNIFIED_SWAP_BRIDGE, + ); expect(bridgeController.state.quoteFetchError).toBeNull(); expect(bridgeController.state.quotesLoadingStatus).toBeNull(); expect(bridgeController.state.quotes).toStrictEqual([]); @@ -2967,6 +2985,7 @@ describe('BridgeController', function () { token_symbol_destination: 'USDC', usd_amount_source: 100, token_security_type_destination: null, + feature_id: FeatureId.QUICK_BUY_FOLLOW_TRADING, }, ); jest.clearAllMocks(); @@ -2977,6 +2996,7 @@ describe('BridgeController', function () { location: MetaMetricsSwapsEventSource.MainView, token_symbol_source: 'ETH', token_symbol_destination: null, + feature_id: FeatureId.QUICK_BUY_FOLLOW_TRADING, }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -3002,13 +3022,14 @@ describe('BridgeController', function () { token_symbol_destination: 'USDC', usd_amount_source: 100, token_security_type_destination: null, + feature_id: FeatureId.QUICK_BUY_FOLLOW_TRADING, }, ); jest.clearAllMocks(); rootMessenger.call( 'BridgeController:trackUnifiedSwapBridgeEvent', UnifiedSwapBridgeEventName.PageViewed, - {}, + { feature_id: FeatureId.QUICK_BUY_TOKEN_DETAILS }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -3033,6 +3054,7 @@ describe('BridgeController', function () { token_symbol_destination: 'USDC', usd_amount_source: 100, token_security_type_destination: null, + feature_id: FeatureId.QUICK_BUY_FOLLOW_TRADING, }, ); jest.clearAllMocks(); @@ -3043,6 +3065,7 @@ describe('BridgeController', function () { input: 'token_amount_source', input_value: '1', input_amount_preset: InputAmountPreset.PERCENT_90, + feature_id: FeatureId.QUICK_BUY_FOLLOW_TRADING, }, ); @@ -3054,6 +3077,7 @@ describe('BridgeController', function () { input: 'token_amount_source', input_value: '1', input_amount_preset: InputAmountPreset.PERCENT_90, + feature_id: FeatureId.QUICK_BUY_FOLLOW_TRADING, }), ); }); @@ -3076,6 +3100,7 @@ describe('BridgeController', function () { token_symbol_destination: 'USDC', usd_amount_source: 100, token_security_type_destination: null, + feature_id: FeatureId.QUICK_BUY_FOLLOW_TRADING, }, ); jest.clearAllMocks(); @@ -3086,6 +3111,7 @@ describe('BridgeController', function () { input: 'token_amount_source', input_value: '1', input_amount_preset: '85%', + feature_id: FeatureId.QUICK_BUY_FOLLOW_TRADING, }, ); rootMessenger.call( @@ -3095,6 +3121,7 @@ describe('BridgeController', function () { input: 'token_amount_source', input_value: '1', input_amount_preset: '95%', + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); @@ -3133,6 +3160,7 @@ describe('BridgeController', function () { token_symbol_destination: 'USDC', usd_amount_source: 100, token_security_type_destination: null, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); jest.clearAllMocks(); @@ -3147,6 +3175,7 @@ describe('BridgeController', function () { token_address_source: getNativeAssetForChainId(1).assetId, chain_id_destination: formatChainIdToCaip(10), token_address_destination: getNativeAssetForChainId(10).assetId, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -3172,6 +3201,7 @@ describe('BridgeController', function () { token_symbol_destination: 'USDC', usd_amount_source: 100, token_security_type_destination: null, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); jest.clearAllMocks(); @@ -3185,6 +3215,7 @@ describe('BridgeController', function () { gas_included: false, stx_enabled: false, can_submit: true, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -3210,6 +3241,7 @@ describe('BridgeController', function () { token_symbol_destination: 'USDC', usd_amount_source: 100, token_security_type_destination: null, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); jest.clearAllMocks(); @@ -3225,6 +3257,7 @@ describe('BridgeController', function () { best_quote_provider: 'provider_bridge2', token_symbol_destination: 'USDC', can_submit: true, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -3250,6 +3283,7 @@ describe('BridgeController', function () { token_symbol_destination: 'USDC', usd_amount_source: 100, token_security_type_destination: null, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); jest.clearAllMocks(); @@ -3267,6 +3301,7 @@ describe('BridgeController', function () { provider: 'provider_bridge', best_quote_provider: 'provider_bridge2', can_submit: false, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -3292,6 +3327,7 @@ describe('BridgeController', function () { token_symbol_destination: 'USDC', usd_amount_source: 100, token_security_type_destination: null, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); jest.clearAllMocks(); @@ -3310,6 +3346,7 @@ describe('BridgeController', function () { best_quote_provider: 'provider_bridge2', can_submit: true, usd_balance_source: 0, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); expect(messengerCallMock.mock.calls).toMatchSnapshot(); @@ -3332,6 +3369,7 @@ describe('BridgeController', function () { token_symbol_destination: 'USDC', usd_amount_source: 100, token_security_type_destination: 'Malicious', + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); jest.clearAllMocks(); @@ -3350,6 +3388,7 @@ describe('BridgeController', function () { best_quote_provider: 'provider_bridge2', can_submit: true, usd_balance_source: 0, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -3378,6 +3417,7 @@ describe('BridgeController', function () { token_symbol_destination: 'USDC', usd_amount_source: 100, token_security_type_destination: null, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); jest.clearAllMocks(); @@ -3390,6 +3430,7 @@ describe('BridgeController', function () { token_contract: '0x123', chain_name: 'Ethereum', chain_id: '1', + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -3442,6 +3483,7 @@ describe('BridgeController', function () { token_symbol_destination: 'USDC', stx_enabled: false, usd_amount_source: 100, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -3485,6 +3527,7 @@ describe('BridgeController', function () { chain_id_destination: formatChainIdToCaip(10), token_symbol_destination: 'USDC', token_address_destination: getNativeAssetForChainId(10).assetId, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -3526,6 +3569,7 @@ describe('BridgeController', function () { token_address_destination: getNativeAssetForChainId(ChainId.SOLANA) .assetId, security_warnings: [], + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); expect(messengerCallMock).toHaveBeenCalledTimes(0); @@ -3574,6 +3618,7 @@ describe('BridgeController', function () { token_symbol_destination: 'USDC', stx_enabled: false, usd_amount_source: 100, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -3611,6 +3656,7 @@ describe('BridgeController', function () { { failures: ['Failed to submit tx'], refresh_count: 0, + feature_id: FeatureId.PERPS, }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -3682,6 +3728,7 @@ describe('BridgeController', function () { usd_amount_source: 100, token_symbol_destination: 'USDC', token_security_type_destination: null, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); rootMessenger.call( @@ -3699,6 +3746,7 @@ describe('BridgeController', function () { best_quote_provider: 'provider_bridge2', can_submit: true, usd_balance_source: 0, + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(0); @@ -3752,9 +3800,11 @@ describe('BridgeController', function () { ...overrides, }); + let getBridgeFeatureFlagsSpy: jest.SpyInstance; + beforeEach(() => { jest.clearAllMocks(); - jest + getBridgeFeatureFlagsSpy = jest .spyOn(featureFlagUtils, 'getBridgeFeatureFlags') .mockReturnValueOnce({ ...defaultFlags, @@ -3767,6 +3817,7 @@ describe('BridgeController', function () { }, }); messengerCallMock.mockResolvedValueOnce('AUTH_TOKEN'); + messengerCallMock.mockResolvedValueOnce('AUTH_TOKEN'); messengerCallMock.mockReturnValueOnce(() => ({ address: '0x123', })); @@ -3799,8 +3850,8 @@ describe('BridgeController', function () { gasIncluded7702: false, fee: 0, }, - null, FeatureId.PERPS, + null, ); expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); @@ -3871,8 +3922,8 @@ describe('BridgeController', function () { gasIncluded: false, gasIncluded7702: false, } as never, - null, FeatureId.PERPS, + null, ), ).rejects.toThrow('Account address is required'); @@ -3906,8 +3957,8 @@ describe('BridgeController', function () { gasIncluded: false, gasIncluded7702: false, }, - null, FeatureId.PERPS, + null, ); expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); @@ -3975,35 +4026,102 @@ describe('BridgeController', function () { gasIncluded: false, gasIncluded7702: false, }, + FeatureId.UNIFIED_SWAP_BRIDGE, null, ); expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); expect(fetchBridgeQuotesSpy.mock.calls).toMatchInlineSnapshot(` - [ - [ - { - "destChainId": "1", - "destTokenAddress": "0x1234", - "gasIncluded": false, - "gasIncluded7702": false, - "resetApproval": false, - "slippage": 0.5, - "srcChainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", - "srcTokenAddress": "NATIVE", - "srcTokenAmount": "1000000", - "walletAddress": "0x123", - }, - null, - "extension", - "AUTH_TOKEN", - [Function], - "https://bridge.api.cx.metamask.io", - null, - "13.7.0", - ], - ] - `); + [ + [ + { + "destChainId": "1", + "destTokenAddress": "0x1234", + "gasIncluded": false, + "gasIncluded7702": false, + "resetApproval": false, + "slippage": 0.5, + "srcChainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "srcTokenAddress": "NATIVE", + "srcTokenAmount": "1000000", + "walletAddress": "0x123", + }, + null, + "extension", + "AUTH_TOKEN", + [Function], + "https://bridge.api.cx.metamask.io", + "unified_swap_bridge", + "13.7.0", + ], + ] + `); + expect(quotes).toStrictEqual(mockBridgeQuotesSolErc20); + expect(bridgeController.state).toStrictEqual(expectedControllerState); + }, + ); + }); + + it('should not add aggIds and fee if quoteRequestOverrides is not set', async () => { + await withController( + async ({ controller: bridgeController, rootMessenger }) => { + getBridgeFeatureFlagsSpy.mockRestore(); + getBridgeFeatureFlagsSpy.mockReturnValueOnce({ + ...defaultFlags, + quoteRequestOverrides: undefined, + }); + + const fetchBridgeQuotesSpy = jest + .spyOn(fetchUtils, 'fetchBridgeQuotes') + .mockResolvedValueOnce({ + quotes: mockBridgeQuotesSolErc20 as never, + validationFailures: [], + }); + const expectedControllerState = bridgeController.state; + + const quotes = await rootMessenger.call( + 'BridgeController:fetchQuotes', + { + srcChainId: SolScope.Mainnet, + destChainId: '1', + srcTokenAddress: 'NATIVE', + destTokenAddress: '0x1234', + srcTokenAmount: '1000000', + walletAddress: '0x123', + slippage: 0.5, + gasIncluded: false, + gasIncluded7702: false, + }, + FeatureId.PERPS, + null, + ); + + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + expect(fetchBridgeQuotesSpy.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "destChainId": "1", + "destTokenAddress": "0x1234", + "gasIncluded": false, + "gasIncluded7702": false, + "resetApproval": false, + "slippage": 0.5, + "srcChainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "srcTokenAddress": "NATIVE", + "srcTokenAmount": "1000000", + "walletAddress": "0x123", + }, + null, + "extension", + "AUTH_TOKEN", + [Function], + "https://bridge.api.cx.metamask.io", + "perps", + "13.7.0", + ], + ] + `); expect(quotes).toStrictEqual(mockBridgeQuotesSolErc20); expect(bridgeController.state).toStrictEqual(expectedControllerState); }, @@ -4036,6 +4154,7 @@ describe('BridgeController', function () { const quotes = await rootMessenger.call( 'BridgeController:fetchQuotes', makeQuoteRequest(), + FeatureId.UNIFIED_SWAP_BRIDGE, ); expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index f878cb7a70..01abb1852b 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -185,25 +185,8 @@ const metadata: StateMetadata = { */ type BridgePollingInput = { quoteRequests: GenericQuoteRequest[]; - context: Pick< - RequiredEventContextFromClient, - UnifiedSwapBridgeEventName.QuotesError - >[UnifiedSwapBridgeEventName.QuotesError] & - Pick< - RequiredEventContextFromClient, - UnifiedSwapBridgeEventName.QuotesRequested - >[UnifiedSwapBridgeEventName.QuotesRequested] & - /** - * Client-supplied security classification for the destination token - * (e.g. from token security/scanning data). Stored on the controller - * and merged into every analytics event that includes - * `token_address_destination`. Pass `null` when no security data is - * available for the selected destination token. - */ - Pick< - RequiredEventContextFromClient[UnifiedSwapBridgeEventName.InputSourceDestinationSwitched], - 'token_security_type_destination' - >; + context: RequiredEventContextFromClient[UnifiedSwapBridgeEventName.QuotesError] & + RequiredEventContextFromClient[UnifiedSwapBridgeEventName.QuotesRequested]; }; const MESSENGER_EXPOSED_METHODS = [ @@ -355,7 +338,11 @@ export class BridgeController extends StaticIntervalPollingController= quoteRequestCount) { return; } - this.#trackInputChangedEvents(paramsToUpdate, quoteRequestIndex); + this.#trackInputChangedEvents( + paramsToUpdate, + context.feature_id, + quoteRequestIndex, + ); this.resetState(AbortReason.QuoteRequestUpdated, quoteRequestIndex); this.update((state) => { // Update only the specified quote request and keep the rest of the quote requests unchanged @@ -400,14 +387,14 @@ export class BridgeController extends StaticIntervalPollingController => { const bridgeFeatureFlags = getBridgeFeatureFlags(this.messenger); const jwt = await this.#getJwt(); @@ -431,7 +418,7 @@ export class BridgeController extends StaticIntervalPollingController { + readonly #trackQuoteValidationFailures = ( + validationFailures: string[], + featureId: FeatureId, + ) => { if (validationFailures.length === 0) { return; } this.trackUnifiedSwapBridgeEvent( UnifiedSwapBridgeEventName.QuotesValidationFailed, { + feature_id: featureId, failures: validationFailures, location: this.#location, }, @@ -543,7 +534,7 @@ export class BridgeController extends StaticIntervalPollingController[], + quoteRequests: GenericQuoteRequest[], ) => { const exchangeRateSources = this.#getExchangeRateSources(); @@ -552,18 +543,14 @@ export class BridgeController extends StaticIntervalPollingController [ - quoteRequest.srcTokenAddress && quoteRequest.srcChainId - ? getAssetIdsForToken( - quoteRequest.srcTokenAddress, - quoteRequest.srcChainId, - ) - : undefined, - quoteRequest.destTokenAddress && quoteRequest.destChainId - ? getAssetIdsForToken( - quoteRequest.destTokenAddress, - quoteRequest.destChainId, - ) - : undefined, + getAssetIdsForToken( + quoteRequest.srcTokenAddress, + quoteRequest.srcChainId, + ), + getAssetIdsForToken( + quoteRequest.destTokenAddress, + quoteRequest.destChainId, + ), ].flat(), ) .filter( @@ -694,8 +681,8 @@ export class BridgeController extends StaticIntervalPollingController { this.stopAllPolling(); // If polling is stopped before quotes finish loading, track QuotesReceived @@ -724,8 +711,9 @@ export class BridgeController extends StaticIntervalPollingController { - this.stopPollingForQuotes(reason); + this.stopPollingForQuotes(context, reason); this.update((state) => { // Cannot do direct assignment to state, i.e. state = {... }, need to manually assign each field if (quoteRequestIndex === null) { @@ -854,6 +842,7 @@ export class BridgeController extends StaticIntervalPollingController { @@ -947,6 +937,7 @@ export class BridgeController extends StaticIntervalPollingController { @@ -965,11 +956,13 @@ export class BridgeController extends StaticIntervalPollingController + this.#trackQuoteValidationFailures(validationFailures, featureId), onValidQuoteReceived: async (quote: QuoteResponse) => { const feeAppendPromise = (async () => { const quotesWithFees = await appendFeesToQuotes( @@ -1269,6 +1262,7 @@ export class BridgeController extends StaticIntervalPollingController, + featureId: FeatureId, quoteRequestIndex: number = 0, ) => { Object.entries(paramsToUpdate).forEach(([key, value]) => { @@ -1292,6 +1286,7 @@ export class BridgeController extends StaticIntervalPollingController & { token_symbol_source: RequestParams['token_symbol_source']; token_symbol_destination: RequestParams['token_symbol_destination']; + token_security_type_destination: RequestParams['token_security_type_destination']; }; [UnifiedSwapBridgeEventName.QuotesReceived]: TradeData & { warnings: QuoteWarning[]; @@ -283,6 +291,7 @@ export type RequiredEventContextFromClient = { location?: MetaMetricsSwapsEventSource; ab_tests?: Record; active_ab_tests?: { key: string; value: string }[]; + feature_id: FeatureId; }; }; From 09ba94e921bca661fc380ae85259edf2b67b7066 Mon Sep 17 00:00:00 2001 From: micaelae Date: Wed, 3 Jun 2026 08:43:46 -0700 Subject: [PATCH 07/14] chore: populate feature_id and batch_id --- .../src/utils/metrics/types.ts | 83 +++--- .../bridge-status-controller.test.ts.snap | 151 ++++++---- ...ridge-status-controller.batch-sell.test.ts | 32 ++- .../bridge-status-controller.intent.test.ts | 1 + .../src/bridge-status-controller.test.ts | 257 +++++++++++++++++- .../src/bridge-status-controller.ts | 125 ++++----- .../bridge-status-controller/src/constants.ts | 8 + .../src/utils/bridge.ts | 6 +- .../src/utils/metrics.ts | 40 +-- .../test/mock-batch-sell-erc20-erc20.ts | 8 +- 10 files changed, 489 insertions(+), 222 deletions(-) diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index 3762935b39..57d3dab157 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import type { CaipAssetType, CaipChainId, Hex } from '@metamask/utils'; +import type { CaipAssetType, CaipChainId } from '@metamask/utils'; import type { FeatureId, SortOrder, StatusTypes } from '../../types'; import type { @@ -172,7 +172,7 @@ type RequiredEventContextFromClientBase = { | 'token_security_type_destination' > & { action_type: MetricsActionType; - batch_id: Hex; + batch_id?: string; }; [UnifiedSwapBridgeEventName.Completed]: TradeData & Pick & @@ -185,19 +185,17 @@ type RequiredEventContextFromClientBase = { quote_vs_execution_ratio: number; quoted_vs_used_gas_ratio: number; action_type: MetricsActionType; - batch_id: Hex; + batch_id?: string; }; - [UnifiedSwapBridgeEventName.Failed]: + [UnifiedSwapBridgeEventName.Failed]: ( | // Tx failed before confirmation - (TradeData & - Pick & - Pick< - RequestMetadata, - | 'stx_enabled' - | 'usd_amount_source' - | 'is_hardware_wallet' - | 'account_hardware_type' - > & + (Pick< + RequestMetadata, + | 'stx_enabled' + | 'usd_amount_source' + | 'is_hardware_wallet' + | 'account_hardware_type' + > & Pick< RequestParams, | 'token_symbol_source' @@ -205,16 +203,28 @@ type RequiredEventContextFromClientBase = { | 'token_address_source' | 'token_address_destination' | 'token_security_type_destination' - > & { error_message: string }) // Tx failed after confirmation + >) + // Tx failed after confirmation | (RequestParams & RequestMetadata & - Pick & - TxStatusData & - TradeData & { + TxStatusData & { actual_time_minutes: number; - error_message?: string; - batch_id: Hex; - }); + }) + ) & + TradeData & + Pick & { + error_message: string; + batch_id?: string; + }; + [UnifiedSwapBridgeEventName.PollingStatusUpdated]: { + polling_status: PollingStatus; + retry_attempts: number; + batch_id?: string; + }; + [UnifiedSwapBridgeEventName.StatusValidationFailed]: { + failures: string[]; + refresh_count: number; + }; // Emitted by clients [UnifiedSwapBridgeEventName.AllQuotesOpened]: Pick< TradeData, @@ -251,30 +261,10 @@ type RequiredEventContextFromClientBase = { }; [UnifiedSwapBridgeEventName.QuotesValidationFailed]: { failures: string[]; - // TODO optional batch_id - }; - [UnifiedSwapBridgeEventName.StatusValidationFailed]: { - failures: string[]; - refresh_count: number; }; [UnifiedSwapBridgeEventName.AssetPickerOpened]: { asset_location: 'source' | 'destination'; }; - [UnifiedSwapBridgeEventName.PollingStatusUpdated]: TradeData & - Pick & - Omit & - Pick< - RequestParams, - | 'token_symbol_source' - | 'token_symbol_destination' - | 'chain_id_source' - | 'chain_id_destination' - > & { - action_type: MetricsActionType; - polling_status: PollingStatus; - retry_attempts: number; - batch_id: Hex; - }; }; /** @@ -352,7 +342,18 @@ export type EventPropertiesFromControllerState = { }; [UnifiedSwapBridgeEventName.StatusValidationFailed]: RequestParams; [UnifiedSwapBridgeEventName.AssetPickerOpened]: null; - [UnifiedSwapBridgeEventName.PollingStatusUpdated]: null; + [UnifiedSwapBridgeEventName.PollingStatusUpdated]: TradeData & + Pick & + Omit & + Pick< + RequestParams, + | 'token_symbol_source' + | 'token_symbol_destination' + | 'chain_id_source' + | 'chain_id_destination' + > & { + batch_id?: string; + }; }; /** diff --git a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap index 9bd3c24076..ff811eea43 100644 --- a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap +++ b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap @@ -228,6 +228,7 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus emits bridgeTransa "chain_id_source": "eip155:42161", "custom_slippage": true, "destination_transaction": "FAILED", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -412,6 +413,7 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus stops polling when "chain_id_source": "eip155:42161", "custom_slippage": true, "destination_transaction": "COMPLETE", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -600,8 +602,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -616,6 +618,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "chain_id_destination": "eip155:10", "chain_id_source": "eip155:8453", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -914,8 +917,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -930,6 +933,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "chain_id_destination": "eip155:10", "chain_id_source": "eip155:59144", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -1265,10 +1269,10 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", { "best_quote_provider": "lifi_across", "can_submit": true, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "price_impact": 0, @@ -1281,6 +1285,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac "low_return", ], }, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -1295,6 +1300,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -1528,8 +1534,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -1544,6 +1550,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -1842,8 +1849,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -1858,6 +1865,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -2156,8 +2164,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -2172,6 +2180,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -2490,8 +2499,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -2506,6 +2515,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -2779,8 +2789,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -2795,6 +2805,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -2875,8 +2886,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -2891,6 +2902,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -2971,6 +2983,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "chain_id_source": "eip155:42161", "custom_slippage": false, "error_message": "Approval tx failed", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -2997,8 +3010,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -3013,6 +3026,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -3096,6 +3110,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "chain_id_source": "eip155:42161", "custom_slippage": false, "error_message": "Failed to submit cross-chain swap tx: txMeta for txHash was not found", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -3270,8 +3285,8 @@ exports[`BridgeStatusController submitTx: EVM bridge waits for approval tx confi [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -3286,6 +3301,7 @@ exports[`BridgeStatusController submitTx: EVM bridge waits for approval tx confi "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": true, @@ -3455,8 +3471,8 @@ exports[`BridgeStatusController submitTx: EVM swap should gracefully handle isAt [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -3471,6 +3487,7 @@ exports[`BridgeStatusController submitTx: EVM swap should gracefully handle isAt "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -3905,8 +3922,8 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -3921,6 +3938,7 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -4031,7 +4049,7 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an } `; -exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with featureId 1`] = ` +exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with featureId=perps 1`] = ` { "chainId": "0xa4b1", "hash": "0xevmTxHash", @@ -4050,12 +4068,12 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an } `; -exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with featureId 2`] = ` +exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with featureId=perps 2`] = ` [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -4311,8 +4329,8 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -4327,6 +4345,7 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -4767,8 +4786,8 @@ exports[`BridgeStatusController submitTx: Solana bridge should handle snap contr [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -4783,6 +4802,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should handle snap contr "chain_id_destination": "eip155:1", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -4830,6 +4850,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should handle snap contr "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, "error_message": "Snap error", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -4856,8 +4877,8 @@ exports[`BridgeStatusController submitTx: Solana bridge should successfully subm [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -4872,6 +4893,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should successfully subm "chain_id_destination": "eip155:1", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -5057,8 +5079,8 @@ exports[`BridgeStatusController submitTx: Solana bridge should throw error when [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -5073,6 +5095,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should throw error when "chain_id_destination": "eip155:1", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -5102,6 +5125,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should throw error when "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, "error_message": "Failed to submit cross-chain swap transaction: undefined snap id", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -5128,8 +5152,8 @@ exports[`BridgeStatusController submitTx: Solana swap should handle snap control [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -5144,6 +5168,7 @@ exports[`BridgeStatusController submitTx: Solana swap should handle snap control "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": true, @@ -5191,6 +5216,7 @@ exports[`BridgeStatusController submitTx: Solana swap should handle snap control "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, "error_message": "Snap error", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": true, @@ -5217,8 +5243,8 @@ exports[`BridgeStatusController submitTx: Solana swap should successfully submit [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -5233,6 +5259,7 @@ exports[`BridgeStatusController submitTx: Solana swap should successfully submit "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": true, @@ -5290,6 +5317,7 @@ exports[`BridgeStatusController submitTx: Solana swap should successfully submit "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": true, "destination_transaction": "PENDING", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": true, @@ -5456,8 +5484,8 @@ exports[`BridgeStatusController submitTx: Solana swap should throw error when ac [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -5470,8 +5498,8 @@ exports[`BridgeStatusController submitTx: Solana swap should throw error when sn [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -5486,6 +5514,7 @@ exports[`BridgeStatusController submitTx: Solana swap should throw error when sn "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -5515,6 +5544,7 @@ exports[`BridgeStatusController submitTx: Solana swap should throw error when sn "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, "error_message": "Failed to submit cross-chain swap transaction: undefined snap id", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -5541,8 +5571,8 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should handle [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -5557,6 +5587,7 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should handle "chain_id_destination": "tron:728126428", "chain_id_source": "tron:728126428", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -5608,6 +5639,7 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should handle "chain_id_source": "tron:728126428", "custom_slippage": false, "error_message": "Approval transaction failed", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -5634,8 +5666,8 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should success [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -5650,6 +5682,7 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should success "chain_id_destination": "eip155:1", "chain_id_source": "tron:728126428", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -5859,8 +5892,8 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should success [ [ "BridgeController:stopPollingForQuotes", - "Transaction submitted", undefined, + "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -5875,6 +5908,7 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should success "chain_id_destination": "tron:728126428", "chain_id_source": "tron:728126428", "custom_slippage": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -6089,6 +6123,9 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran [ "AuthenticationController:getBearerToken", ], + [ + "AuthenticationController:getBearerToken", + ], [ "BridgeController:trackUnifiedSwapBridgeEvent", "Unified SwapBridge Status Failed Validation", @@ -6099,6 +6136,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "failures": [ "across|status", ], + "feature_id": "perps", "location": "Main View", "refresh_count": 0, "token_address_destination": "eip155:10/slip44:60", @@ -6116,6 +6154,25 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "failures": [ "across|unknown", ], + "feature_id": "quick_buy_follow_trading", + "location": "Main View", + "refresh_count": 0, + "token_address_destination": "eip155:10/slip44:60", + "token_address_source": "eip155:42161/slip44:60", + "token_security_type_destination": null, + }, + ], + [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Status Failed Validation", + { + "action_type": "swapbridge-v1", + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:42161", + "failures": [ + "across|unknown", + ], + "feature_id": "unified_swap_bridge", "location": "Main View", "refresh_count": 0, "token_address_destination": "eip155:10/slip44:60", @@ -6139,50 +6196,34 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran ] `; -exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (confirmed) should start polling for completed bridge tx with featureId 2`] = ` +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (confirmed) should start polling for completed bridge tx with featureId=perps 2`] = ` { - "bridge": "across", "destChain": { - "amount": "990654755978611", "chainId": 10, - "token": { - "address": "0x0000000000000000000000000000000000000000", - "chainId": 10, - "coinKey": "ETH", - "decimals": 18, - "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "name": "ETH", - "priceUSD": "2478.63", - "symbol": "ETH", - }, - "txHash": "0xdestTxHash1", + "token": {}, }, - "isExpectedToken": true, "srcChain": { "amount": "991250000000000", "chainId": 42161, "token": { "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", "name": "ETH", - "priceUSD": "2478.7", + "priceUSD": "2518.47", "symbol": "ETH", }, "txHash": "0xperpsSrcTxHash1", }, - "status": "COMPLETE", + "status": "PENDING", } `; -exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (confirmed) should start polling for failed bridge tx with featureId 2`] = ` +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (confirmed) should start polling for failed bridge tx with featureId=perps 2`] = ` { - "bridge": "debridge", "destChain": { "chainId": 10, "token": {}, @@ -6192,18 +6233,18 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chainId": 42161, "token": { "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "iconUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", "name": "ETH", + "priceUSD": "2518.47", "symbol": "ETH", }, "txHash": "0xperpsSrcTxHash1", }, - "status": "FAILED", + "status": "PENDING", } `; @@ -6229,6 +6270,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chain_id_source": "eip155:42161", "custom_slippage": true, "destination_transaction": "PENDING", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -6273,6 +6315,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "custom_slippage": true, "destination_transaction": "FAILED", "error_message": "Transaction failed. tx-error", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -6317,11 +6360,13 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "actual_time_minutes": 833734.9086166667, "allowance_reset_transaction": undefined, "approval_transaction": undefined, + "batch_id": "0xBatchIdFailed1", "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": true, "destination_transaction": "FAILED", "error_message": "Transaction failed. tx-error", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -6373,6 +6418,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "custom_slippage": true, "destination_transaction": "FAILED", "error_message": "Transaction dropped. tx-error", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -6415,6 +6461,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chain_id_source": "eip155:42161", "custom_slippage": false, "error_message": "Transaction failed. tx-error", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -6466,6 +6513,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "custom_slippage": true, "destination_transaction": "FAILED", "error_message": "Transaction failed. tx-error", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -6510,6 +6558,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "custom_slippage": true, "destination_transaction": "FAILED", "error_message": "Transaction failed. approval-tx-error", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, diff --git a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts index 438fe9c74e..cb0e9b64f8 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts @@ -4,7 +4,10 @@ import type { BatchSellTradesResponse, Quote, } from '@metamask/bridge-controller'; -import { BatchSellTransactionType } from '@metamask/bridge-controller'; +import { + BatchSellTransactionType, + FeatureId, +} from '@metamask/bridge-controller'; import { toHex } from '@metamask/controller-utils'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { @@ -33,7 +36,7 @@ import type { import { getBatchSellHistoryItemsForTxHash } from './utils/history'; import { shouldDisable7702 } from './utils/transaction'; -const mockGenerateBatchId = jest.fn().mockReturnValue('0xBatchId1'); +const mockGenerateBatchId = jest.fn(); jest.mock('@metamask/transaction-controller', () => ({ ...jest.requireActual('@metamask/transaction-controller'), generateBatchId: (): string => mockGenerateBatchId(), @@ -203,7 +206,7 @@ describe('BridgeStatusController', () => { dateNowSpy.mockReturnValueOnce(1779922719705); dateNowSpy.mockReturnValueOnce(1779988819705); dateNowSpy.mockReturnValueOnce(1779988919705); - mockGenerateBatchId.mockReturnValueOnce('0xBatchId1'); + mockGenerateBatchId.mockReturnValueOnce('0xGeneratedBatchId1'); }); it.each([true, false])( @@ -298,8 +301,8 @@ describe('BridgeStatusController', () => { ['BridgeController:getState'], [ 'BridgeController:stopPollingForQuotes', - 'Transaction submitted', undefined, + 'Transaction submitted', ], [ 'AccountsController:getAccountByAddress', @@ -314,6 +317,7 @@ describe('BridgeStatusController', () => { chain_id_destination: 'eip155:10', chain_id_source: 'eip155:10', custom_slippage: false, + feature_id: FeatureId.BATCH_SELL, gas_included: gasIncluded, gas_included_7702: gasIncluded7702, is_hardware_wallet: false, @@ -333,7 +337,7 @@ describe('BridgeStatusController', () => { usd_amount_source: 100, usd_quoted_gas: 0, usd_quoted_return: 0, - batch_id: '0xBatchId1', + batch_id: '0xGeneratedBatchId1', }, ], [ @@ -367,7 +371,7 @@ describe('BridgeStatusController', () => { origin: 'metamask', requireApproval: false, skipInitialGasEstimate: false, - batchId: '0xBatchId1', + batchId: '0xGeneratedBatchId1', }); expect(transactions).toStrictEqual( @@ -406,6 +410,7 @@ describe('BridgeStatusController', () => { isStxEnabled: stxEnabled, batchSellData: mockBatchSellTrades, txMetaId: result.id, + featureId: FeatureId.BATCH_SELL, quote: { ...mockQuotes[0].quote, // Gas params should be merged to the initial quote @@ -429,7 +434,7 @@ describe('BridgeStatusController', () => { quoteObject: Quote, ): Partial => ({ batchId: undefined, - featureId: undefined, + featureId: FeatureId.BATCH_SELL, slippagePercentage: 0, txMetaId: undefined, actionId: undefined, @@ -533,6 +538,8 @@ describe('BridgeStatusController', () => { { account_hardware_type: null, action_type: 'swapbridge-v1', + batch_id: '0xBatchId1', + feature_id: FeatureId.BATCH_SELL, // actual_time_minutes: expect.closeTo(29644790, -1), actual_time_minutes: expect.any(Number), allowance_reset_transaction: undefined, @@ -615,11 +622,13 @@ describe('BridgeStatusController', () => { ), allowance_reset_transaction: undefined, approval_transaction: 'COMPLETE', + batch_id: '0xBatchId1', chain_id_destination: 'eip155:10', chain_id_source: 'eip155:10', custom_slippage: true, destination_transaction: 'FAILED', error_message: 'Transaction failed', + feature_id: FeatureId.BATCH_SELL, gas_included: gasIncluded, gas_included_7702: gasIncluded7702, is_hardware_wallet: false, @@ -722,7 +731,10 @@ describe('BridgeStatusController', () => { const result = await expect( rootMessenger.call('BridgeStatusController:submitBatchSell', { accountAddress: (mockQuotes[0].trade as TxData).from, - quoteResponses: mockQuotes, + quoteResponses: mockQuotes.map((quote) => ({ + ...quote, + featureId: FeatureId.BATCH_SELL, + })), isStxEnabled: stxEnabled, }), ).rejects.toThrow( @@ -738,8 +750,8 @@ describe('BridgeStatusController', () => { ['BridgeController:getState'], [ 'BridgeController:stopPollingForQuotes', - 'Transaction submitted', undefined, + 'Transaction submitted', ], [ 'AccountsController:getAccountByAddress', @@ -754,6 +766,7 @@ describe('BridgeStatusController', () => { chain_id_destination: 'eip155:10', chain_id_source: 'eip155:10', custom_slippage: false, + feature_id: FeatureId.BATCH_SELL, gas_included: gasIncluded, gas_included_7702: gasIncluded7702, is_hardware_wallet: false, @@ -796,6 +809,7 @@ describe('BridgeStatusController', () => { custom_slippage: false, error_message: 'Failed to add BatchSell trade to history: txMeta not found', + feature_id: FeatureId.BATCH_SELL, gas_included: false, gas_included_7702: true, is_hardware_wallet: false, diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts index e72961514a..55288c4b65 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts @@ -1055,6 +1055,7 @@ describe('BridgeStatusController (target uncovered branches)', () => { "failures": [ "across|status", ], + "feature_id": "unified_swap_bridge", "location": "Main View", "refresh_count": 3, "token_address_destination": "eip155:10/slip44:60", 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 9977f5d44a..695f23c0e7 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -1124,6 +1124,7 @@ describe('BridgeStatusController constructor', () => { chain_id_source: expect.any(String), custom_slippage: true, destination_transaction: 'PENDING', + feature_id: 'unified_swap_bridge', gas_included: false, gas_included_7702: false, is_hardware_wallet: false, @@ -3899,7 +3900,7 @@ describe('BridgeStatusController', () => { ); }); - it('should successfully submit an EVM swap transaction with featureId', async () => { + it('should successfully submit an EVM swap transaction with featureId=perps', async () => { mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); mockMessengerCall.mockReturnValueOnce([]); // isAtomicBatchSupported setupApprovalMocks(); @@ -4571,6 +4572,7 @@ describe('BridgeStatusController', () => { "chain_id_source": "eip155:42161", "custom_slippage": false, "error_message": "Failed to submit cross-chain swap batch transaction: unknown account in trade data", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -4658,6 +4660,7 @@ describe('BridgeStatusController', () => { "chain_id_source": "eip155:42161", "custom_slippage": false, "error_message": "Failed to update cross-chain swap transaction batch: tradeMeta not found", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -5140,6 +5143,11 @@ describe('BridgeStatusController', () => { srcTxHash: '0xperpsSrcTxHash1', featureId: FeatureId.PERPS as never, }), + ...MockTxHistory.getPending({ + txMetaId: 'quickBuyBridgeTxMetaId1', + srcTxHash: '0xperpsSrcTxHash1', + featureId: FeatureId.QUICK_BUY_FOLLOW_TRADING as never, + }), // ActionId-keyed entries for pre-submission failure tests 'pre-submission-action-id': { ...baseHistoryItem, @@ -5182,6 +5190,7 @@ describe('BridgeStatusController', () => { type: TransactionType.bridge, status: TransactionStatus.failed, id: 'bridgeTxMetaId1', + batchId: '0xBatchIdFailed1', }, }, ); @@ -5239,6 +5248,7 @@ describe('BridgeStatusController', () => { "chain_id_source": "eip155:42161", "custom_slippage": false, "error_message": "Transaction failed. tx-error", + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -5308,6 +5318,7 @@ describe('BridgeStatusController', () => { active_ab_tests: [ { key: 'bridge_quote_sorting', value: 'variant_b' }, ], + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }), ); }); @@ -5353,7 +5364,7 @@ describe('BridgeStatusController', () => { ).toBe(StatusTypes.FAILED); }); - it('should not track failed event for bridge transaction with featureId', () => { + it('should not track failed event for bridge transaction with featureId=perps', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); mockMessenger.publish( 'TransactionController:transactionStatusUpdated', @@ -5375,7 +5386,85 @@ describe('BridgeStatusController', () => { bridgeStatusController.state.txHistory.perpsBridgeTxMetaId1.status .status, ).toBe(StatusTypes.FAILED); - expect(messengerCallSpy).not.toHaveBeenCalled(); + expect(messengerCallSpy.mock.calls).toMatchInlineSnapshot(`[]`); + }); + + it('should track failed event for transaction with featureId=quick_buy_follow_trading', () => { + const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.failed, + id: 'quickBuyBridgeTxMetaId1', + batchId: '0xBatchId3', + }, + }, + ); + + expect( + bridgeStatusController.state.txHistory.quickBuyBridgeTxMetaId1.status + .status, + ).toBe(StatusTypes.FAILED); + expect(messengerCallSpy.mock.calls).toMatchInlineSnapshot(` + [ + [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + [ + "TransactionController:getState", + ], + [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Failed", + { + "account_hardware_type": null, + "action_type": "swapbridge-v1", + "actual_time_minutes": 833734.9086166667, + "allowance_reset_transaction": undefined, + "approval_transaction": undefined, + "batch_id": "0xBatchId3", + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:42161", + "custom_slippage": true, + "destination_transaction": "FAILED", + "error_message": "Transaction failed. tx-error", + "feature_id": "quick_buy_follow_trading", + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "location": "Main View", + "price_impact": 0, + "provider": "lifi_across", + "quote_vs_execution_ratio": 0, + "quoted_time_minutes": 0.25, + "quoted_vs_used_gas_ratio": 0, + "security_warnings": [], + "slippage_limit": 0, + "source_transaction": "COMPLETE", + "stx_enabled": false, + "swap_type": "crosschain", + "token_address_destination": "eip155:10/slip44:60", + "token_address_source": "eip155:42161/slip44:60", + "token_security_type_destination": null, + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", + "usd_actual_gas": 0, + "usd_actual_return": 0, + "usd_amount_source": 0, + "usd_quoted_gas": 2.5778, + "usd_quoted_return": 0, + }, + ], + ] + `); }); it('should track failed event for swap transaction if approval fails', () => { @@ -5725,7 +5814,7 @@ describe('BridgeStatusController', () => { expect(consoleFnSpy.mock.calls).toMatchSnapshot(); }); - it('should start polling for completed bridge tx with featureId', async () => { + it('should start polling for completed bridge tx with featureId=perps', async () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); mockFetchFn.mockClear(); @@ -5759,6 +5848,74 @@ describe('BridgeStatusController', () => { [ "AuthenticationController:getBearerToken", ], + [ + "AuthenticationController:getBearerToken", + ], + [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + [ + "TransactionController:getState", + ], + [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Completed", + { + "account_hardware_type": null, + "action_type": "swapbridge-v1", + "actual_time_minutes": 833734.9086166667, + "allowance_reset_transaction": undefined, + "approval_transaction": undefined, + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:42161", + "custom_slippage": true, + "destination_transaction": "COMPLETE", + "feature_id": "quick_buy_follow_trading", + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "location": "Main View", + "price_impact": 0, + "provider": "lifi_across", + "quote_vs_execution_ratio": 0, + "quoted_time_minutes": 0.25, + "quoted_vs_used_gas_ratio": 0, + "security_warnings": [], + "slippage_limit": 0, + "source_transaction": "COMPLETE", + "stx_enabled": false, + "swap_type": "crosschain", + "token_address_destination": "eip155:10/slip44:60", + "token_address_source": "eip155:42161/slip44:60", + "token_security_type_destination": null, + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", + "usd_actual_gas": 0, + "usd_actual_return": 0, + "usd_amount_source": 0, + "usd_quoted_gas": 2.5778, + "usd_quoted_return": 0, + }, + ], + [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Status Failed Validation", + { + "action_type": "swapbridge-v1", + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:42161", + "failures": [ + "across|unknown", + ], + "feature_id": "perps", + "location": "Main View", + "refresh_count": 0, + "token_address_destination": "eip155:10/slip44:60", + "token_address_source": "eip155:42161/slip44:60", + "token_security_type_destination": null, + }, + ], ] `); expect(mockFetchFn).toHaveBeenCalledWith( @@ -5776,7 +5933,7 @@ describe('BridgeStatusController', () => { expect(consoleFnSpy).not.toHaveBeenCalled(); }); - it('should start polling for failed bridge tx with featureId', async () => { + it('should start polling for failed bridge tx with featureId=perps', async () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); mockFetchFn.mockClear(); @@ -5810,6 +5967,74 @@ describe('BridgeStatusController', () => { [ "AuthenticationController:getBearerToken", ], + [ + "AuthenticationController:getBearerToken", + ], + [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + [ + "TransactionController:getState", + ], + [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Failed", + { + "account_hardware_type": null, + "action_type": "swapbridge-v1", + "actual_time_minutes": 833734.9086166667, + "allowance_reset_transaction": undefined, + "approval_transaction": undefined, + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:42161", + "custom_slippage": true, + "destination_transaction": "FAILED", + "feature_id": "quick_buy_follow_trading", + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "location": "Main View", + "price_impact": 0, + "provider": "lifi_across", + "quote_vs_execution_ratio": 0, + "quoted_time_minutes": 0.25, + "quoted_vs_used_gas_ratio": 0, + "security_warnings": [], + "slippage_limit": 0, + "source_transaction": "COMPLETE", + "stx_enabled": false, + "swap_type": "crosschain", + "token_address_destination": "eip155:10/slip44:60", + "token_address_source": "eip155:42161/slip44:60", + "token_security_type_destination": null, + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", + "usd_actual_gas": 0, + "usd_actual_return": 0, + "usd_amount_source": 0, + "usd_quoted_gas": 2.5778, + "usd_quoted_return": 0, + }, + ], + [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Status Failed Validation", + { + "action_type": "swapbridge-v1", + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:42161", + "failures": [ + "across|unknown", + ], + "feature_id": "perps", + "location": "Main View", + "refresh_count": 0, + "token_address_destination": "eip155:10/slip44:60", + "token_address_source": "eip155:42161/slip44:60", + "token_security_type_destination": null, + }, + ], ] `); expect(mockFetchFn).toHaveBeenCalledWith( @@ -5847,7 +6072,7 @@ describe('BridgeStatusController', () => { expect(messengerCallSpy.mock.calls).toMatchSnapshot(); }); - it('should not track completed event for swap transaction with featureId', () => { + it('should not track completed event for swap transaction with perps featureId', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); mockMessenger.publish( 'TransactionController:transactionStatusUpdated', @@ -6002,12 +6227,30 @@ describe('BridgeStatusController', () => { [ "AuthenticationController:getBearerToken", ], + [ + "AuthenticationController:getBearerToken", + ], [ "AccountsController:getAccountByAddress", "0xaccount1", ], [ - "TransactionController:getState", + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Status Failed Validation", + { + "action_type": "swapbridge-v1", + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:42161", + "failures": [ + "across|unknown", + ], + "feature_id": "perps", + "location": "Main View", + "refresh_count": 0, + "token_address_destination": "eip155:10/slip44:60", + "token_address_source": "eip155:42161/slip44:60", + "token_security_type_destination": null, + }, ], ] `); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 225f6b2eeb..368e114e14 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -32,6 +32,7 @@ import type { Hex } from '@metamask/utils'; import { IntentManager } from './bridge-status-controller.intent'; import { + ALLOWED_FEATURE_IDS_FOR_STATUS_EVENTS, BRIDGE_PROD_API_BASE_URL, BRIDGE_STATUS_CONTROLLER_NAME, DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, @@ -433,37 +434,14 @@ export class BridgeStatusController extends StaticIntervalPollingController, + // eslint-disable-next-line @typescript-eslint/naming-convention + FeatureIdType extends { feature_id?: FeatureId } = { + // eslint-disable-next-line @typescript-eslint/naming-convention + feature_id?: FeatureId; + }, >( eventName: EventName, - txMetaId?: string, - eventProperties?: Pick< - RequiredEventContextFromClient, - EventName - >[EventName], - featureIdOverride?: FeatureId, + txHistoryKey?: string, + eventProperties?: EventProperties & FeatureIdType, ): void => { - const historyItem: BridgeHistoryItem | undefined = txMetaId - ? this.state.txHistory[txMetaId] + const historyItem: BridgeHistoryItem | undefined = txHistoryKey + ? this.state.txHistory[txHistoryKey] : undefined; - const featureId = featureIdOverride ?? historyItem?.featureId; - - const shouldSkipMetrics = - // Skip tracking all other events when featureId is set (i.e. PERPS) - featureId && - // Always publish StatusValidationFailed event, regardless of featureId - eventName !== UnifiedSwapBridgeEventName.StatusValidationFailed; - if (shouldSkipMetrics) { + + const featureId = + eventProperties?.feature_id ?? + historyItem?.featureId ?? + FeatureId.UNIFIED_SWAP_BRIDGE; + + if ( + !( + featureId === FeatureId.UNIFIED_SWAP_BRIDGE || + ALLOWED_FEATURE_IDS_FOR_STATUS_EVENTS.includes(featureId) || + eventName === UnifiedSwapBridgeEventName.StatusValidationFailed + ) + ) { return; } // Legacy/new metrics fields are intentionally kept independent during migration. - const historyAbTests = txMetaId - ? this.state.txHistory?.[txMetaId]?.abTests + const historyAbTests = txHistoryKey + ? this.state.txHistory?.[txHistoryKey]?.abTests : undefined; - const historyActiveAbTests = txMetaId - ? this.state.txHistory?.[txMetaId]?.activeAbTests + const historyActiveAbTests = txHistoryKey + ? this.state.txHistory?.[txHistoryKey]?.activeAbTests : undefined; const resolvedAbTests = eventProperties?.ab_tests ?? historyAbTests; const resolvedActiveAbTests = eventProperties?.active_ab_tests ?? historyActiveAbTests; const location = - (txMetaId ? this.state.txHistory?.[txMetaId]?.location : undefined) ?? - MetaMetricsSwapsEventSource.MainView; + (txHistoryKey + ? this.state.txHistory?.[txHistoryKey]?.location + : undefined) ?? MetaMetricsSwapsEventSource.MainView; const baseProperties = { action_type: MetricsActionType.SWAPBRIDGE_V1, + feature_id: featureId ?? FeatureId.UNIFIED_SWAP_BRIDGE, + ...(historyItem?.batchId ? { batch_id: historyItem.batchId } : {}), ...(eventProperties ?? {}), location, ...(resolvedAbTests && @@ -1393,7 +1374,7 @@ export class BridgeStatusController extends StaticIntervalPollingController tx.id === txMetaId, + (tx: TransactionMeta) => tx.id === txHistoryKey, ); const approvalTxMeta = transactions.find( (tx: TransactionMeta) => tx.id === approvalTxId, diff --git a/packages/bridge-status-controller/src/constants.ts b/packages/bridge-status-controller/src/constants.ts index 7c98fe3339..08109649dd 100644 --- a/packages/bridge-status-controller/src/constants.ts +++ b/packages/bridge-status-controller/src/constants.ts @@ -1,4 +1,5 @@ import type { BridgeStatusControllerState } from './types'; +import { FeatureId } from '@metamask/bridge-controller'; export const REFRESH_INTERVAL_MS = 10 * 1000; // 10 seconds export const MAX_ATTEMPTS = 7; // at 7 attempts, delay is 10:40, cumulative time is 21:10 @@ -21,3 +22,10 @@ export enum TraceName { SwapTransactionApprovalCompleted = 'Swap Transaction Approval Completed', SwapTransactionCompleted = 'Swap Transaction Completed', } + +export const ALLOWED_FEATURE_IDS_FOR_STATUS_EVENTS = [ + FeatureId.QUICK_BUY_FOLLOW_TRADING, + FeatureId.QUICK_BUY_TOKEN_DETAILS, + FeatureId.UNIFIED_SWAP_BRIDGE, + FeatureId.BATCH_SELL, +]; diff --git a/packages/bridge-status-controller/src/utils/bridge.ts b/packages/bridge-status-controller/src/utils/bridge.ts index 5a541bed20..8ce15e6b21 100644 --- a/packages/bridge-status-controller/src/utils/bridge.ts +++ b/packages/bridge-status-controller/src/utils/bridge.ts @@ -1,6 +1,5 @@ import { AbortReason, - FeatureId, UnifiedSwapBridgeEventName, BatchSellTradesResponse, RequiredEventContextFromClient, @@ -10,15 +9,12 @@ import { BridgeStatusControllerMessenger } from '../types'; export const stopPollingForQuotes = ( messenger: BridgeStatusControllerMessenger, - featureId?: FeatureId, metricsContext?: RequiredEventContextFromClient[UnifiedSwapBridgeEventName.QuotesReceived], ): void => { messenger.call( 'BridgeController:stopPollingForQuotes', + metricsContext, AbortReason.TransactionSubmitted, - // If trade is submitted before all quotes are loaded, the QuotesReceived event is published - // If the trade has a featureId, it means it was submitted outside of the Unified Swap and Bridge experience, so no QuotesReceived event is published - featureId ? undefined : metricsContext, ); }; diff --git a/packages/bridge-status-controller/src/utils/metrics.ts b/packages/bridge-status-controller/src/utils/metrics.ts index 73011ebde0..99510c4aa5 100644 --- a/packages/bridge-status-controller/src/utils/metrics.ts +++ b/packages/bridge-status-controller/src/utils/metrics.ts @@ -15,6 +15,7 @@ import { MetricsActionType, MetricsSwapType, MetaMetricsSwapsEventSource, + FeatureId, } from '@metamask/bridge-controller'; import type { AccountHardwareType, @@ -25,7 +26,6 @@ import type { RequestParams, TradeData, RequestMetadata, - PollingStatus, BatchSellTradesResponse, } from '@metamask/bridge-controller'; import { @@ -36,11 +36,7 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import type { CaipAssetType, Hex } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; -import type { - BridgeHistoryItem, - BridgeStatusControllerMessenger, -} from '../types'; -import { getAccountByAddress } from './accounts'; +import type { BridgeHistoryItem } from '../types'; import { calcActualGasUsed } from './gas'; import { getActualBridgeReceivedAmount, @@ -239,7 +235,8 @@ export const getPreConfirmationPropertiesFromQuote = ( activeAbTests.length > 0 && { active_ab_tests: activeAbTests, }), - ...(batchId && { batch_id: batchId }), + ...(batchId ? { batch_id: batchId } : {}), + feature_id: quoteResponse.featureId ?? FeatureId.UNIFIED_SWAP_BRIDGE, }; }; @@ -345,33 +342,6 @@ export const getEVMTxPropertiesFromTransactionMeta = ( usd_actual_return: 0, usd_actual_gas: 0, action_type: MetricsActionType.SWAPBRIDGE_V1, - }; -}; - -export const getPollingStatusUpdatedProperties = ( - messenger: BridgeStatusControllerMessenger, - pollingStatus: PollingStatus, - historyItem: BridgeHistoryItem, -) => { - const selectedAccount = getAccountByAddress(messenger, historyItem.account); - const requestParams = getRequestParamFromHistory(historyItem); - const requestMetadata = getRequestMetadataFromHistory( - historyItem, - selectedAccount, - ); - const { security_warnings: _, ...metadataWithoutWarnings } = requestMetadata; - - return { - ...getTradeDataFromHistory(historyItem), - ...getPriceImpactFromQuote(historyItem.quote), - ...metadataWithoutWarnings, - chain_id_source: requestParams.chain_id_source, - chain_id_destination: requestParams.chain_id_destination, - token_symbol_source: requestParams.token_symbol_source, - token_symbol_destination: requestParams.token_symbol_destination, - action_type: MetricsActionType.SWAPBRIDGE_V1, - polling_status: pollingStatus, - retry_attempts: historyItem.attempts?.counter ?? 0, - ...(historyItem.batchId ? { batch_id: historyItem.batchId } : {}), + ...(transactionMeta.batchId ? { batch_id: transactionMeta.batchId } : {}), }; }; diff --git a/packages/bridge-status-controller/test/mock-batch-sell-erc20-erc20.ts b/packages/bridge-status-controller/test/mock-batch-sell-erc20-erc20.ts index 5879795485..2bc1d68ada 100644 --- a/packages/bridge-status-controller/test/mock-batch-sell-erc20-erc20.ts +++ b/packages/bridge-status-controller/test/mock-batch-sell-erc20-erc20.ts @@ -7,6 +7,7 @@ import { QuoteResponse, StatusTypes, TxData, + FeatureId, } from '@metamask/bridge-controller'; import { toHex } from '@metamask/controller-utils'; import { @@ -19,6 +20,7 @@ import { BridgeHistoryItem } from '../src'; export const mockBatchSellErc20Erc20: QuoteResponse[] = [ { + featureId: FeatureId.BATCH_SELL, quote: { requestId: '90ae8e69-f03a-4cf6-bab7-ed4e3431eb37', srcChainId: 10, @@ -82,6 +84,7 @@ export const mockBatchSellErc20Erc20: QuoteResponse[] = [ estimatedProcessingTimeInSeconds: 60, }, { + featureId: FeatureId.BATCH_SELL, quote: { requestId: '0b6caac9-456d-47e6-8982-1945ae81ae82', srcChainId: 10, @@ -263,13 +266,14 @@ export const getTxMetasForBatch = ({ export const getHistoryItem = ( params: Partial, ): BridgeHistoryItem => { - const { isStxEnabled, batchSellData, txMetaId, quote, quoteIds } = params; + const { isStxEnabled, batchSellData, txMetaId, quote, quoteIds, featureId } = + params; return { account: '0xaccount1', actionId: undefined, batchId: '0xBatchId1', - featureId: undefined, + featureId, hasApprovalTx: true, isStxEnabled, initialDestAssetBalance: undefined, From 326b079e9898a3e2d5a74ecbe0dc334dfde1d526 Mon Sep 17 00:00:00 2001 From: micaelae Date: Wed, 3 Jun 2026 10:31:38 -0700 Subject: [PATCH 08/14] refactor: comments and move batch-sell-strategy tradeMeta step --- .../src/strategy/batch-sell-strategy.ts | 14 +++---- .../src/strategy/types.ts | 37 ++++++++++++++++++- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/bridge-status-controller/src/strategy/batch-sell-strategy.ts b/packages/bridge-status-controller/src/strategy/batch-sell-strategy.ts index 48df217676..c05fc09354 100644 --- a/packages/bridge-status-controller/src/strategy/batch-sell-strategy.ts +++ b/packages/bridge-status-controller/src/strategy/batch-sell-strategy.ts @@ -96,6 +96,13 @@ export async function* submitBatchSellHandler( ); } + yield { + type: SubmitStep.SetTradeMeta, + payload: { + tradeMeta: firstTradeMeta, + }, + }; + // Nested/7702 batch if (is7702Tx(firstTradeMeta) || hasNestedSwapTransactions(firstTradeMeta)) { const quoteIds = Array.from( @@ -142,11 +149,4 @@ export async function* submitBatchSellHandler( }; } } - - yield { - type: SubmitStep.SetTradeMeta, - payload: { - tradeMeta: firstTradeMeta, - }, - }; } diff --git a/packages/bridge-status-controller/src/strategy/types.ts b/packages/bridge-status-controller/src/strategy/types.ts index 361a5862e9..8feeab5465 100644 --- a/packages/bridge-status-controller/src/strategy/types.ts +++ b/packages/bridge-status-controller/src/strategy/types.ts @@ -22,11 +22,31 @@ import type { import type { Hex } from '@metamask/utils'; export enum SubmitStep { + /** + * Adds quote and submission data to BridgeStatusController's `txHistory` + */ AddHistoryItem = 'addHistoryItem', + /** + * Rekeys the history item keyed by the old history key to the new history key, + * and merges in the tradeMeta's id and hash + */ RekeyHistoryItem = 'rekeyHistoryItem', + /** + * Triggers polling for the transaction's status + */ StartPolling = 'startPolling', + /** + * Publishes the Unified SwapBridge Completed metrics event + */ PublishCompletedEvent = 'publishCompletedEvent', + /** + * Sets the tradeMeta returned to the client after submission + */ SetTradeMeta = 'setTradeMeta', + /** + * Updates the transaction type of batch transactions to swap/bridge/swapApproval/bridgeApproval + * for display purposes. + */ UpdateBatchTransactions = 'updateBatchTransactions', } @@ -95,13 +115,26 @@ export type SubmitStrategyParams< | undefined | null = BatchSellTradesResponse | undefined | null, > = { + /** + * The response from obtainGaslessBatch API containing submittable transactions and their fees + */ batchSellTrades: BatchSellTradesResponseType; + /** + * The function to add a transaction batch to the {@link TransactionControllers} + */ addTransactionBatchFn: TransactionController['addTransactionBatch']; isBridgeTx: boolean; isDelegatedAccount: boolean; + /** + * Whether the STX is enabled in the wallet. Does not necessarily mean that + * STX will be used to submit the transaction. + */ isStxEnabled: boolean; messenger: BridgeStatusControllerMessenger; quoteResponses: (QuoteResponse & QuoteMetadata)[]; + /** + * Set to true so hardware wallets get prompted for approval on mobile + */ requireApproval: boolean; selectedAccount: AccountsControllerState['internalAccounts']['accounts'][string]; traceFn: TraceCallback; @@ -110,8 +143,8 @@ export type SubmitStrategyParams< clientId: BridgeClientId; bridgeApiBaseUrl: string; /** - * The batch ID of the transaction batch. - * This is only used for batched transactions. + * The batch ID of the transaction batch passed to the addTransactionBatchFn + * This is only used for batch-sell transactions. */ batchId?: Hex; }; From 4ddac534e45d7f37851776166b1bfe49c01d5ad7 Mon Sep 17 00:00:00 2001 From: micaelae Date: Wed, 3 Jun 2026 10:36:33 -0700 Subject: [PATCH 09/14] fix: update transaction-pay fetchQuotes params --- .../src/strategy/bridge/bridge-quotes.test.ts | 45 +++++++------------ .../src/strategy/bridge/bridge-quotes.ts | 3 +- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts index e69a73072a..c4ae5dabcf 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts @@ -184,8 +184,7 @@ describe('Bridge Quotes Utils', () => { slippage: 0.5, insufficientBal: false, }), - undefined, - undefined, + FeatureId.PERPS, ); expect(fetchQuotesMock).toHaveBeenCalledWith( @@ -199,8 +198,7 @@ describe('Bridge Quotes Utils', () => { slippage: 0.5, insufficientBal: false, }), - undefined, - undefined, + FeatureId.PERPS, ); }); @@ -364,16 +362,14 @@ describe('Bridge Quotes Utils', () => { expect.objectContaining({ srcTokenAmount: '1000000000000000000', }), - undefined, - undefined, + FeatureId.PERPS, ); expect(fetchQuotesMock).toHaveBeenCalledWith( expect.objectContaining({ srcTokenAmount: '1500000000000000000', }), - undefined, - undefined, + FeatureId.PERPS, ); }); @@ -528,16 +524,14 @@ describe('Bridge Quotes Utils', () => { expect.objectContaining({ srcTokenAmount: '1000000000000000000', }), - undefined, - undefined, + FeatureId.PERPS, ); expect(fetchQuotesMock).toHaveBeenCalledWith( expect.objectContaining({ srcTokenAmount: '1400000000000000000', }), - undefined, - undefined, + FeatureId.PERPS, ); }); @@ -594,8 +588,7 @@ describe('Bridge Quotes Utils', () => { expect.objectContaining({ srcTokenAmount: '1000000000000000000', }), - undefined, - undefined, + FeatureId.PERPS, ); expect(fetchQuotesMock).toHaveBeenNthCalledWith( @@ -603,8 +596,7 @@ describe('Bridge Quotes Utils', () => { expect.objectContaining({ srcTokenAmount: '1000000000000000000', }), - undefined, - undefined, + FeatureId.PERPS, ); }); @@ -675,8 +667,7 @@ describe('Bridge Quotes Utils', () => { srcTokenAmount: '1000000000000000000', destTokenAddress: QUOTE_REQUEST_1_MOCK.targetTokenAddress, }), - undefined, - undefined, + FeatureId.PERPS, ); expect(fetchQuotesMock).toHaveBeenNthCalledWith( @@ -685,8 +676,7 @@ describe('Bridge Quotes Utils', () => { srcTokenAmount: '1000000000000000000', destTokenAddress: QUOTE_REQUEST_2_MOCK.targetTokenAddress, }), - undefined, - undefined, + FeatureId.PERPS, ); expect(fetchQuotesMock).toHaveBeenNthCalledWith( @@ -695,8 +685,7 @@ describe('Bridge Quotes Utils', () => { srcTokenAmount: '1400000000000000000', destTokenAddress: QUOTE_REQUEST_1_MOCK.targetTokenAddress, }), - undefined, - undefined, + FeatureId.PERPS, ); }); @@ -873,8 +862,7 @@ describe('Bridge Quotes Utils', () => { expect.objectContaining({ slippage: 0.5, }), - undefined, - undefined, + FeatureId.PERPS, ); expect(quotes.map((quote) => quote.original)).toStrictEqual([ @@ -894,7 +882,6 @@ describe('Bridge Quotes Utils', () => { expect(fetchQuotesMock).toHaveBeenCalledWith( expect.anything(), - undefined, FeatureId.PERPS, ); }); @@ -1008,7 +995,10 @@ describe('Bridge Quotes Utils', () => { const newQuote = await refreshQuote( { - original: { ...QUOTE_2_MOCK, request: QUOTE_REQUEST_2_MOCK }, + original: { + ...QUOTE_2_MOCK, + request: QUOTE_REQUEST_2_MOCK, + }, } as TransactionPayQuote, messenger, TRANSACTION_META_MOCK, @@ -1024,8 +1014,7 @@ describe('Bridge Quotes Utils', () => { destTokenAddress: QUOTE_REQUEST_2_MOCK.targetTokenAddress, insufficientBal: false, }), - undefined, - undefined, + FeatureId.PERPS, ); expect(newQuote).toMatchObject(QUOTE_2_MOCK); diff --git a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts index 1846de2146..32371c00d8 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts @@ -334,8 +334,7 @@ async function getSingleBridgeQuote( const quotes = await messenger.call( 'BridgeController:fetchQuotes', bridgeRequest, - undefined, - featureId, + featureId ?? FeatureId.PERPS, ); if (!quotes.length) { From 51dc34ef31fdaa4c090fda3882ec6adba86a2810 Mon Sep 17 00:00:00 2001 From: micaelae Date: Wed, 3 Jun 2026 11:12:48 -0700 Subject: [PATCH 10/14] fix: lint and tests --- .../src/bridge-controller.sse.test.ts | 4 +- .../src/utils/metrics/properties.ts | 2 +- .../src/utils/metrics/types.ts | 2 +- .../bridge-status-controller.test.ts.snap | 49 +++-- .../src/bridge-status-controller.test.ts | 168 +----------------- .../src/bridge-status-controller.ts | 2 - .../bridge-status-controller/src/constants.ts | 3 +- .../src/strategy/types.ts | 2 +- 8 files changed, 36 insertions(+), 196 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index b6f0058d0d..636d98c147 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -1474,7 +1474,7 @@ describe('BridgeController SSE', function () { expect(bridgeController.state.tokenWarnings).toStrictEqual([mockWarning]); - bridgeController.resetState(FeatureId.UNIFIED_SWAP_BRIDGE); + bridgeController.resetState(); expect(bridgeController.state.tokenWarnings).toStrictEqual([]); }); }); @@ -1672,7 +1672,7 @@ describe('BridgeController SSE', function () { mockComplete, ); - bridgeController.resetState(FeatureId.UNIFIED_SWAP_BRIDGE); + bridgeController.resetState(); expect(bridgeController.state.quoteStreamComplete).toBeNull(); }); }); diff --git a/packages/bridge-controller/src/utils/metrics/properties.ts b/packages/bridge-controller/src/utils/metrics/properties.ts index ce431dfcd8..b033fe4b46 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.ts @@ -9,6 +9,7 @@ import type { QuoteResponse, TxData, } from '../../types'; +import { FeatureId } from '../../types'; import { getNativeAssetForChainId, isCrossChain } from '../bridge'; import { formatAddressToAssetId, @@ -22,7 +23,6 @@ import type { QuoteWarning, RequestParams, } from './types'; -import { FeatureId } from '../../types'; export const toInputChangedPropertyKey: Partial< Record diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index 57d3dab157..d84f630f36 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -189,7 +189,7 @@ type RequiredEventContextFromClientBase = { }; [UnifiedSwapBridgeEventName.Failed]: ( | // Tx failed before confirmation - (Pick< + (Pick< RequestMetadata, | 'stx_enabled' | 'usd_amount_source' diff --git a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap index 6ecddb2a73..449724b59d 100644 --- a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap +++ b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap @@ -6172,9 +6172,6 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran [ "AuthenticationController:getBearerToken", ], - [ - "AuthenticationController:getBearerToken", - ], [ "BridgeController:trackUnifiedSwapBridgeEvent", "Unified SwapBridge Status Failed Validation", @@ -6193,24 +6190,6 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "token_security_type_destination": null, }, ], - [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Status Failed Validation", - { - "action_type": "swapbridge-v1", - "chain_id_destination": "eip155:10", - "chain_id_source": "eip155:42161", - "failures": [ - "across|unknown", - ], - "feature_id": "quick_buy_follow_trading", - "location": "Main View", - "refresh_count": 0, - "token_address_destination": "eip155:10/slip44:60", - "token_address_source": "eip155:42161/slip44:60", - "token_security_type_destination": null, - }, - ], [ "BridgeController:trackUnifiedSwapBridgeEvent", "Unified SwapBridge Status Failed Validation", @@ -6247,32 +6226,48 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (confirmed) should start polling for completed bridge tx with featureId=perps 2`] = ` { + "bridge": "across", "destChain": { + "amount": "990654755978611", "chainId": 10, - "token": {}, + "token": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 10, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.63", + "symbol": "ETH", + }, + "txHash": "0xdestTxHash1", }, + "isExpectedToken": true, "srcChain": { "amount": "991250000000000", "chainId": 42161, "token": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", "name": "ETH", - "priceUSD": "2518.47", + "priceUSD": "2478.7", "symbol": "ETH", }, "txHash": "0xperpsSrcTxHash1", }, - "status": "PENDING", + "status": "COMPLETE", } `; exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (confirmed) should start polling for failed bridge tx with featureId=perps 2`] = ` { + "bridge": "debridge", "destChain": { "chainId": 10, "token": {}, @@ -6282,18 +6277,18 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chainId": 42161, "token": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "iconUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", "name": "ETH", - "priceUSD": "2518.47", "symbol": "ETH", }, "txHash": "0xperpsSrcTxHash1", }, - "status": "PENDING", + "status": "FAILED", } `; 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 2f3aef3cb8..cd54565e56 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -5237,9 +5237,9 @@ describe('BridgeStatusController', () => { srcTxHash: '0xperpsSrcTxHash1', featureId: FeatureId.PERPS as never, }), - ...MockTxHistory.getPending({ + ...MockTxHistory.getPendingSwap({ txMetaId: 'quickBuyBridgeTxMetaId1', - srcTxHash: '0xperpsSrcTxHash1', + srcTxHash: '0xquickBuySrcTxHash1', featureId: FeatureId.QUICK_BUY_FOLLOW_TRADING as never, }), // ActionId-keyed entries for pre-submission failure tests @@ -5525,7 +5525,7 @@ describe('BridgeStatusController', () => { "allowance_reset_transaction": undefined, "approval_transaction": undefined, "batch_id": "0xBatchId3", - "chain_id_destination": "eip155:10", + "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": true, "destination_transaction": "FAILED", @@ -5544,8 +5544,8 @@ describe('BridgeStatusController', () => { "slippage_limit": 0, "source_transaction": "COMPLETE", "stx_enabled": false, - "swap_type": "crosschain", - "token_address_destination": "eip155:10/slip44:60", + "swap_type": "single_chain", + "token_address_destination": "eip155:42161/slip44:60", "token_address_source": "eip155:42161/slip44:60", "token_security_type_destination": null, "token_symbol_destination": "ETH", @@ -5553,7 +5553,7 @@ describe('BridgeStatusController', () => { "usd_actual_gas": 0, "usd_actual_return": 0, "usd_amount_source": 0, - "usd_quoted_gas": 2.5778, + "usd_quoted_gas": 0, "usd_quoted_return": 0, }, ], @@ -5942,74 +5942,6 @@ describe('BridgeStatusController', () => { [ "AuthenticationController:getBearerToken", ], - [ - "AuthenticationController:getBearerToken", - ], - [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - [ - "TransactionController:getState", - ], - [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Completed", - { - "account_hardware_type": null, - "action_type": "swapbridge-v1", - "actual_time_minutes": 833734.9086166667, - "allowance_reset_transaction": undefined, - "approval_transaction": undefined, - "chain_id_destination": "eip155:10", - "chain_id_source": "eip155:42161", - "custom_slippage": true, - "destination_transaction": "COMPLETE", - "feature_id": "quick_buy_follow_trading", - "gas_included": false, - "gas_included_7702": false, - "is_hardware_wallet": false, - "location": "Main View", - "price_impact": 0, - "provider": "lifi_across", - "quote_vs_execution_ratio": 0, - "quoted_time_minutes": 0.25, - "quoted_vs_used_gas_ratio": 0, - "security_warnings": [], - "slippage_limit": 0, - "source_transaction": "COMPLETE", - "stx_enabled": false, - "swap_type": "crosschain", - "token_address_destination": "eip155:10/slip44:60", - "token_address_source": "eip155:42161/slip44:60", - "token_security_type_destination": null, - "token_symbol_destination": "ETH", - "token_symbol_source": "ETH", - "usd_actual_gas": 0, - "usd_actual_return": 0, - "usd_amount_source": 0, - "usd_quoted_gas": 2.5778, - "usd_quoted_return": 0, - }, - ], - [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Status Failed Validation", - { - "action_type": "swapbridge-v1", - "chain_id_destination": "eip155:10", - "chain_id_source": "eip155:42161", - "failures": [ - "across|unknown", - ], - "feature_id": "perps", - "location": "Main View", - "refresh_count": 0, - "token_address_destination": "eip155:10/slip44:60", - "token_address_source": "eip155:42161/slip44:60", - "token_security_type_destination": null, - }, - ], ] `); expect(mockFetchFn).toHaveBeenCalledWith( @@ -6061,74 +5993,6 @@ describe('BridgeStatusController', () => { [ "AuthenticationController:getBearerToken", ], - [ - "AuthenticationController:getBearerToken", - ], - [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - [ - "TransactionController:getState", - ], - [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Failed", - { - "account_hardware_type": null, - "action_type": "swapbridge-v1", - "actual_time_minutes": 833734.9086166667, - "allowance_reset_transaction": undefined, - "approval_transaction": undefined, - "chain_id_destination": "eip155:10", - "chain_id_source": "eip155:42161", - "custom_slippage": true, - "destination_transaction": "FAILED", - "feature_id": "quick_buy_follow_trading", - "gas_included": false, - "gas_included_7702": false, - "is_hardware_wallet": false, - "location": "Main View", - "price_impact": 0, - "provider": "lifi_across", - "quote_vs_execution_ratio": 0, - "quoted_time_minutes": 0.25, - "quoted_vs_used_gas_ratio": 0, - "security_warnings": [], - "slippage_limit": 0, - "source_transaction": "COMPLETE", - "stx_enabled": false, - "swap_type": "crosschain", - "token_address_destination": "eip155:10/slip44:60", - "token_address_source": "eip155:42161/slip44:60", - "token_security_type_destination": null, - "token_symbol_destination": "ETH", - "token_symbol_source": "ETH", - "usd_actual_gas": 0, - "usd_actual_return": 0, - "usd_amount_source": 0, - "usd_quoted_gas": 2.5778, - "usd_quoted_return": 0, - }, - ], - [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Status Failed Validation", - { - "action_type": "swapbridge-v1", - "chain_id_destination": "eip155:10", - "chain_id_source": "eip155:42161", - "failures": [ - "across|unknown", - ], - "feature_id": "perps", - "location": "Main View", - "refresh_count": 0, - "token_address_destination": "eip155:10/slip44:60", - "token_address_source": "eip155:42161/slip44:60", - "token_security_type_destination": null, - }, - ], ] `); expect(mockFetchFn).toHaveBeenCalledWith( @@ -6321,30 +6185,12 @@ describe('BridgeStatusController', () => { [ "AuthenticationController:getBearerToken", ], - [ - "AuthenticationController:getBearerToken", - ], [ "AccountsController:getAccountByAddress", "0xaccount1", ], [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Status Failed Validation", - { - "action_type": "swapbridge-v1", - "chain_id_destination": "eip155:10", - "chain_id_source": "eip155:42161", - "failures": [ - "across|unknown", - ], - "feature_id": "perps", - "location": "Main View", - "refresh_count": 0, - "token_address_destination": "eip155:10/slip44:60", - "token_address_source": "eip155:42161/slip44:60", - "token_security_type_destination": null, - }, + "TransactionController:getState", ], ] `); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 368e114e14..dae8effb20 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -79,7 +79,6 @@ import { getEVMTxPropertiesFromTransactionMeta, getTxStatusesFromHistory, getPreConfirmationPropertiesFromQuote, - getPollingStatusUpdatedProperties, } from './utils/metrics'; import { getSelectedChainId } from './utils/network'; import { getTraceParams } from './utils/trace'; @@ -1295,7 +1294,6 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Wed, 3 Jun 2026 11:19:06 -0700 Subject: [PATCH 11/14] fix: test --- .../bridge-controller/src/utils/metrics/properties.test.ts | 1 + .../src/bridge-status-controller.ts | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/bridge-controller/src/utils/metrics/properties.test.ts b/packages/bridge-controller/src/utils/metrics/properties.test.ts index 3afc19043e..b2e59c10f0 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.test.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.test.ts @@ -427,6 +427,7 @@ describe('properties', () => { { "best_quote_provider": "bridge2_bridge2", "can_submit": false, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "price_impact": 0, diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index dae8effb20..a5b644208d 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1272,16 +1272,14 @@ export class BridgeStatusController extends StaticIntervalPollingController, - // eslint-disable-next-line @typescript-eslint/naming-convention - FeatureIdType extends { feature_id?: FeatureId } = { + > & { // eslint-disable-next-line @typescript-eslint/naming-convention feature_id?: FeatureId; }, >( eventName: EventName, txHistoryKey?: string, - eventProperties?: EventProperties & FeatureIdType, + eventProperties?: EventProperties, ): void => { const historyItem: BridgeHistoryItem | undefined = txHistoryKey ? this.state.txHistory[txHistoryKey] From 6051dd1b0f5a151a4798d9e81b201f1806065e9b Mon Sep 17 00:00:00 2001 From: micaelae Date: Wed, 3 Jun 2026 11:21:01 -0700 Subject: [PATCH 12/14] fix: test --- .../src/bridge-controller.test.ts | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 2c880537b9..015edfd079 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -173,10 +173,7 @@ async function withController( ...options, }); if (!options.state) { - newRootMessenger.call( - 'BridgeController:resetState', - FeatureId.UNIFIED_SWAP_BRIDGE, - ); + newRootMessenger.call('BridgeController:resetState'); } return await testFunction({ controller, rootMessenger: newRootMessenger }); } @@ -633,10 +630,7 @@ describe('BridgeController', function () { srcTokenAddress: '0x2ABC', }); - rootMessenger.call( - 'BridgeController:resetState', - FeatureId.UNIFIED_SWAP_BRIDGE, - ); + rootMessenger.call('BridgeController:resetState'); expect(bridgeController.state.quoteRequest[0]).toStrictEqual({ srcTokenAddress: '0x0000000000000000000000000000000000000000', }); @@ -682,10 +676,7 @@ describe('BridgeController', function () { 'Warning', ); - rootMessenger.call( - 'BridgeController:resetState', - FeatureId.UNIFIED_SWAP_BRIDGE, - ); + rootMessenger.call('BridgeController:resetState'); expect(bridgeController.state.tokenSecurityTypeDestination).toBeNull(); }, ); @@ -2311,10 +2302,7 @@ describe('BridgeController', function () { expect(bridgeController.state.quotes).toStrictEqual([]); // Verify state is reset - rootMessenger.call( - 'BridgeController:resetState', - FeatureId.UNIFIED_SWAP_BRIDGE, - ); + rootMessenger.call('BridgeController:resetState'); expect(bridgeController.state.quoteFetchError).toBeNull(); expect(bridgeController.state.quotesLoadingStatus).toBeNull(); expect(bridgeController.state.quotes).toStrictEqual([]); From 1b5dc76bf0bfd773c6272f8597b6efc7a46b73e2 Mon Sep 17 00:00:00 2001 From: micaelae Date: Wed, 3 Jun 2026 15:28:57 -0700 Subject: [PATCH 13/14] chore: require batch_id --- .../src/bridge-controller.sse.batch.test.ts | 5 +- .../src/bridge-controller.ts | 4 +- .../src/utils/metrics/types.ts | 12 ++--- .../bridge-status-controller.test.ts.snap | 50 +++++++++---------- ...ridge-status-controller.batch-sell.test.ts | 9 ++-- .../src/bridge-status-controller.ts | 8 ++- .../src/utils/bridge.ts | 2 +- 7 files changed, 44 insertions(+), 46 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.sse.batch.test.ts b/packages/bridge-controller/src/bridge-controller.sse.batch.test.ts index dba30e1cb4..a4b35ff56d 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.batch.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.batch.test.ts @@ -753,10 +753,7 @@ describe('BridgeController BatchSell (multiple quote requests) SSE', function () 'BridgeController:updateBatchSellTrades', [], ); - rootMessenger.call( - 'BridgeController:resetState', - FeatureId.BATCH_SELL, - ); + rootMessenger.call('BridgeController:resetState'); await jest.advanceTimersByTimeAsync(1000); await flushPromises(); diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 01abb1852b..f84809c296 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -681,8 +681,8 @@ export class BridgeController extends StaticIntervalPollingController { this.stopAllPolling(); // If polling is stopped before quotes finish loading, track QuotesReceived @@ -713,7 +713,7 @@ export class BridgeController extends StaticIntervalPollingController { - this.stopPollingForQuotes(context, reason); + this.stopPollingForQuotes(reason, context); this.update((state) => { // Cannot do direct assignment to state, i.e. state = {... }, need to manually assign each field if (quoteRequestIndex === null) { diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index d84f630f36..c27e6abfc7 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -172,7 +172,7 @@ type RequiredEventContextFromClientBase = { | 'token_security_type_destination' > & { action_type: MetricsActionType; - batch_id?: string; + batch_id: string; }; [UnifiedSwapBridgeEventName.Completed]: TradeData & Pick & @@ -185,11 +185,11 @@ type RequiredEventContextFromClientBase = { quote_vs_execution_ratio: number; quoted_vs_used_gas_ratio: number; action_type: MetricsActionType; - batch_id?: string; + batch_id: string; }; [UnifiedSwapBridgeEventName.Failed]: ( | // Tx failed before confirmation - (Pick< + (Pick< RequestMetadata, | 'stx_enabled' | 'usd_amount_source' @@ -214,12 +214,12 @@ type RequiredEventContextFromClientBase = { TradeData & Pick & { error_message: string; - batch_id?: string; + batch_id: string; }; [UnifiedSwapBridgeEventName.PollingStatusUpdated]: { polling_status: PollingStatus; retry_attempts: number; - batch_id?: string; + batch_id: string; }; [UnifiedSwapBridgeEventName.StatusValidationFailed]: { failures: string[]; @@ -352,7 +352,7 @@ export type EventPropertiesFromControllerState = { | 'chain_id_source' | 'chain_id_destination' > & { - batch_id?: string; + batch_id: string; }; }; diff --git a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap index 449724b59d..b2c411900a 100644 --- a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap +++ b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap @@ -602,8 +602,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -917,8 +917,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -1272,6 +1272,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac [ [ "BridgeController:stopPollingForQuotes", + "Transaction submitted", { "best_quote_provider": "lifi_across", "can_submit": true, @@ -1288,7 +1289,6 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac "low_return", ], }, - "Transaction submitted", ], [ "AccountsController:getAccountByAddress", @@ -1537,8 +1537,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -1852,8 +1852,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -2167,8 +2167,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -2502,8 +2502,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -2792,8 +2792,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -2889,8 +2889,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -3013,8 +3013,8 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -3288,8 +3288,8 @@ exports[`BridgeStatusController submitTx: EVM bridge waits for approval tx confi [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -3474,8 +3474,8 @@ exports[`BridgeStatusController submitTx: EVM swap should gracefully handle isAt [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -3971,8 +3971,8 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -4121,8 +4121,8 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -4378,8 +4378,8 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -4835,8 +4835,8 @@ exports[`BridgeStatusController submitTx: Solana bridge should handle snap contr [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -4926,8 +4926,8 @@ exports[`BridgeStatusController submitTx: Solana bridge should successfully subm [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -5128,8 +5128,8 @@ exports[`BridgeStatusController submitTx: Solana bridge should throw error when [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -5201,8 +5201,8 @@ exports[`BridgeStatusController submitTx: Solana swap should handle snap control [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -5292,8 +5292,8 @@ exports[`BridgeStatusController submitTx: Solana swap should successfully submit [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -5533,8 +5533,8 @@ exports[`BridgeStatusController submitTx: Solana swap should throw error when ac [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -5547,8 +5547,8 @@ exports[`BridgeStatusController submitTx: Solana swap should throw error when sn [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -5620,8 +5620,8 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should handle [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -5715,8 +5715,8 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should success [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", @@ -5941,8 +5941,8 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should success [ [ "BridgeController:stopPollingForQuotes", - undefined, "Transaction submitted", + undefined, ], [ "AccountsController:getAccountByAddress", diff --git a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts index 7ce2063d1d..6352d1e610 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts @@ -301,8 +301,8 @@ describe('BridgeStatusController', () => { ['BridgeController:getState'], [ 'BridgeController:stopPollingForQuotes', - undefined, 'Transaction submitted', + undefined, ], [ 'AccountsController:getAccountByAddress', @@ -733,10 +733,7 @@ describe('BridgeStatusController', () => { const result = await expect( rootMessenger.call('BridgeStatusController:submitBatchSell', { accountAddress: (mockQuotes[0].trade as TxData).from, - quoteResponses: mockQuotes.map((quote) => ({ - ...quote, - featureId: FeatureId.BATCH_SELL, - })), + quoteResponses: mockQuotes, isStxEnabled: stxEnabled, }), ).rejects.toThrow( @@ -752,8 +749,8 @@ describe('BridgeStatusController', () => { ['BridgeController:getState'], [ 'BridgeController:stopPollingForQuotes', - undefined, 'Transaction submitted', + undefined, ], [ 'AccountsController:getAccountByAddress', diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index a5b644208d..cdd0739b92 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1271,10 +1271,12 @@ export class BridgeStatusController extends StaticIntervalPollingController & { // eslint-disable-next-line @typescript-eslint/naming-convention feature_id?: FeatureId; + // eslint-disable-next-line @typescript-eslint/naming-convention + batch_id?: string; }, >( eventName: EventName, @@ -1315,10 +1317,12 @@ export class BridgeStatusController extends StaticIntervalPollingController { messenger.call( 'BridgeController:stopPollingForQuotes', - metricsContext, AbortReason.TransactionSubmitted, + metricsContext, ); }; From 5c9a8ea39f844c985c9e0c6ec163f3c33614d125 Mon Sep 17 00:00:00 2001 From: micaelae Date: Wed, 3 Jun 2026 15:57:39 -0700 Subject: [PATCH 14/14] chore: changelog --- packages/bridge-controller/CHANGELOG.md | 11 +++++++++++ packages/bridge-controller/src/utils/metrics/types.ts | 11 +++++------ packages/bridge-status-controller/CHANGELOG.md | 8 ++++++++ .../src/bridge-status-controller.ts | 8 ++------ packages/transaction-controller/CHANGELOG.md | 1 + packages/transaction-pay-controller/CHANGELOG.md | 1 + 6 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index cdce1db005..5de8ceb741 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,8 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `QUICK_BUY_FOLLOW_TRADING`, `QUICK_BUY_TOKEN_DETAILS`, `BATCH_SELL` and `UNIFIED_SWAP_BRIDGE` to FeatureId enum ([#8964](https://github.com/MetaMask/core/pull/8964)) +- Update metrics schema with `batch_id` property ([#8964](https://github.com/MetaMask/core/pull/8964)) + ### Changed +- **BREAKING**: require all events to have the `feature_id` property ([#8964](https://github.com/MetaMask/core/pull/8964)) +- **BREAKING**: require FeatureId argument when calling `BridgeController:fetchQuotes` ([#8964](https://github.com/MetaMask/core/pull/8964)) +- Rename FeatureIds to match segment property conventions ([#8964](https://github.com/MetaMask/core/pull/8964)) + - `quickBuy` to `quick_buy_follow_trading` and `quick_buy_token_details` + - `dappSwap` to `dapp_swap` + - Bump `@metamask/assets-controllers` from `^108.2.0` to `^108.4.0` ([#8941](https://github.com/MetaMask/core/pull/8941), [#8981](https://github.com/MetaMask/core/pull/8981)) - Bump `@metamask/assets-controller` from `^8.1.0` to `^8.3.1` ([#8943](https://github.com/MetaMask/core/pull/8943), [#8981](https://github.com/MetaMask/core/pull/8981), [#8985](https://github.com/MetaMask/core/pull/8985)) - Bump `@metamask/remote-feature-flag-controller` from `^4.2.1` to `^4.2.2` ([#8986](https://github.com/MetaMask/core/pull/8986)) diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index c27e6abfc7..b766a6ba01 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -172,7 +172,7 @@ type RequiredEventContextFromClientBase = { | 'token_security_type_destination' > & { action_type: MetricsActionType; - batch_id: string; + batch_id?: string; }; [UnifiedSwapBridgeEventName.Completed]: TradeData & Pick & @@ -185,11 +185,11 @@ type RequiredEventContextFromClientBase = { quote_vs_execution_ratio: number; quoted_vs_used_gas_ratio: number; action_type: MetricsActionType; - batch_id: string; + batch_id?: string; }; [UnifiedSwapBridgeEventName.Failed]: ( | // Tx failed before confirmation - (Pick< + (Pick< RequestMetadata, | 'stx_enabled' | 'usd_amount_source' @@ -214,12 +214,11 @@ type RequiredEventContextFromClientBase = { TradeData & Pick & { error_message: string; - batch_id: string; + batch_id?: string; }; [UnifiedSwapBridgeEventName.PollingStatusUpdated]: { polling_status: PollingStatus; retry_attempts: number; - batch_id: string; }; [UnifiedSwapBridgeEventName.StatusValidationFailed]: { failures: string[]; @@ -352,7 +351,7 @@ export type EventPropertiesFromControllerState = { | 'chain_id_source' | 'chain_id_destination' > & { - batch_id: string; + batch_id?: string; }; }; diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 11c948e4c1..b0c489dcde 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `batch_id` property to BatchSell events ([#8964](https://github.com/MetaMask/core/pull/8964)) + - pre-generate the batchId using transaction controller's `generateBatchId` util + - attach batchId to the `Submitted`, `Completed` and `Failed` events + - provide batchId to the `TransactionController:addTransactionBatch` to propagate it the TransactionMeta +- Publish tx submission metrics for `BatchSell`, `QuickBuy` and `UnifiedSwapBridge` actions ([#8964](https://github.com/MetaMask/core/pull/8964)) + ## [72.0.1] ### Fixed diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index cdd0739b92..a5b644208d 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1271,12 +1271,10 @@ export class BridgeStatusController extends StaticIntervalPollingController & { // eslint-disable-next-line @typescript-eslint/naming-convention feature_id?: FeatureId; - // eslint-disable-next-line @typescript-eslint/naming-convention - batch_id?: string; }, >( eventName: EventName, @@ -1317,12 +1315,10 @@ export class BridgeStatusController extends StaticIntervalPollingController