From 70c3280ee25ec35e947bbefa48fee6e35da6122c Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 09:44:23 -0800 Subject: [PATCH 01/17] chore: export getQuotesReceivedProperties util --- packages/bridge-controller/src/index.ts | 1 + .../src/utils/metrics/properties.ts | 24 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index a1e8bef2b9e..e9aa19f9c5e 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -21,6 +21,7 @@ export { getSwapType, isHardwareWallet, isCustomSlippage, + getQuotesReceivedProperties, } from './utils/metrics/properties'; export type { diff --git a/packages/bridge-controller/src/utils/metrics/properties.ts b/packages/bridge-controller/src/utils/metrics/properties.ts index 6d6baa3f6ec..114e48704de 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.ts @@ -3,7 +3,7 @@ import type { AccountsControllerState } from '@metamask/accounts-controller'; import { MetricsSwapType } from './constants'; import type { InputKeys, InputValues, RequestParams } from './types'; import { DEFAULT_BRIDGE_CONTROLLER_STATE } from '../../constants/bridge'; -import type { QuoteResponse, TxData } from '../../types'; +import type { QuoteMetadata, QuoteResponse, TxData } from '../../types'; import { ChainId, type GenericQuoteRequest, @@ -114,3 +114,25 @@ export const isHardwareWallet = ( export const isCustomSlippage = (slippage: GenericQuoteRequest['slippage']) => { return slippage !== DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest.slippage; }; + +export const getQuotesReceivedProperties = ( + activeQuote: QuoteResponse & Partial, + warnings: string[] = [], + recommendedQuote?: QuoteResponse & Partial, +) => { + const provider =activeQuote ? formatProviderLabel(activeQuote.quote) : '_' + return { + can_submit: true, + gas_included: Boolean(activeQuote?.quote?.gasIncluded), + gas_included_7702: Boolean(activeQuote?.quote?.gasIncluded7702), + quoted_time_minutes: activeQuote?.estimatedProcessingTimeInSeconds + ? activeQuote.estimatedProcessingTimeInSeconds / 60 + : 0, + usd_quoted_gas: Number(activeQuote?.gasFee?.effective?.usd ?? 0), + usd_quoted_return: Number(activeQuote?.toTokenAmount?.usd ?? 0), + best_quote_provider: recommendedQuote ? formatProviderLabel(recommendedQuote.quote) : provider, + provider, + warnings, + price_impact: Number(activeQuote?.quote.priceData?.priceImpact ?? 0), + }; +}; From d93b3786b16f0b60e86047e86252399947891d30 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 09:45:13 -0800 Subject: [PATCH 02/17] fix: publish QuotesReceived event if trade is submitted before quotes finish loading --- .../src/bridge-status-controller.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index bf7d613ff31..1592f2eee97 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1,11 +1,12 @@ import type { AccountsControllerState } from '@metamask/accounts-controller'; import type { StateMetadata } from '@metamask/base-controller'; -import type { - QuoteMetadata, - RequiredEventContextFromClient, - TxData, - QuoteResponse, - Trade, +import { + type QuoteMetadata, + type RequiredEventContextFromClient, + type TxData, + type QuoteResponse, + type Trade, + getQuotesReceivedProperties, } from '@metamask/bridge-controller'; import { formatChainIdToHex, @@ -65,6 +66,7 @@ import { getEVMTxPropertiesFromTransactionMeta, getTxStatusesFromHistory, getPreConfirmationPropertiesFromQuote, + } from './utils/metrics'; import { findAndUpdateTransactionsInBatch, @@ -1031,13 +1033,21 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata, isStxEnabledOnClient: boolean, + isLoading?: boolean, + warnings?: string[], ): Promise> => { + // If trade is submitted before all quotes are loaded, publish QuotesReceived event + if(isLoading) { + this.#trackUnifiedSwapBridgeEvent(UnifiedSwapBridgeEventName.QuotesReceived, undefined, getQuotesReceivedProperties(quoteResponse, warnings)); + } this.messenger.call('BridgeController:stopPollingForQuotes'); const selectedAccount = this.#getMultichainSelectedAccount(accountAddress); @@ -1252,7 +1262,8 @@ export class BridgeStatusController extends StaticIntervalPollingController( eventName: T, txMetaId?: string, From 168b280176bda243467d197f339d5b0dd6424b7e Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 09:51:42 -0800 Subject: [PATCH 03/17] feat: export QuoteWarning type --- packages/bridge-controller/src/index.ts | 1 + .../bridge-controller/src/utils/metrics/properties.ts | 4 ++-- packages/bridge-controller/src/utils/metrics/types.ts | 9 ++++++++- .../src/bridge-status-controller.ts | 3 ++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index e9aa19f9c5e..21a678b8940 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -13,6 +13,7 @@ export type { RequestMetadata, TxStatusData, QuoteFetchData, + QuoteWarning, } from './utils/metrics/types'; export { diff --git a/packages/bridge-controller/src/utils/metrics/properties.ts b/packages/bridge-controller/src/utils/metrics/properties.ts index 114e48704de..0e0181d2d17 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.ts @@ -1,7 +1,7 @@ import type { AccountsControllerState } from '@metamask/accounts-controller'; import { MetricsSwapType } from './constants'; -import type { InputKeys, InputValues, RequestParams } from './types'; +import type { InputKeys, InputValues, QuoteWarning, RequestParams } from './types'; import { DEFAULT_BRIDGE_CONTROLLER_STATE } from '../../constants/bridge'; import type { QuoteMetadata, QuoteResponse, TxData } from '../../types'; import { @@ -117,7 +117,7 @@ export const isCustomSlippage = (slippage: GenericQuoteRequest['slippage']) => { export const getQuotesReceivedProperties = ( activeQuote: QuoteResponse & Partial, - warnings: string[] = [], + warnings: QuoteWarning[] = [], recommendedQuote?: QuoteResponse & Partial, ) => { const provider =activeQuote ? formatProviderLabel(activeQuote.quote) : '_' diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index 829070da14e..23441a763db 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -70,6 +70,13 @@ export type InputValues = { slippage: number; }; +export type QuoteWarning = + | 'low_return' + | 'no_quotes' + | 'insufficient_gas_balance' + | 'insufficient_gas_for_selected_quote' + | 'insufficient_balance'; + /** * Properties that are required to be provided when trackUnifiedSwapBridgeEvent is called */ @@ -104,7 +111,7 @@ export type RequiredEventContextFromClient = { token_symbol_destination: RequestParams['token_symbol_destination']; }; [UnifiedSwapBridgeEventName.QuotesReceived]: TradeData & { - warnings: string[]; // TODO standardize warnings + warnings: QuoteWarning[]; best_quote_provider: QuoteFetchData['best_quote_provider']; price_impact: QuoteFetchData['price_impact']; can_submit: QuoteFetchData['can_submit']; diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 1592f2eee97..9305afe6774 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -7,6 +7,7 @@ import { type QuoteResponse, type Trade, getQuotesReceivedProperties, + QuoteWarning, } from '@metamask/bridge-controller'; import { formatChainIdToHex, @@ -1042,7 +1043,7 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata, isStxEnabledOnClient: boolean, isLoading?: boolean, - warnings?: string[], + warnings?: QuoteWarning[], ): Promise> => { // If trade is submitted before all quotes are loaded, publish QuotesReceived event if(isLoading) { From ca16f8fd40316af828ed72ded18a980e7cf639b0 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 10:02:09 -0800 Subject: [PATCH 04/17] chore: add default values --- .../bridge-status-controller/src/bridge-status-controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 9305afe6774..bcf314e3676 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1042,8 +1042,8 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata, isStxEnabledOnClient: boolean, - isLoading?: boolean, - warnings?: QuoteWarning[], + isLoading: boolean=false, + warnings: QuoteWarning[]=[], ): Promise> => { // If trade is submitted before all quotes are loaded, publish QuotesReceived event if(isLoading) { From babd23f15bd12cf00b7e79054221a05b9be03e6c Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 10:21:33 -0800 Subject: [PATCH 05/17] fix: allow null quote --- packages/bridge-controller/src/utils/metrics/properties.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/utils/metrics/properties.ts b/packages/bridge-controller/src/utils/metrics/properties.ts index 0e0181d2d17..c276656404c 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.ts @@ -116,9 +116,9 @@ export const isCustomSlippage = (slippage: GenericQuoteRequest['slippage']) => { }; export const getQuotesReceivedProperties = ( - activeQuote: QuoteResponse & Partial, + activeQuote: null |QuoteResponse & Partial, warnings: QuoteWarning[] = [], - recommendedQuote?: QuoteResponse & Partial, + recommendedQuote?: null | QuoteResponse & Partial, ) => { const provider =activeQuote ? formatProviderLabel(activeQuote.quote) : '_' return { From 1430a8cec0f15f8e845b2bfcd1a6db3310582151 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 11:38:19 -0800 Subject: [PATCH 06/17] fix: lint errors --- .../src/utils/metrics/properties.ts | 17 ++++++++++++----- .../src/bridge-status-controller.ts | 17 ++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/bridge-controller/src/utils/metrics/properties.ts b/packages/bridge-controller/src/utils/metrics/properties.ts index c276656404c..97ce7b66d37 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.ts @@ -1,7 +1,12 @@ import type { AccountsControllerState } from '@metamask/accounts-controller'; import { MetricsSwapType } from './constants'; -import type { InputKeys, InputValues, QuoteWarning, RequestParams } from './types'; +import type { + InputKeys, + InputValues, + QuoteWarning, + RequestParams, +} from './types'; import { DEFAULT_BRIDGE_CONTROLLER_STATE } from '../../constants/bridge'; import type { QuoteMetadata, QuoteResponse, TxData } from '../../types'; import { @@ -116,11 +121,11 @@ export const isCustomSlippage = (slippage: GenericQuoteRequest['slippage']) => { }; export const getQuotesReceivedProperties = ( - activeQuote: null |QuoteResponse & Partial, + activeQuote: null | (QuoteResponse & Partial), warnings: QuoteWarning[] = [], - recommendedQuote?: null | QuoteResponse & Partial, + recommendedQuote?: null | (QuoteResponse & Partial), ) => { - const provider =activeQuote ? formatProviderLabel(activeQuote.quote) : '_' + const provider = activeQuote ? formatProviderLabel(activeQuote.quote) : '_'; return { can_submit: true, gas_included: Boolean(activeQuote?.quote?.gasIncluded), @@ -130,7 +135,9 @@ export const getQuotesReceivedProperties = ( : 0, usd_quoted_gas: Number(activeQuote?.gasFee?.effective?.usd ?? 0), usd_quoted_return: Number(activeQuote?.toTokenAmount?.usd ?? 0), - best_quote_provider: recommendedQuote ? formatProviderLabel(recommendedQuote.quote) : provider, + best_quote_provider: recommendedQuote + ? formatProviderLabel(recommendedQuote.quote) + : provider, provider, warnings, price_impact: Number(activeQuote?.quote.priceData?.priceImpact ?? 0), diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index bcf314e3676..b4ee5084919 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1,13 +1,13 @@ import type { AccountsControllerState } from '@metamask/accounts-controller'; import type { StateMetadata } from '@metamask/base-controller'; -import { +import type { QuoteWarning } from '@metamask/bridge-controller'; +import { type QuoteMetadata, type RequiredEventContextFromClient, type TxData, type QuoteResponse, type Trade, getQuotesReceivedProperties, - QuoteWarning, } from '@metamask/bridge-controller'; import { formatChainIdToHex, @@ -67,7 +67,6 @@ import { getEVMTxPropertiesFromTransactionMeta, getTxStatusesFromHistory, getPreConfirmationPropertiesFromQuote, - } from './utils/metrics'; import { findAndUpdateTransactionsInBatch, @@ -1042,12 +1041,16 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata, isStxEnabledOnClient: boolean, - isLoading: boolean=false, - warnings: QuoteWarning[]=[], + isLoading: boolean = false, + warnings: QuoteWarning[] = [], ): Promise> => { // If trade is submitted before all quotes are loaded, publish QuotesReceived event - if(isLoading) { - this.#trackUnifiedSwapBridgeEvent(UnifiedSwapBridgeEventName.QuotesReceived, undefined, getQuotesReceivedProperties(quoteResponse, warnings)); + if (isLoading) { + this.#trackUnifiedSwapBridgeEvent( + UnifiedSwapBridgeEventName.QuotesReceived, + undefined, + getQuotesReceivedProperties(quoteResponse, warnings), + ); } this.messenger.call('BridgeController:stopPollingForQuotes'); From 0d0a442be7855c8f18e6d4bd98cd9fad48d2bee5 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 11:43:27 -0800 Subject: [PATCH 07/17] fix: bridge-controller tests --- .../src/__snapshots__/bridge-controller.test.ts.snap | 4 ++-- packages/bridge-controller/src/bridge-controller.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) 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 1abf5ffb20e..06d48ac4790 100644 --- a/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap +++ b/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap @@ -403,7 +403,7 @@ Array [ "usd_quoted_gas": 0, "usd_quoted_return": 100, "warnings": Array [ - "warning1", + "insufficient_balance", ], }, ], @@ -494,7 +494,7 @@ Array [ "usd_quoted_gas": 0, "usd_quoted_return": 100, "warnings": Array [ - "warning1", + "low_return", ], }, ], diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 03ce729f772..f5ec35b1ac9 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -1050,7 +1050,7 @@ describe('BridgeController', function () { bridgeController.trackUnifiedSwapBridgeEvent( UnifiedSwapBridgeEventName.QuotesReceived, { - warnings: ['warning1'], + warnings: ['low_return'], usd_quoted_gas: 0, gas_included: false, gas_included_7702: false, @@ -2267,7 +2267,7 @@ describe('BridgeController', function () { bridgeController.trackUnifiedSwapBridgeEvent( UnifiedSwapBridgeEventName.QuotesReceived, { - warnings: ['warning1'], + warnings: ['insufficient_balance'], usd_quoted_gas: 0, gas_included: false, gas_included_7702: false, @@ -2569,7 +2569,7 @@ describe('BridgeController', function () { bridgeController.trackUnifiedSwapBridgeEvent( UnifiedSwapBridgeEventName.QuotesReceived, { - warnings: ['warning1'], + warnings: ['low_return'], usd_quoted_gas: 0, gas_included: false, gas_included_7702: false, From 54708ea9d205c2e1fba11c7e31b319c7337e06b5 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 11:54:17 -0800 Subject: [PATCH 08/17] fix: unit tests --- .../bridge-status-controller.test.ts.snap | 29 +++++++++++++++---- .../src/bridge-status-controller.test.ts | 5 +++- 2 files changed, 28 insertions(+), 6 deletions(-) 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 450ef55a439..7755ebe12b3 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 @@ -1081,7 +1081,7 @@ Array [ ] `; -exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 1`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions and publish QuotesReceived event if quotes are still loading 1`] = ` Object { "batchId": "batchId1", "chainId": "0xa4b1", @@ -1105,7 +1105,7 @@ Object { } `; -exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 2`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions and publish QuotesReceived event if quotes are still loading 2`] = ` Object { "account": "0xaccount1", "approvalTxId": undefined, @@ -1227,7 +1227,7 @@ Object { } `; -exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 3`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions and publish QuotesReceived event if quotes are still loading 3`] = ` Array [ Array [ Object { @@ -1245,7 +1245,7 @@ Array [ ] `; -exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 4`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions and publish QuotesReceived event if quotes are still loading 4`] = ` Array [ Array [ Object { @@ -1279,8 +1279,27 @@ Array [ ] `; -exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 5`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions and publish QuotesReceived event if quotes are still loading 5`] = ` Array [ + Array [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Quotes Received", + Object { + "action_type": "swapbridge-v1", + "best_quote_provider": "lifi_across", + "can_submit": true, + "gas_included": false, + "gas_included_7702": false, + "price_impact": 0, + "provider": "lifi_across", + "quoted_time_minutes": 0.25, + "usd_quoted_gas": 2.5778, + "usd_quoted_return": 0.134214, + "warnings": Array [ + "low_return", + ], + }, + ], Array [ "BridgeController:stopPollingForQuotes", ], 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 640027d8c45..e8ab1b7553b 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -2559,7 +2559,8 @@ describe('BridgeStatusController', () => { expect(mockMessengerCall.mock.calls).toMatchSnapshot(); }); - it('should handle smart transactions', async () => { + it('should handle smart transactions and publish QuotesReceived event if quotes are still loading', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // track QuotesReceived event setupEventTrackingMocks(mockMessengerCall); setupBridgeStxMocks(mockMessengerCall); addTransactionBatchFn.mockResolvedValueOnce({ @@ -2573,6 +2574,8 @@ describe('BridgeStatusController', () => { (quoteWithoutApproval.trade as TxData).from, quoteWithoutApproval, true, + true, + ['low_return'], ); controller.stopAllPolling(); From 4269d76464f7aaa569dc053121e49ec51c4e45ac Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 12:00:09 -0800 Subject: [PATCH 09/17] chore: changelog --- packages/bridge-controller/CHANGELOG.md | 4 ++++ packages/bridge-status-controller/CHANGELOG.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index c71d9d3c227..2f5d85dd977 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add and export `getQuotesReceivedProperties` utility to build the metrics payload for clients ([#7182](https://github.com/MetaMask/core/pull/7182)) + ## [61.0.0] ### Changed diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index cb5ce316535..bb9d81841ed 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Update `submitTx` handler to accept optional `isLoading` and `warnings` arguments. When `isLoading=true`, the QuotesReceived event is published ([#7182](https://github.com/MetaMask/core/pull/7182)) + ## [61.0.0] ### Changed From 35e17d559c8359251e7f83c468917ed69dffcb28 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 12:06:03 -0800 Subject: [PATCH 10/17] chore: add BTC fee error test --- .../src/bridge-controller.test.ts | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index f5ec35b1ac9..b0d0b614153 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -2101,6 +2101,99 @@ describe('BridgeController', function () { expect(quotes[1].nonEvmFeesInNative).toBe('0.00005'); // BTC fee as-is }); + it('should catch BTC chain fees errors and return undefined fees', async () => { + jest.useFakeTimers(); + // Use the actual Solana mock which already has string trade type + const btcQuoteResponse = mockBridgeQuotesSolErc20.map((quote) => ({ + ...quote, + quote: { + ...quote.quote, + srcChainId: ChainId.BTC, + }, + })) as unknown as QuoteResponse[]; + + messengerMock.call.mockImplementation( + ( + ...args: Parameters + ): ReturnType => { + const [actionType, params] = args; + + if (actionType === 'AccountsController:getAccountByAddress') { + return { + type: 'btc:p2wpkh', + id: 'btc-account-1', + scopes: [BtcScope.Mainnet], + methods: [], + address: 'bc1q...', + metadata: { + name: 'BTC Account 1', + importTime: 1717334400, + keyring: { + type: 'Snap Keyring', + }, + snap: { + id: 'btc-snap-id', + name: 'BTC Snap', + }, + }, + } as never; + } + + if (actionType === 'SnapController:handleRequest') { + return new Promise((_, reject) => { + reject(new Error('Failed to compute fees')); + }); + } + + return { + provider: jest.fn() as never, + } as never; + }, + ); + + jest.spyOn(fetchUtils, 'fetchBridgeQuotes').mockResolvedValue({ + quotes: btcQuoteResponse, + validationFailures: [], + }); + + const quoteParams = { + srcChainId: ChainId.BTC.toString(), + destChainId: '1', + srcTokenAddress: 'NATIVE', + destTokenAddress: '0x0000000000000000000000000000000000000000', + srcTokenAmount: '100000', // satoshis + walletAddress: 'bc1q...', + destWalletAddress: '0x5342', + slippage: 0.5, + }; + + await bridgeController.updateBridgeQuoteRequestParams( + quoteParams, + metricsContext, + ); + + // Wait for polling to start + jest.advanceTimersByTime(201); + await flushPromises(); + + // Wait for fetch to trigger + jest.advanceTimersByTime(295); + await flushPromises(); + + // Wait for fetch to complete + jest.advanceTimersByTime(2601); + await flushPromises(); + + // Final wait for fee calculation + jest.advanceTimersByTime(100); + await flushPromises(); + + const { quotes } = bridgeController.state; + expect(quotes).toHaveLength(2); // mockBridgeQuotesSolErc20 has 2 quotes + expect(quotes[0].nonEvmFeesInNative).toBeUndefined(); + expect(quotes[1].nonEvmFeesInNative).toBeUndefined(); + }); + describe('trackUnifiedSwapBridgeEvent client-side calls', () => { beforeEach(async () => { jest.clearAllMocks(); From be867e25fd997b7c93ab7d7c6b77b9a7fd44f909 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 12:24:55 -0800 Subject: [PATCH 11/17] test: add 7702 quote test --- .../bridge-controller/src/selectors.test.ts | 107 +++++++++++++++++- 1 file changed, 105 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/selectors.test.ts b/packages/bridge-controller/src/selectors.test.ts index e46d2e843a8..e77efb045f8 100644 --- a/packages/bridge-controller/src/selectors.test.ts +++ b/packages/bridge-controller/src/selectors.test.ts @@ -427,6 +427,7 @@ describe('Bridge Selectors', () => { amount: string; asset: Pick; }, + gasIncluded7702?: boolean, ): BridgeAppState => { const chainId = 56; const currencyRates = { @@ -446,6 +447,10 @@ describe('Bridge Selectors', () => { price: '1', currency: 'BNB', }, + '0x0000000000000000000000000000000000000001': { + price: '1.5498387253001357', + currency: 'BNB', + }, }, } as unknown as Record>; const srcTokenAmount = new BigNumber('10') // $10 worth of src token @@ -473,8 +478,8 @@ describe('Bridge Selectors', () => { }, txFee, }, - gasIncluded: Boolean(txFee), - gasIncluded7702: false, + gasIncluded: Boolean(txFee) && !gasIncluded7702, + gasIncluded7702: Boolean(gasIncluded7702), srcTokenAmount, destTokenAmount: new BigNumber('9') .dividedBy(marketData['0x38'][destAsset.address].price) @@ -875,6 +880,104 @@ describe('Bridge Selectors', () => { } `); }); + + it('when gas is included and is taken from dest token', () => { + const newState = getMockSwapState( + { + address: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', + decimals: 18, + assetId: + 'eip155:1/erc20:0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', + }, + { + address: '0x0000000000000000000000000000000000000001', + decimals: 18, + assetId: + 'eip155:1/erc20:0x0000000000000000000000000000000000000001', + }, + { + amount: '1000000000000000000', + asset: { + address: + 'eip155:1/erc20:0x0000000000000000000000000000000000000001', + decimals: 18, + assetId: + 'eip155:1/erc20:0x0000000000000000000000000000000000000001', + }, + }, + true, + ); + + const { sortedQuotes } = selectBridgeQuotes(newState, mockClientParams); + + const { + quote, + trade, + approval, + estimatedProcessingTimeInSeconds, + ...quoteMetadata + } = sortedQuotes[0]; + expect(quoteMetadata).toMatchInlineSnapshot(` + Object { + "adjustedReturn": Object { + "usd": "10.518641979781876096240273601395823616", + "valueInCurrency": "8.999999999999999949780980627632791914", + }, + "cost": Object { + "usd": "1.168737997753541853682403691190760358912", + "valueInCurrency": "1.000000000000000050216414294183215375298", + }, + "gasFee": Object { + "effective": Object { + "amount": "0.000008087", + "usd": "0.00521708544", + "valueInCurrency": "0.00446386226", + }, + "max": Object { + "amount": "0.000016174", + "usd": "0.01043417088", + "valueInCurrency": "0.00892772452", + }, + "total": Object { + "amount": "0.000008087", + "usd": "0.00521708544", + "valueInCurrency": "0.00446386226", + }, + }, + "includedTxFees": Object { + "amount": "1", + "usd": "999.831958465623542784", + "valueInCurrency": "855.479979591168903686", + }, + "minToTokenAmount": Object { + "amount": "0.009994389353314869", + "usd": "9.992709880792782241436661998044855296", + "valueInCurrency": "8.549999999999999909517932616692707134", + }, + "sentAmount": Object { + "amount": "11.689344272882887843", + "usd": "11.687379977535417949922677292586583974912", + "valueInCurrency": "9.999999999999999999997394921816007289298", + }, + "swapRate": "0.00089999999999999999", + "toTokenAmount": Object { + "amount": "0.010520409845594599", + "usd": "10.518641979781876096240273601395823616", + "valueInCurrency": "8.999999999999999949780980627632791914", + }, + "totalMaxNetworkFee": Object { + "amount": "0.000016174", + "usd": "0.01043417088", + "valueInCurrency": "0.00892772452", + }, + "totalNetworkFee": Object { + "amount": "0.000008087", + "usd": "0.00521708544", + "valueInCurrency": "0.00446386226", + }, + } + `); + }); }); it('should only fetch quotes once if balance is insufficient', () => { From 057c2d31879512a5e333624b5b3a2e0a86015156 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 12:33:16 -0800 Subject: [PATCH 12/17] chore: add submittable param to util --- packages/bridge-controller/src/utils/metrics/properties.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/utils/metrics/properties.ts b/packages/bridge-controller/src/utils/metrics/properties.ts index 97ce7b66d37..b7948db6a2c 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.ts @@ -122,12 +122,13 @@ export const isCustomSlippage = (slippage: GenericQuoteRequest['slippage']) => { export const getQuotesReceivedProperties = ( activeQuote: null | (QuoteResponse & Partial), + isSubmittable: boolean = true, warnings: QuoteWarning[] = [], recommendedQuote?: null | (QuoteResponse & Partial), ) => { const provider = activeQuote ? formatProviderLabel(activeQuote.quote) : '_'; return { - can_submit: true, + can_submit: isSubmittable, gas_included: Boolean(activeQuote?.quote?.gasIncluded), gas_included_7702: Boolean(activeQuote?.quote?.gasIncluded7702), quoted_time_minutes: activeQuote?.estimatedProcessingTimeInSeconds From 78b7d9ed439160f30f1d343aff86d6a3edd65850 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 12:34:12 -0800 Subject: [PATCH 13/17] fix: unit tests --- .../src/bridge-controller.test.ts | 6 +- .../bridge-controller/src/selectors.test.ts | 2 +- .../src/utils/metrics/properties.test.ts | 83 +++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index b0d0b614153..e5f20e0d06c 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -2116,7 +2116,7 @@ describe('BridgeController', function () { ( ...args: Parameters ): ReturnType => { - const [actionType, params] = args; + const [actionType] = args; if (actionType === 'AccountsController:getAccountByAddress') { return { @@ -2140,8 +2140,8 @@ describe('BridgeController', function () { } if (actionType === 'SnapController:handleRequest') { - return new Promise((_, reject) => { - reject(new Error('Failed to compute fees')); + return new Promise((_resolve, reject) => { + reject(new Error('Failed to compute fees')); }); } diff --git a/packages/bridge-controller/src/selectors.test.ts b/packages/bridge-controller/src/selectors.test.ts index e77efb045f8..bc82a628366 100644 --- a/packages/bridge-controller/src/selectors.test.ts +++ b/packages/bridge-controller/src/selectors.test.ts @@ -881,7 +881,7 @@ describe('Bridge Selectors', () => { `); }); - it('when gas is included and is taken from dest token', () => { + it('when gasIncluded7702=true and is taken from dest token', () => { const newState = getMockSwapState( { address: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', diff --git a/packages/bridge-controller/src/utils/metrics/properties.test.ts b/packages/bridge-controller/src/utils/metrics/properties.test.ts index 9dce6cc4de3..22263c37a52 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.test.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.test.ts @@ -8,6 +8,7 @@ import { getSwapTypeFromQuote, formatProviderLabel, getRequestParams, + getQuotesReceivedProperties, } from './properties'; import type { QuoteResponse } from '../../types'; import { getNativeAssetForChainId } from '../bridge'; @@ -293,4 +294,86 @@ describe('properties', () => { }); }); }); + + describe('getQuotesReceivedProperties', () => { + it('should return quotes received properties correctly', () => { + const mockQuoteResponse: QuoteResponse = { + quote: { + requestId: 'request1', + srcChainId: 1, + srcAsset: { + chainId: 1, + address: '0x123', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + assetId: 'eip155:1/slip44:60', + }, + srcTokenAmount: '1000000000000000000', + destChainId: 1, + destAsset: { + chainId: 1, + address: '0x456', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + assetId: 'eip155:1/erc20:0x456', + }, + destTokenAmount: '1000000', + minDestTokenAmount: '950000', + feeData: { + metabridge: { + amount: '10000000000000000', + asset: { + chainId: 1, + address: '0x123', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + assetId: 'eip155:1/slip44:60', + }, + }, + }, + bridgeId: 'bridge1', + bridges: ['bridge1'], + steps: [], + }, + trade: { + chainId: 1, + to: '0x789', + from: '0xabc', + value: '0', + data: '0x', + gasLimit: 100000, + }, + estimatedProcessingTimeInSeconds: 60, + }; + + const result = getQuotesReceivedProperties(mockQuoteResponse, false, [], { + ...mockQuoteResponse, + quote: { + ...mockQuoteResponse.quote, + bridges: ['bridge2'], + bridgeId: 'bridge2', + }, + }); + + expect(result).toMatchInlineSnapshot( + ` + Object { + "best_quote_provider": "bridge2_bridge2", + "can_submit": false, + "gas_included": false, + "gas_included_7702": false, + "price_impact": 0, + "provider": "bridge1_bridge1", + "quoted_time_minutes": 1, + "usd_quoted_gas": 0, + "usd_quoted_return": 0, + "warnings": Array [], + } + `, + ); + }); + }); }); From c32e31f45bccd6476518a33c782660bae8c68077 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 13:22:35 -0800 Subject: [PATCH 14/17] fix: reorder params --- packages/bridge-controller/src/utils/metrics/properties.test.ts | 2 +- packages/bridge-controller/src/utils/metrics/properties.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/utils/metrics/properties.test.ts b/packages/bridge-controller/src/utils/metrics/properties.test.ts index 22263c37a52..203ef200762 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.test.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.test.ts @@ -349,7 +349,7 @@ describe('properties', () => { estimatedProcessingTimeInSeconds: 60, }; - const result = getQuotesReceivedProperties(mockQuoteResponse, false, [], { + const result = getQuotesReceivedProperties(mockQuoteResponse, [], false,{ ...mockQuoteResponse, quote: { ...mockQuoteResponse.quote, diff --git a/packages/bridge-controller/src/utils/metrics/properties.ts b/packages/bridge-controller/src/utils/metrics/properties.ts index b7948db6a2c..65fcc37dc19 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.ts @@ -122,8 +122,8 @@ export const isCustomSlippage = (slippage: GenericQuoteRequest['slippage']) => { export const getQuotesReceivedProperties = ( activeQuote: null | (QuoteResponse & Partial), - isSubmittable: boolean = true, warnings: QuoteWarning[] = [], + isSubmittable: boolean = true, recommendedQuote?: null | (QuoteResponse & Partial), ) => { const provider = activeQuote ? formatProviderLabel(activeQuote.quote) : '_'; From f902fded6529e9b26672b85c8495130788d04618 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 16:16:49 -0800 Subject: [PATCH 15/17] fix: lint error --- packages/bridge-controller/src/utils/metrics/properties.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/utils/metrics/properties.test.ts b/packages/bridge-controller/src/utils/metrics/properties.test.ts index 203ef200762..fd47f58f684 100644 --- a/packages/bridge-controller/src/utils/metrics/properties.test.ts +++ b/packages/bridge-controller/src/utils/metrics/properties.test.ts @@ -349,7 +349,7 @@ describe('properties', () => { estimatedProcessingTimeInSeconds: 60, }; - const result = getQuotesReceivedProperties(mockQuoteResponse, [], false,{ + const result = getQuotesReceivedProperties(mockQuoteResponse, [], false, { ...mockQuoteResponse, quote: { ...mockQuoteResponse.quote, From 312213f52464a8d8fad41de176590aad1787361b Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 16:29:01 -0800 Subject: [PATCH 16/17] chore: tx_alert --- packages/bridge-controller/src/utils/metrics/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index 23441a763db..8a1d2be85ed 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -75,7 +75,8 @@ export type QuoteWarning = | 'no_quotes' | 'insufficient_gas_balance' | 'insufficient_gas_for_selected_quote' - | 'insufficient_balance'; + | 'insufficient_balance' + | 'tx_alert'; /** * Properties that are required to be provided when trackUnifiedSwapBridgeEvent is called From 89bb4b56c412d32fc98da2bab5b5b461a57e6dc1 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Nov 2025 16:30:01 -0800 Subject: [PATCH 17/17] chore: price_impact --- packages/bridge-controller/src/utils/metrics/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/bridge-controller/src/utils/metrics/types.ts b/packages/bridge-controller/src/utils/metrics/types.ts index 8a1d2be85ed..1504c332063 100644 --- a/packages/bridge-controller/src/utils/metrics/types.ts +++ b/packages/bridge-controller/src/utils/metrics/types.ts @@ -76,6 +76,7 @@ export type QuoteWarning = | 'insufficient_gas_balance' | 'insufficient_gas_for_selected_quote' | 'insufficient_balance' + | 'price_impact' | 'tx_alert'; /**