diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index e247bd367b7..83a209b0607 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Derive `security_warnings` from controller state `tokenWarnings` in `QuotesReceived` metrics event ([#8394](https://github.com/MetaMask/core/pull/8394)) - Bump `@metamask/accounts-controller` from `^37.1.1` to `^37.2.0` ([#8363](https://github.com/MetaMask/core/pull/8363)) - Bump `@metamask/messenger` from `^1.0.0` to `^1.1.1` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373)) 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 5fb18da1a0c..6e4daa910d5 100644 --- a/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap +++ b/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap @@ -159,6 +159,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent bridge-status-controller c "lifi_mayan", "lifi_mayanMCTP", ], + "security_warnings": [], "slippage_limit": 0.5, "stx_enabled": false, "swap_type": "crosschain", @@ -236,6 +237,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "price_impact": 6, "quotes_count": 0, "quotes_list": [], + "security_warnings": [], "slippage_limit": undefined, "stx_enabled": false, "swap_type": "crosschain", @@ -267,6 +269,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "price_impact": 6, "quotes_count": 0, "quotes_list": [], + "security_warnings": [], "slippage_limit": undefined, "sort_order": "cost_ascending", "stx_enabled": false, @@ -376,6 +379,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "quoted_time_minutes": 10, "quotes_count": 0, "quotes_list": [], + "security_warnings": [], "slippage_limit": undefined, "swap_type": "crosschain", "token_address_destination": null, @@ -419,6 +423,9 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t "quotes_count": 0, "quotes_list": [], "refresh_count": 0, + "security_warnings": [ + "insufficient_balance", + ], "slippage_limit": undefined, "swap_type": "crosschain", "token_address_destination": null, @@ -518,6 +525,9 @@ exports[`BridgeController updateBridgeQuoteRequestParams should only poll once i "lifi_celercircle", ], "refresh_count": 1, + "security_warnings": [ + "low_return", + ], "slippage_limit": 0.5, "swap_type": "crosschain", "token_address_destination": "eip155:10/erc20:0x123", diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index f442bab2f4e..1ee18c6d267 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -26,6 +26,7 @@ import * as selectors from './selectors'; import { ChainId, RequestStatus, SortOrder, StatusTypes } from './types'; import type { BridgeControllerMessenger, + BridgeControllerState, QuoteResponse, GenericQuoteRequest, } from './types'; @@ -41,7 +42,7 @@ import { MetricsSwapType, UnifiedSwapBridgeEventName, } from './utils/metrics/constants'; -import { FeatureId } from './utils/validators'; +import { FeatureId, TokenFeatureType } from './utils/validators'; import { flushPromises } from '../../../tests/helpers'; import { handleFetch } from '../../controller-utils/src'; import mockBridgeQuotesErc20Native from '../tests/mock-quotes-erc20-native.json'; @@ -3231,10 +3232,66 @@ describe('BridgeController', function () { }, ); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); - expect(trackMetaMetricsFn.mock.calls).toMatchSnapshot(); }); }); + + it('should include controller-derived security_warnings in QuotesReceived', async () => { + await withController(async ({ controller, rootMessenger }) => { + jest.spyOn(console, 'warn').mockImplementationOnce(jest.fn()); + await rootMessenger.call( + 'BridgeController:updateBridgeQuoteRequestParams', + { + walletAddress: '0x123', + }, + { + stx_enabled: false, + security_warnings: [], + token_symbol_source: 'ETH', + token_symbol_destination: 'USDC', + usd_amount_source: 100, + }, + ); + ( + controller as unknown as { + update: (updater: (s: BridgeControllerState) => void) => void; + } + ).update((state: BridgeControllerState) => { + state.tokenWarnings = [ + { + feature_id: 'token_flagged_malicious', + type: TokenFeatureType.MALICIOUS, + description: 'Token is flagged', + }, + ]; + }); + jest.clearAllMocks(); + rootMessenger.call( + 'BridgeController:trackUnifiedSwapBridgeEvent', + UnifiedSwapBridgeEventName.QuotesReceived, + { + warnings: ['insufficient_balance'], + usd_quoted_gas: 0, + gas_included: false, + gas_included_7702: false, + quoted_time_minutes: 10, + usd_quoted_return: 100, + price_impact: 0, + provider: 'provider_bridge', + best_quote_provider: 'provider_bridge2', + can_submit: true, + usd_balance_source: 0, + }, + ); + expect(trackMetaMetricsFn).toHaveBeenCalledTimes(1); + expect(trackMetaMetricsFn).toHaveBeenCalledWith( + UnifiedSwapBridgeEventName.QuotesReceived, + expect.objectContaining({ + security_warnings: ['insufficient_balance', 'Token is flagged'], + }), + ); + }); + }); }); describe('trackUnifiedSwapBridgeEvent bridge-status-controller calls', () => { @@ -3494,7 +3551,7 @@ describe('BridgeController', function () { it('should not track the event if the account keyring type is not set', async () => { await withController(async ({ rootMessenger }) => { - rootMessenger.call( + await rootMessenger.call( 'BridgeController:setLocation', MetaMetricsSwapsEventSource.TrendingExplore, ); diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 858f6046583..d4648353c75 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -958,9 +958,25 @@ export class BridgeController extends StaticIntervalPollingController quote.gasIncluded, ), + security_warnings: this.state.tokenWarnings.map( + (warning) => warning.description, + ), }; }; + // The client-provided `warnings` field comes from the Blockaid `/message/scan/` + // endpoint, currently used for Solana only. This has not been migrated to the + // backend yet, so it is merged here with controller-derived token warnings. + readonly #mergeSecurityWarnings = ( + clientProps: Record, + ): string[] => { + const clientWarnings = (clientProps.warnings as string[]) ?? []; + const controllerWarnings = this.state.tokenWarnings.map( + (warning) => warning.description, + ); + return [...new Set([...clientWarnings, ...controllerWarnings])]; + }; + readonly #getEventProperties = < EventName extends (typeof UnifiedSwapBridgeEventName)[keyof typeof UnifiedSwapBridgeEventName], @@ -1000,6 +1016,7 @@ export class BridgeController extends StaticIntervalPollingController