From 2a0553ae443807b226476e6650d82f84949e8e35 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Jun 2025 16:46:19 -0700 Subject: [PATCH 1/5] feat: add stopPollingForQuotes handler to bridge-controller --- packages/bridge-controller/src/bridge-controller.ts | 12 ++++++++++-- packages/bridge-controller/src/types.ts | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 70cdd9fd01b..c8af6a51455 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -231,6 +231,10 @@ export class BridgeController extends StaticIntervalPollingController { @@ -413,9 +417,13 @@ export class BridgeController extends StaticIntervalPollingController { + stopPollingForQuotes = (reason?: string) => { this.stopAllPolling(); - this.#abortController?.abort(RESET_STATE_ABORT_MESSAGE); + this.#abortController?.abort(reason); + }; + + resetState = () => { + this.stopPollingForQuotes(RESET_STATE_ABORT_MESSAGE); this.update((state) => { // Cannot do direct assignment to state, i.e. state = {... }, need to manually assign each field diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 699c498f214..0ffcd238206 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -348,6 +348,7 @@ export enum BridgeBackgroundAction { RESET_STATE = 'resetState', GET_BRIDGE_ERC20_ALLOWANCE = 'getBridgeERC20Allowance', TRACK_METAMETRICS_EVENT = 'trackUnifiedSwapBridgeEvent', + STOP_POLLING_FOR_QUOTES = 'stopPollingForQuotes', } export type BridgeControllerState = { @@ -382,6 +383,7 @@ export type BridgeControllerActions = | BridgeControllerAction | BridgeControllerAction | BridgeControllerAction + | BridgeControllerAction | BridgeControllerAction; export type BridgeControllerEvents = ControllerStateChangeEvent< From 228e232f332b9f6f3a1d30e81a586b31390907eb Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Jun 2025 16:47:06 -0700 Subject: [PATCH 2/5] chore: stop quote polling when submitTx is called --- .../bridge-status-controller/src/bridge-status-controller.ts | 2 ++ packages/bridge-status-controller/src/types.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 7a6a6ccf775..79be421caa9 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -849,6 +849,8 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata, isStxEnabledOnClient: boolean, ): Promise> => { + this.messagingSystem.call('BridgeController:stopPollingForQuotes'); + let txMeta: (TransactionMeta & Partial) | undefined; const isBridgeTx = isCrossChain( diff --git a/packages/bridge-status-controller/src/types.ts b/packages/bridge-status-controller/src/types.ts index 93366bdcdc7..65cd837b903 100644 --- a/packages/bridge-status-controller/src/types.ts +++ b/packages/bridge-status-controller/src/types.ts @@ -335,6 +335,7 @@ type AllowedActions = | TransactionControllerGetStateAction | BridgeControllerAction | BridgeControllerAction + | BridgeControllerAction | GetGasFeeState | AccountsControllerGetAccountByAddressAction | RemoteFeatureFlagControllerGetStateAction; From fb79d51be2b30298b47df9952301ed8035d5790f Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Jun 2025 17:08:21 -0700 Subject: [PATCH 3/5] chore: update changelog --- packages/bridge-controller/CHANGELOG.md | 4 ++++ packages/bridge-status-controller/CHANGELOG.md | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 2b151037d0c..4bc13bee365 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 `stopPollingForQuotes` handler that stops quote polling without resetting the bridge controller's state ([#5994](https://github.com/MetaMask/core/pull/5994)) + ## [32.2.0] ### Changed diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 3b81fbc9c73..3f688981ee8 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 + +- **BREAKING:** BridgeStatusController now requires the `BridgeController:stopPollingForQuotes` action permission ([#5994](https://github.com/MetaMask/core/pull/5994)) + +### Changed + +- **BREAKING:** Adds a call to bridge-controller's `stopPollingForQuotes` handler to prevent quotes from refreshing during tx submission. This enables "pausing" the quote polling loop without resetting the entire state. Without this, it's possible for the activeQuote to change while the UI's tx submission is in-progress ([#5994](https://github.com/MetaMask/core/pull/5994)) + ## [30.0.0] ### Changed From f2d462b04ef7a0f474288685ab87635c94f587a7 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Jun 2025 17:22:01 -0700 Subject: [PATCH 4/5] fix: unit tests --- .../bridge-status-controller.test.ts.snap | 36 +++++++++++++++++++ .../src/bridge-status-controller.test.ts | 14 ++++++++ 2 files changed, 50 insertions(+) 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 f222057da90..2bf3b423d42 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 @@ -475,6 +475,9 @@ Object { exports[`BridgeStatusController submitTx: EVM bridge should delay after submitting linea approval 3`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -716,6 +719,9 @@ Array [ exports[`BridgeStatusController submitTx: EVM bridge should handle smart accounts (4337) 4`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -979,6 +985,9 @@ Array [ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 5`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -1273,6 +1282,9 @@ Array [ exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 5`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "BridgeController:getBridgeERC20Allowance", "0x0000000000000000000000000000000000000000", @@ -1533,6 +1545,9 @@ Array [ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with approval 4`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -1774,6 +1789,9 @@ Array [ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 5`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -1861,6 +1879,9 @@ Array [ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if approval tx fails 2`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -1902,6 +1923,9 @@ Array [ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if approval tx meta is undefined 2`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -2324,6 +2348,9 @@ Array [ exports[`BridgeStatusController submitTx: EVM swap should handle smart transactions 5`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -2548,6 +2575,9 @@ Array [ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with approval 4`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -2789,6 +2819,9 @@ Array [ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with no approval 5`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -2851,6 +2884,9 @@ Array [ exports[`BridgeStatusController submitTx: Solana should successfully submit a Solana transaction 1`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getSelectedMultichainAccount", ], 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 482cd303aa4..b1183580681 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -1459,6 +1459,7 @@ describe('BridgeStatusController', () => { }); it('should successfully submit a Solana transaction', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes mockMessengerCall.mockReturnValueOnce(mockSolanaAccount); mockMessengerCall.mockResolvedValueOnce('signature'); @@ -1482,6 +1483,7 @@ describe('BridgeStatusController', () => { }); it('should throw error when snap ID is missing', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // stopPollingForQuotes const accountWithoutSnap = { ...mockSelectedAccount, metadata: { snap: undefined }, @@ -1514,6 +1516,7 @@ describe('BridgeStatusController', () => { }); it('should handle snap controller errors', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes mockMessengerCall.mockReturnValueOnce(mockSolanaAccount); // Mock the RemoteFeatureFlagController:getState call that happens in getBridgeFeatureFlags mockMessengerCall.mockReturnValueOnce({ @@ -1679,6 +1682,7 @@ describe('BridgeStatusController', () => { }; it('should successfully submit an EVM bridge transaction with approval', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes setupApprovalMocks(); setupBridgeMocks(); @@ -1704,6 +1708,7 @@ describe('BridgeStatusController', () => { }); it('should successfully submit an EVM bridge transaction with no approval', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes setupBridgeMocks(); const { controller, startPollingForBridgeTxStatusSpy } = @@ -1748,6 +1753,7 @@ describe('BridgeStatusController', () => { }); it('should handle smart transactions', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes setupBridgeMocks(); const { controller, startPollingForBridgeTxStatusSpy } = @@ -1774,6 +1780,7 @@ describe('BridgeStatusController', () => { }); it('should handle smart accounts (4337)', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes mockMessengerCall.mockReturnValueOnce({ ...mockSelectedAccount, type: EthAccountType.Erc4337, @@ -1836,6 +1843,7 @@ describe('BridgeStatusController', () => { }); it('should reset USDT allowance', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes mockIsEthUsdt.mockReturnValueOnce(true); // USDT approval reset @@ -1870,6 +1878,7 @@ describe('BridgeStatusController', () => { }); it('should throw an error if approval tx fails', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); mockMessengerCall.mockReturnValueOnce('arbitrum-client-id'); mockMessengerCall.mockReturnValueOnce({ @@ -1892,6 +1901,7 @@ describe('BridgeStatusController', () => { }); it('should throw an error if approval tx meta is undefined', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); mockMessengerCall.mockReturnValueOnce('arbitrum-client-id'); mockMessengerCall.mockReturnValueOnce({ @@ -1922,6 +1932,7 @@ describe('BridgeStatusController', () => { }); it('should delay after submitting linea approval', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes const handleLineaDelaySpy = jest .spyOn(transactionUtils, 'handleLineaDelay') .mockResolvedValueOnce(); @@ -2084,6 +2095,7 @@ describe('BridgeStatusController', () => { }; it('should successfully submit an EVM swap transaction with approval', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes setupApprovalMocks(); setupBridgeMocks(); @@ -2109,6 +2121,7 @@ describe('BridgeStatusController', () => { }); it('should successfully submit an EVM swap transaction with no approval', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes setupBridgeMocks(); const { controller, startPollingForBridgeTxStatusSpy } = @@ -2153,6 +2166,7 @@ describe('BridgeStatusController', () => { }); it('should handle smart transactions', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes setupBridgeMocks(); const { controller, startPollingForBridgeTxStatusSpy } = From 7fa0cb2d9d82e98912661de9ad5856e9735194b5 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Jun 2025 18:20:23 -0700 Subject: [PATCH 5/5] fix: unit tests --- .../__snapshots__/bridge-status-controller.test.ts.snap | 5 ++++- .../src/bridge-status-controller.test.ts | 8 +++++--- 2 files changed, 9 insertions(+), 4 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 2bf3b423d42..973089b6dee 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 @@ -741,7 +741,7 @@ Array [ ], Array [ "AccountsController:getAccountByAddress", - "", + "0xaccount1", ], Array [ "BridgeController:trackUnifiedSwapBridgeEvent", @@ -2085,6 +2085,9 @@ Array [ exports[`BridgeStatusController submitTx: EVM swap should handle smart accounts (4337) 4`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", 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 b1183580681..904dc7a4cb2 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -1678,7 +1678,6 @@ describe('BridgeStatusController', () => { }); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); - mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); }; it('should successfully submit an EVM bridge transaction with approval', async () => { @@ -1798,7 +1797,10 @@ describe('BridgeStatusController', () => { mockMessengerCall.mockReturnValueOnce({ transactions: [mockEvmTxMeta], }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockReturnValueOnce({ + ...mockSelectedAccount, + type: EthAccountType.Erc4337, + }); const { controller, startPollingForBridgeTxStatusSpy } = getController(mockMessengerCall); @@ -2091,7 +2093,6 @@ describe('BridgeStatusController', () => { }); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); - mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); }; it('should successfully submit an EVM swap transaction with approval', async () => { @@ -2193,6 +2194,7 @@ describe('BridgeStatusController', () => { }); it('should handle smart accounts (4337)', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes mockMessengerCall.mockReturnValueOnce({ ...mockSelectedAccount, type: EthAccountType.Erc4337,