diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index cbf9fe39139..06b089594a5 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 + +- Refactor `handleLineaDelay` to `handleApprovalDelay` for improved abstraction and add support for Base chain by using an array and `includes` for chain ID checks ([#6674](https://github.com/MetaMask/core/pull/6674)) + ## [44.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 e14f4a0b25e..a7bb4a96e47 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 @@ -604,6 +604,237 @@ Array [ ] `; +exports[`BridgeStatusController submitTx: EVM bridge should delay after submitting base approval 1`] = ` +Object { + "chainId": "0xa4b1", + "hash": "0xevmTxHash", + "id": "test-tx-id", + "status": "unapproved", + "time": 1234567890, + "txParams": Object { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gasLimit": "0x5208", + "to": "0xbridgeContract", + "value": "0x0", + }, + "txReceipt": Object { + "effectiveGasPrice": "0x1880a", + "gasUsed": "0x2c92a", + }, + "type": "bridge", +} +`; + +exports[`BridgeStatusController submitTx: EVM bridge should delay after submitting base approval 2`] = ` +Object { + "account": "0xaccount1", + "approvalTxId": "test-approval-tx-id", + "batchId": undefined, + "estimatedProcessingTimeInSeconds": 15, + "hasApprovalTx": true, + "initialDestAssetBalance": undefined, + "isStxEnabled": false, + "pricingData": Object { + "amountSent": "1.234", + "amountSentInUsd": "1.01", + "quotedGasAmount": ".00055", + "quotedGasInUsd": "2.5778", + "quotedReturnInUsd": "0.134214", + }, + "quote": Object { + "bridgeId": "lifi", + "bridges": Array [ + "across", + ], + "destAsset": Object { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:60", + "chainId": 10, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.63", + "symbol": "ETH", + }, + "destChainId": 10, + "destTokenAmount": "990654755978612", + "feeData": Object { + "metabridge": Object { + "amount": "8750000000000", + "asset": Object { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", + "chainId": 42161, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.7", + "symbol": "ETH", + }, + }, + }, + "minDestTokenAmount": "941000000000000", + "requestId": "197c402f-cb96-4096-9f8c-54aed84ca776", + "srcAsset": Object { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", + "chainId": 42161, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.7", + "symbol": "ETH", + }, + "srcChainId": 8453, + "srcTokenAmount": "991250000000000", + "steps": Array [ + Object { + "action": "bridge", + "destAmount": "990654755978612", + "destAsset": Object { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:60", + "chainId": 10, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.63", + "symbol": "ETH", + }, + "destChainId": 10, + "protocol": Object { + "displayName": "Across", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png", + "name": "across", + }, + "srcAmount": "991250000000000", + "srcAsset": Object { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", + "chainId": 42161, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.7", + "symbol": "ETH", + }, + "srcChainId": 42161, + }, + ], + }, + "slippagePercentage": 0, + "startTime": 1234567890, + "status": Object { + "srcChain": Object { + "chainId": 8453, + "txHash": "0xevmTxHash", + }, + "status": "PENDING", + }, + "targetContractAddress": undefined, + "txMetaId": "test-tx-id", +} +`; + +exports[`BridgeStatusController submitTx: EVM bridge should delay after submitting base approval 3`] = ` +Array [ + Array [ + "BridgeController:stopPollingForQuotes", + ], + Array [ + "AccountsController:getSelectedMultichainAccount", + ], + Array [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Submitted", + Object { + "action_type": "swapbridge-v1", + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:8453", + "custom_slippage": false, + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "price_impact": 0, + "provider": "lifi_across", + "quoted_time_minutes": 0.25, + "stx_enabled": false, + "swap_type": "crosschain", + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", + "usd_amount_source": 1.01, + "usd_quoted_gas": 2.5778, + "usd_quoted_return": 0, + }, + ], + Array [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + Array [ + "NetworkController:findNetworkClientIdByChainId", + "0xa4b1", + ], + Array [ + "GasFeeController:getState", + ], + Array [ + "TransactionController:getState", + ], + Array [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + Array [ + "NetworkController:findNetworkClientIdByChainId", + "0xa4b1", + ], + Array [ + "GasFeeController:getState", + ], + Array [ + "TransactionController:getState", + ], +] +`; + +exports[`BridgeStatusController submitTx: EVM bridge should delay after submitting base approval 4`] = ` +Array [ + Array [ + Object { + "data": Object { + "srcChainId": "eip155:8453", + "stxEnabled": false, + }, + "name": "Bridge Transaction Completed", + }, + [Function], + ], + Array [ + Object { + "data": Object { + "srcChainId": "eip155:8453", + "stxEnabled": false, + }, + "name": "Bridge Transaction Approval Completed", + }, + [Function], + ], +] +`; + exports[`BridgeStatusController submitTx: EVM bridge should delay after submitting linea approval 1`] = ` Object { "chainId": "0xa4b1", 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 242f972bf1e..1c962c19950 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -2466,7 +2466,7 @@ describe('BridgeStatusController', () => { it('should delay after submitting linea approval', async () => { const handleLineaDelaySpy = jest - .spyOn(transactionUtils, 'handleLineaDelay') + .spyOn(transactionUtils, 'handleApprovalDelay') .mockResolvedValueOnce(); const mockTraceFn = jest .fn() @@ -2502,6 +2502,44 @@ describe('BridgeStatusController', () => { expect(mockTraceFn.mock.calls).toMatchSnapshot(); }); + it('should delay after submitting base approval', async () => { + const handleBaseDelaySpy = jest + .spyOn(transactionUtils, 'handleApprovalDelay') + .mockResolvedValueOnce(); + const mockTraceFn = jest + .fn() + .mockImplementation((_p, callback) => callback()); + + setupEventTrackingMocks(mockMessengerCall); + setupApprovalMocks(mockMessengerCall); + setupBridgeMocks(mockMessengerCall); + + const { controller, startPollingForBridgeTxStatusSpy } = getController( + mockMessengerCall, + mockTraceFn, + ); + + const baseQuoteResponse = { + ...mockEvmQuoteResponse, + quote: { ...mockEvmQuoteResponse.quote, srcChainId: 8453 }, + trade: { + ...(mockEvmQuoteResponse.trade as TxData), + gasLimit: undefined, + } as never, + }; + + const result = await controller.submitTx(baseQuoteResponse, false); + controller.stopAllPolling(); + + expect(mockTraceFn).toHaveBeenCalledTimes(2); + expect(handleBaseDelaySpy).toHaveBeenCalledTimes(1); + expect(result).toMatchSnapshot(); + expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); + expect(controller.state.txHistory[result.id]).toMatchSnapshot(); + expect(mockMessengerCall.mock.calls).toMatchSnapshot(); + expect(mockTraceFn.mock.calls).toMatchSnapshot(); + }); + it('should call handleMobileHardwareWalletDelay for hardware wallet on mobile', async () => { const handleMobileHardwareWalletDelaySpy = jest .spyOn(transactionUtils, 'handleMobileHardwareWalletDelay') diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index cfdaeb34912..4feb44d280f 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -69,7 +69,7 @@ import { getClientRequest, getStatusRequestParams, getUSDTAllowanceResetTx, - handleLineaDelay, + handleApprovalDelay, handleMobileHardwareWalletDelay, handleNonEvmTxResponse, generateActionId, @@ -818,7 +818,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { @@ -1238,7 +1238,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }); }); - describe('handleLineaDelay', () => { + describe('handleApprovalDelay', () => { beforeEach(() => { jest.useFakeTimers(); jest.clearAllMocks(); @@ -1271,13 +1271,13 @@ describe('Bridge Status Controller Transaction Utils', () => { } as unknown as QuoteResponse; // Create a promise that will resolve after the delay - const delayPromise = handleLineaDelay(mockQuoteResponse); + const delayPromise = handleApprovalDelay(mockQuoteResponse); // Verify that the timer was set with the correct delay expect(jest.getTimerCount()).toBe(1); // Fast-forward the timer - jest.advanceTimersByTime(LINEA_DELAY_MS); + jest.advanceTimersByTime(APPROVAL_DELAY_MS); // Wait for the promise to resolve await delayPromise; @@ -1286,8 +1286,46 @@ describe('Bridge Status Controller Transaction Utils', () => { expect(jest.getTimerCount()).toBe(0); }); - it('should not delay when source chain is not Linea', async () => { - // Create a minimal mock quote response with a non-Linea source chain + it('should delay when source chain is Base', async () => { + // Create a minimal mock quote response with Base as the source chain + const mockQuoteResponse = { + quote: { + srcChainId: ChainId.BASE, + // Other required properties with minimal values + requestId: 'test-request-id', + srcAsset: { address: '0x123', symbol: 'ETH', decimals: 18 }, + srcTokenAmount: '1000000000000000000', + destChainId: ChainId.ETH, + destAsset: { address: '0x456', symbol: 'ETH', decimals: 18 }, + destTokenAmount: '1000000000000000000', + bridgeId: 'test-bridge', + bridges: ['test-bridge'], + steps: [], + feeData: {}, + }, + // Required properties for QuoteResponse + trade: {} as TxData, + estimatedProcessingTimeInSeconds: 60, + } as unknown as QuoteResponse; + + // Create a promise that will resolve after the delay + const delayPromise = handleApprovalDelay(mockQuoteResponse); + + // Verify that the timer was set with the correct delay + expect(jest.getTimerCount()).toBe(1); + + // Fast-forward the timer + jest.advanceTimersByTime(APPROVAL_DELAY_MS); + + // Wait for the promise to resolve + await delayPromise; + + // Verify that the timer was cleared + expect(jest.getTimerCount()).toBe(0); + }); + + it('should not delay when source chain is not Linea or Base', async () => { + // Create a minimal mock quote response with a non-Linea/Base source chain const mockQuoteResponse = { quote: { srcChainId: ChainId.ETH, @@ -1309,7 +1347,7 @@ describe('Bridge Status Controller Transaction Utils', () => { } as unknown as QuoteResponse; // Create a promise that will resolve after the delay - const delayPromise = handleLineaDelay(mockQuoteResponse); + const delayPromise = handleApprovalDelay(mockQuoteResponse); // Verify that no timer was set expect(jest.getTimerCount()).toBe(0); diff --git a/packages/bridge-status-controller/src/utils/transaction.ts b/packages/bridge-status-controller/src/utils/transaction.ts index 49a420aa417..c9a0968e038 100644 --- a/packages/bridge-status-controller/src/utils/transaction.ts +++ b/packages/bridge-status-controller/src/utils/transaction.ts @@ -27,7 +27,7 @@ import { v4 as uuid } from 'uuid'; import { calculateGasFees } from './gas'; import { createClientTransactionRequest } from './snaps'; import type { TransactionBatchSingleRequest } from '../../../transaction-controller/src/types'; -import { LINEA_DELAY_MS } from '../constants'; +import { APPROVAL_DELAY_MS } from '../constants'; import type { BridgeStatusControllerMessenger, SolanaTransactionMeta, @@ -200,16 +200,16 @@ export const handleNonEvmTxResponse = ( }; }; -export const handleLineaDelay = async ( +export const handleApprovalDelay = async ( quoteResponse: QuoteResponse, ) => { - if (ChainId.LINEA === quoteResponse.quote.srcChainId) { + if ([ChainId.LINEA, ChainId.BASE].includes(quoteResponse.quote.srcChainId)) { const debugLog = createProjectLogger('bridge'); debugLog( - 'Delaying submitting bridge tx to make Linea confirmation more likely', + 'Delaying submitting bridge tx to make Linea and Base confirmation more likely', ); const waitPromise = new Promise((resolve) => - setTimeout(resolve, LINEA_DELAY_MS), + setTimeout(resolve, APPROVAL_DELAY_MS), ); await waitPromise; }