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-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< 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 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..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 @@ -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", @@ -735,7 +741,7 @@ Array [ ], Array [ "AccountsController:getAccountByAddress", - "", + "0xaccount1", ], Array [ "BridgeController:trackUnifiedSwapBridgeEvent", @@ -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", @@ -2061,6 +2085,9 @@ Array [ exports[`BridgeStatusController submitTx: EVM swap should handle smart accounts (4337) 4`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -2324,6 +2351,9 @@ Array [ exports[`BridgeStatusController submitTx: EVM swap should handle smart transactions 5`] = ` Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -2548,6 +2578,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 +2822,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 +2887,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..904dc7a4cb2 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({ @@ -1675,10 +1678,10 @@ describe('BridgeStatusController', () => { }); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); - mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); }; it('should successfully submit an EVM bridge transaction with approval', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes setupApprovalMocks(); setupBridgeMocks(); @@ -1704,6 +1707,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 +1752,7 @@ describe('BridgeStatusController', () => { }); it('should handle smart transactions', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes setupBridgeMocks(); const { controller, startPollingForBridgeTxStatusSpy } = @@ -1774,6 +1779,7 @@ describe('BridgeStatusController', () => { }); it('should handle smart accounts (4337)', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes mockMessengerCall.mockReturnValueOnce({ ...mockSelectedAccount, type: EthAccountType.Erc4337, @@ -1791,7 +1797,10 @@ describe('BridgeStatusController', () => { mockMessengerCall.mockReturnValueOnce({ transactions: [mockEvmTxMeta], }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockReturnValueOnce({ + ...mockSelectedAccount, + type: EthAccountType.Erc4337, + }); const { controller, startPollingForBridgeTxStatusSpy } = getController(mockMessengerCall); @@ -1836,6 +1845,7 @@ describe('BridgeStatusController', () => { }); it('should reset USDT allowance', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes mockIsEthUsdt.mockReturnValueOnce(true); // USDT approval reset @@ -1870,6 +1880,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 +1903,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 +1934,7 @@ describe('BridgeStatusController', () => { }); it('should delay after submitting linea approval', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes const handleLineaDelaySpy = jest .spyOn(transactionUtils, 'handleLineaDelay') .mockResolvedValueOnce(); @@ -2080,10 +2093,10 @@ describe('BridgeStatusController', () => { }); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); - mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); }; it('should successfully submit an EVM swap transaction with approval', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes setupApprovalMocks(); setupBridgeMocks(); @@ -2109,6 +2122,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 +2167,7 @@ describe('BridgeStatusController', () => { }); it('should handle smart transactions', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes setupBridgeMocks(); const { controller, startPollingForBridgeTxStatusSpy } = @@ -2179,6 +2194,7 @@ describe('BridgeStatusController', () => { }); it('should handle smart accounts (4337)', async () => { + mockMessengerCall.mockImplementationOnce(jest.fn()); // BridgeController:stopPollingForQuotes mockMessengerCall.mockReturnValueOnce({ ...mockSelectedAccount, type: EthAccountType.Erc4337, 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;