diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 6bf793fa2b..f4c1027503 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,6 +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` + ## [73.2.1] ### Changed diff --git a/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.batch.test.ts.snap b/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.batch.test.ts.snap index 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..a4b35ff56d 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, ), ), diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 0c10142524..636d98c147 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, diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 07d58a7b44..015edfd079 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, @@ -927,7 +934,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 +1043,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 +1538,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 +1596,7 @@ describe('BridgeController', function () { best_quote_provider: 'provider_bridge2', can_submit: true, usd_balance_source: 0, + feature_id: FeatureId.DAPP_SWAP, }, ); @@ -1932,7 +1941,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 +1965,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 +1994,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 +2161,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( @@ -2967,6 +2973,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 +2984,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 +3010,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 +3042,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 +3053,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 +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, }), ); }); @@ -3076,6 +3088,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 +3099,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 +3109,7 @@ describe('BridgeController', function () { input: 'token_amount_source', input_value: '1', input_amount_preset: '95%', + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }, ); @@ -3133,6 +3148,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 +3163,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 +3189,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 +3203,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 +3229,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 +3245,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 +3271,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 +3289,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 +3315,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 +3334,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 +3357,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 +3376,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 +3405,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 +3418,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 +3471,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 +3515,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 +3557,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 +3606,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 +3644,7 @@ describe('BridgeController', function () { { failures: ['Failed to submit tx'], refresh_count: 0, + feature_id: FeatureId.PERPS, }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); @@ -3682,6 +3716,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 +3734,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 +3788,11 @@ describe('BridgeController', function () { ...overrides, }); + let getBridgeFeatureFlagsSpy: jest.SpyInstance; + beforeEach(() => { jest.clearAllMocks(); - jest + getBridgeFeatureFlagsSpy = jest .spyOn(featureFlagUtils, 'getBridgeFeatureFlags') .mockReturnValueOnce({ ...defaultFlags, @@ -3767,6 +3805,7 @@ describe('BridgeController', function () { }, }); messengerCallMock.mockResolvedValueOnce('AUTH_TOKEN'); + messengerCallMock.mockResolvedValueOnce('AUTH_TOKEN'); messengerCallMock.mockReturnValueOnce(() => ({ address: '0x123', })); @@ -3799,8 +3838,8 @@ describe('BridgeController', function () { gasIncluded7702: false, fee: 0, }, - null, FeatureId.PERPS, + null, ); expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); @@ -3871,8 +3910,8 @@ describe('BridgeController', function () { gasIncluded: false, gasIncluded7702: false, } as never, - null, FeatureId.PERPS, + null, ), ).rejects.toThrow('Account address is required'); @@ -3906,8 +3945,8 @@ describe('BridgeController', function () { gasIncluded: false, gasIncluded7702: false, }, - null, FeatureId.PERPS, + null, ); expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); @@ -3975,35 +4014,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 +4142,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 d8eefb0a0a..f84809c296 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: { @@ -186,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 = [ @@ -356,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 @@ -401,14 +387,14 @@ export class BridgeController extends StaticIntervalPollingController => { const bridgeFeatureFlags = getBridgeFeatureFlags(this.messenger); const jwt = await this.#getJwt(); @@ -432,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, }, @@ -544,7 +534,7 @@ export class BridgeController extends StaticIntervalPollingController[], + quoteRequests: GenericQuoteRequest[], ) => { const exchangeRateSources = this.#getExchangeRateSources(); @@ -553,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( @@ -725,8 +711,9 @@ export class BridgeController extends StaticIntervalPollingController { - this.stopPollingForQuotes(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) { @@ -855,6 +842,7 @@ export class BridgeController extends StaticIntervalPollingController { @@ -948,6 +937,7 @@ export class BridgeController extends StaticIntervalPollingController { @@ -966,11 +956,13 @@ export class BridgeController extends StaticIntervalPollingController + this.#trackQuoteValidationFailures(validationFailures, featureId), onValidQuoteReceived: async (quote: QuoteResponse) => { const feeAppendPromise = (async () => { const quotesWithFees = await appendFeesToQuotes( @@ -1270,6 +1262,7 @@ export class BridgeController extends StaticIntervalPollingController, + featureId: FeatureId, quoteRequestIndex: number = 0, ) => { Object.entries(paramsToUpdate).forEach(([key, value]) => { @@ -1293,6 +1286,7 @@ export class BridgeController extends StaticIntervalPollingController { { "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-controller/src/utils/metrics/properties.ts b/packages/bridge-controller/src/utils/metrics/properties.ts index a550e7190d..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, @@ -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/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index 2ad785c827..b766a6ba01 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 } from '@metamask/utils'; -import type { SortOrder, StatusTypes } from '../../types'; +import type { FeatureId, SortOrder, StatusTypes } from '../../types'; import type { UnifiedSwapBridgeEventName, MetaMetricsSwapsEventSource, @@ -20,6 +20,13 @@ export type RequestParams = { token_symbol_destination: string | null; token_address_source: CaipAssetType; token_address_destination: CaipAssetType | null; + /** + * 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. + */ token_security_type_destination: string | null; }; @@ -133,6 +140,7 @@ type RequiredEventContextFromClientBase = { > & { 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[]; @@ -164,6 +172,7 @@ type RequiredEventContextFromClientBase = { | 'token_security_type_destination' > & { action_type: MetricsActionType; + batch_id?: string; }; [UnifiedSwapBridgeEventName.Completed]: TradeData & Pick & @@ -176,18 +185,17 @@ type RequiredEventContextFromClientBase = { quote_vs_execution_ratio: number; quoted_vs_used_gas_ratio: number; action_type: MetricsActionType; + 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' @@ -195,15 +203,27 @@ 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; - }); + }) + ) & + TradeData & + Pick & { + error_message: string; + batch_id?: string; + }; + [UnifiedSwapBridgeEventName.PollingStatusUpdated]: { + polling_status: PollingStatus; + retry_attempts: number; + }; + [UnifiedSwapBridgeEventName.StatusValidationFailed]: { + failures: string[]; + refresh_count: number; + }; // Emitted by clients [UnifiedSwapBridgeEventName.AllQuotesOpened]: Pick< TradeData, @@ -241,27 +261,9 @@ type RequiredEventContextFromClientBase = { [UnifiedSwapBridgeEventName.QuotesValidationFailed]: { failures: string[]; }; - [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; - }; }; /** @@ -278,6 +280,7 @@ export type RequiredEventContextFromClient = { location?: MetaMetricsSwapsEventSource; ab_tests?: Record; active_ab_tests?: { key: string; value: string }[]; + feature_id: FeatureId; }; }; @@ -338,7 +341,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; + }; }; /** @@ -352,6 +366,7 @@ export type CrossChainSwapsEventProperties< T extends UnifiedSwapBridgeEventName, > = | { + feature_id: FeatureId; action_type: MetricsActionType; location: MetaMetricsSwapsEventSource; ab_tests?: Record; 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/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index c9702e5796..de22eb2e99 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.2] ### Changed 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 2d12e0196e..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 @@ -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, @@ -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, @@ -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, @@ -1272,6 +1276,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac { "best_quote_provider": "lifi_across", "can_submit": true, + "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, "price_impact": 0, @@ -1298,6 +1303,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, @@ -1547,6 +1553,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, @@ -1861,6 +1868,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, @@ -2175,6 +2183,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, @@ -2509,6 +2518,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, @@ -2798,6 +2808,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, @@ -2894,6 +2905,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, @@ -2974,6 +2986,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, @@ -3016,6 +3029,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, @@ -3099,6 +3113,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, @@ -3289,6 +3304,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, @@ -3474,6 +3490,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, @@ -3970,6 +3987,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, @@ -4080,7 +4098,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", @@ -4099,7 +4117,7 @@ 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", @@ -4376,6 +4394,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, @@ -4832,6 +4851,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, @@ -4879,6 +4899,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, @@ -4921,6 +4942,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, @@ -5122,6 +5144,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, @@ -5151,6 +5174,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, @@ -5193,6 +5217,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, @@ -5240,6 +5265,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, @@ -5282,6 +5308,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, @@ -5339,6 +5366,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, @@ -5535,6 +5563,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, @@ -5564,6 +5593,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, @@ -5606,6 +5636,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, @@ -5657,6 +5688,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, @@ -5699,6 +5731,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, @@ -5924,6 +5957,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, @@ -6148,6 +6182,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", @@ -6165,6 +6200,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "failures": [ "across|unknown", ], + "feature_id": "unified_swap_bridge", "location": "Main View", "refresh_count": 0, "token_address_destination": "eip155:10/slip44:60", @@ -6188,7 +6224,7 @@ 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": { @@ -6229,7 +6265,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran } `; -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": { @@ -6278,6 +6314,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, @@ -6322,6 +6359,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, @@ -6366,11 +6404,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, @@ -6422,6 +6462,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, @@ -6464,6 +6505,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, @@ -6515,6 +6557,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, @@ -6559,6 +6602,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 f289162d21..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 @@ -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,6 +36,12 @@ import type { import { getBatchSellHistoryItemsForTxHash } from './utils/history'; import { shouldDisable7702 } from './utils/transaction'; +const mockGenerateBatchId = jest.fn(); +jest.mock('@metamask/transaction-controller', () => ({ + ...jest.requireActual('@metamask/transaction-controller'), + generateBatchId: (): string => mockGenerateBatchId(), +})); + type AllBridgeStatusControllerActions = MessengerActions; @@ -197,6 +206,7 @@ describe('BridgeStatusController', () => { dateNowSpy.mockReturnValueOnce(1779922719705); dateNowSpy.mockReturnValueOnce(1779988819705); dateNowSpy.mockReturnValueOnce(1779988919705); + mockGenerateBatchId.mockReturnValueOnce('0xGeneratedBatchId1'); }); it.each([true, false])( @@ -307,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, @@ -326,6 +337,7 @@ describe('BridgeStatusController', () => { usd_amount_source: 100, usd_quoted_gas: 0, usd_quoted_return: 0, + batch_id: '0xGeneratedBatchId1', }, ], [ @@ -358,6 +370,7 @@ describe('BridgeStatusController', () => { networkClientId: 'networkClientId', origin: 'metamask', requireApproval: false, + batchId: '0xGeneratedBatchId1', skipInitialGasEstimate: gasIncluded7702 ? isDelegatedAccount : Boolean(transferTx), @@ -399,6 +412,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 @@ -422,7 +436,7 @@ describe('BridgeStatusController', () => { quoteObject: Quote, ): Partial => ({ batchId: undefined, - featureId: undefined, + featureId: FeatureId.BATCH_SELL, slippagePercentage: 0, txMetaId: undefined, actionId: undefined, @@ -526,6 +540,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, @@ -608,11 +624,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, @@ -747,6 +765,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, @@ -789,6 +808,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 4430f3e730..cd54565e56 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(); @@ -4665,6 +4666,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, @@ -4752,6 +4754,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, @@ -5234,6 +5237,11 @@ describe('BridgeStatusController', () => { srcTxHash: '0xperpsSrcTxHash1', featureId: FeatureId.PERPS as never, }), + ...MockTxHistory.getPendingSwap({ + txMetaId: 'quickBuyBridgeTxMetaId1', + srcTxHash: '0xquickBuySrcTxHash1', + featureId: FeatureId.QUICK_BUY_FOLLOW_TRADING as never, + }), // ActionId-keyed entries for pre-submission failure tests 'pre-submission-action-id': { ...baseHistoryItem, @@ -5276,6 +5284,7 @@ describe('BridgeStatusController', () => { type: TransactionType.bridge, status: TransactionStatus.failed, id: 'bridgeTxMetaId1', + batchId: '0xBatchIdFailed1', }, }, ); @@ -5333,6 +5342,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, @@ -5402,6 +5412,7 @@ describe('BridgeStatusController', () => { active_ab_tests: [ { key: 'bridge_quote_sorting', value: 'variant_b' }, ], + feature_id: FeatureId.UNIFIED_SWAP_BRIDGE, }), ); }); @@ -5447,7 +5458,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', @@ -5469,7 +5480,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:42161", + "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": "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", + "token_symbol_source": "ETH", + "usd_actual_gas": 0, + "usd_actual_return": 0, + "usd_amount_source": 0, + "usd_quoted_gas": 0, + "usd_quoted_return": 0, + }, + ], + ] + `); }); it('should track failed event for swap transaction if approval fails', () => { @@ -5819,7 +5908,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(); @@ -5870,7 +5959,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(); @@ -5941,7 +6030,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', diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 5f7fb8c4a3..a5b644208d 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, @@ -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'; @@ -31,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, @@ -77,7 +79,6 @@ import { getEVMTxPropertiesFromTransactionMeta, getTxStatusesFromHistory, getPreConfirmationPropertiesFromQuote, - getPollingStatusUpdatedProperties, } from './utils/metrics'; import { getSelectedChainId } from './utils/network'; import { getTraceParams } from './utils/trace'; @@ -432,36 +433,14 @@ export class BridgeStatusController extends StaticIntervalPollingController + quoteFeatureId === FeatureId.BATCH_SELL, + ) + ? generateBatchId() + : undefined; + const preConfirmationProperties = getPreConfirmationPropertiesFromQuote( quoteResponse, isStxEnabled, @@ -1110,6 +1091,7 @@ export class BridgeStatusController extends StaticIntervalPollingController & { + // 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, ): 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 ( + !( + 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 && @@ -1382,7 +1370,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..24cff09945 100644 --- a/packages/bridge-status-controller/src/constants.ts +++ b/packages/bridge-status-controller/src/constants.ts @@ -1,3 +1,5 @@ +import { FeatureId } from '@metamask/bridge-controller'; + import type { BridgeStatusControllerState } from './types'; export const REFRESH_INTERVAL_MS = 10 * 1000; // 10 seconds @@ -21,3 +23,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/strategy/batch-sell-strategy.ts b/packages/bridge-status-controller/src/strategy/batch-sell-strategy.ts index 720e1563f2..147e84c5fd 100644 --- a/packages/bridge-status-controller/src/strategy/batch-sell-strategy.ts +++ b/packages/bridge-status-controller/src/strategy/batch-sell-strategy.ts @@ -37,6 +37,7 @@ export async function* submitBatchSellHandler( addTransactionBatchFn, isDelegatedAccount, batchSellTrades, + batchId: batchIdParam, } = args; const tradeData = toQuoteAndTxMetadataBatch({ @@ -64,6 +65,7 @@ export async function* submitBatchSellHandler( ), isGasFeeSponsored: gasSponsored, isGasFeeIncluded: Boolean(gasIncluded7702), + batchId: batchIdParam, skipInitialGasEstimate: gasIncluded7702 ? isDelegatedAccount : Boolean(gasFeeToken), @@ -96,6 +98,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 +151,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 2458ac41f3..c45024a9cb 100644 --- a/packages/bridge-status-controller/src/strategy/types.ts +++ b/packages/bridge-status-controller/src/strategy/types.ts @@ -12,6 +12,7 @@ import type { TransactionController, TransactionMeta, } from '@metamask/transaction-controller'; +import type { Hex } from '@metamask/utils'; import type { BridgeStatusControllerMessenger, @@ -21,11 +22,31 @@ import type { } from '../types'; 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', } @@ -94,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; @@ -108,4 +142,9 @@ export type SubmitStrategyParams< fetchFn: FetchFunction; clientId: BridgeClientId; bridgeApiBaseUrl: string; + /** + * The batch ID of the transaction batch passed to the addTransactionBatchFn + * This is only used for batch-sell transactions. + */ + batchId?: Hex; }; diff --git a/packages/bridge-status-controller/src/utils/bridge.ts b/packages/bridge-status-controller/src/utils/bridge.ts index 5a541bed20..01aa557264 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', 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, + metricsContext, ); }; diff --git a/packages/bridge-status-controller/src/utils/metrics.ts b/packages/bridge-status-controller/src/utils/metrics.ts index 772634f9e3..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 { @@ -33,14 +33,10 @@ import { TransactionType, } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; -import type { CaipAssetType } from '@metamask/utils'; +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, @@ -195,6 +191,7 @@ export const getPriceImpactFromQuote = ( * @param activeAbTests - New A/B test context for `active_ab_tests` (migration target) * @param tokenSecurityTypeDestination - The security classification of the destination token, supplied by the client (e.g. from token security/scanning data). Pass `null` when no security data is available. * @param batchSellTrades - The batch sell trades response + * @param batchId - The batch ID of the transaction batch. * @returns The properties for the pre-confirmation event */ export const getPreConfirmationPropertiesFromQuote = ( @@ -206,6 +203,7 @@ export const getPreConfirmationPropertiesFromQuote = ( activeAbTests?: { key: string; value: string }[], tokenSecurityTypeDestination?: string | null, batchSellTrades?: BatchSellTradesResponse | null, + batchId?: Hex, ) => { const { quote } = quoteResponse; return { @@ -237,6 +235,8 @@ export const getPreConfirmationPropertiesFromQuote = ( activeAbTests.length > 0 && { active_ab_tests: activeAbTests, }), + ...(batchId ? { batch_id: batchId } : {}), + feature_id: quoteResponse.featureId ?? FeatureId.UNIFIED_SWAP_BRIDGE, }; }; @@ -342,32 +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, + ...(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, diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index fd3aeac4a5..fd844c0efc 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Export `generateBatchId` utility ([#8964](https://github.com/MetaMask/core/pull/8964)) + ## [66.0.1] ### Changed 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); diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 42f8f96d98..a86b58bfa2 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Fall back to `FeatureId.PERPS` when calling `BridgeController:fetchQuotes` and the quote does not have a `featureId` ([#8964](https://github.com/MetaMask/core/pull/8964)) - Fiat quote submission now treats the provider code (e.g. `transak-native`) as the canonical form when resolving the provider from a ramps quote, while continuing to accept the legacy path form (e.g. `/providers/transak-native`) for backwards compatibility ([#9004](https://github.com/MetaMask/core/pull/9004)) - Live token balance queries now respect the `confirmations_pay_extended.excludeChainIdsFromInfura` feature flag, skipping the Infura endpoint preference for excluded chains ([#8992](https://github.com/MetaMask/core/pull/8992)) - Bump `@metamask/assets-controllers` from `^108.3.0` to `^108.5.0` ([#8981](https://github.com/MetaMask/core/pull/8981), [#8999](https://github.com/MetaMask/core/pull/8999)) 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) {