From 2bd6df7921f1ae3f8b92619a4d80e726324a28a9 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 9 Mar 2026 17:37:31 -0700 Subject: [PATCH 01/23] test: 100% unit test coverage --- .../bridge-controller/src/utils/validators.ts | 2 +- .../bridge-status-controller.test.ts.snap | 295 +++- .../bridge-status-controller.intent.test.ts | 1440 ++++++++--------- .../src/bridge-status-controller.intent.ts | 19 +- .../src/bridge-status-controller.test.ts | 107 ++ .../src/bridge-status-controller.ts | 84 +- .../bridge-status-controller/src/types.ts | 2 +- .../src/utils/bridge-status.ts | 4 +- .../src/utils/intent-api.ts | 18 +- .../src/utils/validators.ts | 19 +- 10 files changed, 1092 insertions(+), 898 deletions(-) diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index f2e85cb1bb8..555d6dae3f2 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -339,7 +339,7 @@ export const IntentSchema = type({ /** * Optional settlement contract address used for execution. */ - settlementContract: optional(HexAddressSchema), + settlementContract: HexAddressSchema, /** * Optional EIP-712 typed data payload for signing. 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 29bbc53bad7..2d7201b2f42 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 @@ -2875,6 +2875,121 @@ exports[`BridgeStatusController submitTx: EVM swap should estimate gas when gasI } `; +exports[`BridgeStatusController submitTx: EVM swap should handle a gasless swap transaction with approval 2`] = ` +{ + "account": "0xaccount1", + "actionId": undefined, + "approvalTxId": undefined, + "batchId": "batchId1", + "estimatedProcessingTimeInSeconds": 0, + "featureId": undefined, + "hasApprovalTx": true, + "initialDestAssetBalance": undefined, + "isStxEnabled": true, + "location": "Main View", + "originalTransactionId": "test-tx-id", + "pricingData": { + "amountSent": "1.234", + "amountSentInUsd": "1.01", + "quotedGasAmount": ".00055", + "quotedGasInUsd": "2.5778", + "quotedReturnInUsd": "0.134214", + }, + "quote": { + "bridgeId": "lifi", + "bridges": [ + "across", + ], + "destAsset": { + "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": 42161, + "destTokenAmount": "990654755978612", + "feeData": { + "txFee": { + "maxFeePerGas": "123", + "maxPriorityFeePerGas": "123", + }, + }, + "gasIncluded": true, + "minDestTokenAmount": "941000000000000", + "requestId": "197c402f-cb96-4096-9f8c-54aed84ca776", + "srcAsset": { + "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, + "srcTokenAmount": "991250000000000", + "steps": [ + { + "action": "bridge", + "destAmount": "990654755978612", + "destAsset": { + "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": { + "displayName": "Across", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png", + "name": "across", + }, + "srcAmount": "991250000000000", + "srcAsset": { + "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": { + "srcChain": { + "chainId": 42161, + "txHash": "0xevmTxHash", + }, + "status": "PENDING", + }, + "targetContractAddress": undefined, + "txMetaId": "test-tx-id", +} +`; + exports[`BridgeStatusController submitTx: EVM swap should handle smart transactions 1`] = ` { "batchId": "batchId1", @@ -3267,8 +3382,8 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an "pricingData": { "amountSent": "1.234", "amountSentInUsd": "1.01", - "quotedGasAmount": ".00055", - "quotedGasInUsd": "2.5778", + "quotedGasAmount": undefined, + "quotedGasInUsd": undefined, "quotedReturnInUsd": "0.134214", }, "quote": { @@ -3407,7 +3522,7 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an "token_symbol_destination": "WETH", "token_symbol_source": "ETH", "usd_amount_source": 1.01, - "usd_quoted_gas": 2.5778, + "usd_quoted_gas": 0, "usd_quoted_return": 0, }, ], @@ -3428,6 +3543,26 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an ] `; +exports[`BridgeStatusController submitTx: EVM swap should use batch path when gasIncluded7702 is true regardless of STX setting (with approval) 1`] = ` +{ + "batchId": "batchId1", + "chainId": "0xa4b1", + "hash": "0xevmTxHash", + "id": "test-tx-id", + "status": "unapproved", + "time": 1234567890, + "txParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gasLimit": "0x5208", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "swap", +} +`; + exports[`BridgeStatusController submitTx: EVM swap should use batch path when gasIncluded7702 is true regardless of STX setting 1`] = ` { "batchId": "batchId1", @@ -3481,8 +3616,158 @@ exports[`BridgeStatusController submitTx: EVM swap should use quote txFee when g "location": "Main View", "originalTransactionId": "test-tx-id", "pricingData": { - "amountSent": "1.234", - "amountSentInUsd": "1.01", + "amountSent": "0", + "amountSentInUsd": undefined, + "quotedGasAmount": ".00055", + "quotedGasInUsd": "2.5778", + "quotedReturnInUsd": "0.134214", + }, + "quote": { + "bridgeId": "lifi", + "bridges": [ + "across", + ], + "destAsset": { + "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": 42161, + "destTokenAmount": "990654755978612", + "feeData": { + "metabridge": { + "amount": "8750000000000", + "asset": { + "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", + }, + }, + "txFee": { + "maxFeePerGas": "1395348", + "maxPriorityFeePerGas": "1000001", + }, + }, + "gasIncluded": true, + "gasIncluded7702": false, + "minDestTokenAmount": "941000000000000", + "requestId": "197c402f-cb96-4096-9f8c-54aed84ca776", + "srcAsset": { + "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, + "srcTokenAmount": "991250000000000", + "steps": [ + { + "action": "bridge", + "destAmount": "990654755978612", + "destAsset": { + "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": { + "displayName": "Across", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png", + "name": "across", + }, + "srcAmount": "991250000000000", + "srcAsset": { + "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": { + "srcChain": { + "chainId": 42161, + "txHash": "0xevmTxHash", + }, + "status": "PENDING", + }, + "targetContractAddress": undefined, + "txMetaId": "test-tx-id", +} +`; + +exports[`BridgeStatusController submitTx: EVM swap should use quote txFee when gasIncluded is true and STX is off (undefined gasLimit) 1`] = ` +{ + "chainId": "0xa4b1", + "hash": "0xevmTxHash", + "id": "test-tx-id", + "status": "unapproved", + "time": 1234567890, + "txParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gasLimit": "0x5208", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "swap", +} +`; + +exports[`BridgeStatusController submitTx: EVM swap should use quote txFee when gasIncluded is true and STX is off (undefined gasLimit) 2`] = ` +{ + "account": "0xaccount1", + "actionId": "1234567890.456", + "approvalTxId": undefined, + "batchId": undefined, + "estimatedProcessingTimeInSeconds": 0, + "featureId": undefined, + "hasApprovalTx": false, + "initialDestAssetBalance": undefined, + "isStxEnabled": false, + "location": "Main View", + "originalTransactionId": "test-tx-id", + "pricingData": { + "amountSent": "0", + "amountSentInUsd": undefined, "quotedGasAmount": ".00055", "quotedGasInUsd": "2.5778", "quotedReturnInUsd": "0.134214", diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts index 36045d15d5d..d2b24e94824 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts @@ -1,43 +1,31 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable jest/no-restricted-matchers */ -import { - StatusTypes, - UnifiedSwapBridgeEventName, -} from '@metamask/bridge-controller'; -import type { TransactionMeta } from '@metamask/transaction-controller'; +import { BridgeClientId, StatusTypes } from '@metamask/bridge-controller'; +import type { + GasFeeEstimates, + TransactionMeta, + TransactionParams, +} from '@metamask/transaction-controller'; import { TransactionStatus, TransactionType, } from '@metamask/transaction-controller'; +import { BridgeStatusController } from './bridge-status-controller'; import { MAX_ATTEMPTS } from './constants'; +import type { BridgeStatusControllerState } from './types'; +import * as bridgeStatusUtils from './utils/bridge-status'; +import * as intentApi from './utils/intent-api'; +import * as transactionUtils from './utils/transaction'; import { IntentOrderStatus } from './utils/validators'; -type Tx = Pick & { - type?: TransactionType; - chainId?: string; - hash?: string; - txReceipt?: any; -}; - -const seedIntentHistory = (controller: any): any => { - controller.update((state: any) => { - state.txHistory['order-1'] = { - txMetaId: 'order-1', - originalTransactionId: 'tx1', - quote: { - srcChainId: 1, - destChainId: 1, - intent: { protocol: 'cowswap' }, - }, - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '' }, - }, - attempts: undefined, // IMPORTANT: prevents early return - }; - }); -}; +jest + .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') + .mockImplementation(jest.fn()); +jest + .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') + .mockImplementation(jest.fn()); const minimalIntentQuoteResponse = (overrides?: Partial): any => { return { @@ -47,6 +35,8 @@ const minimalIntentQuoteResponse = (overrides?: Partial): any => { destChainId: 1, srcTokenAmount: '1000', destTokenAmount: '990', + bridges: ['cowswap'], + bridgeId: 'cowswap', minDestTokenAmount: '900', srcAsset: { symbol: 'ETH', @@ -118,6 +108,8 @@ const minimalBridgeQuoteResponse = ( decimals: 18, }, feeData: { txFee: { maxFeePerGas: '1', maxPriorityFeePerGas: '1' } }, + bridges: ['across'], + bridgeId: 'socket', }, sentAmount: { amount: '1', usd: '1' }, gasFee: { effective: { amount: '0', usd: '0' } }, @@ -141,8 +133,9 @@ const minimalBridgeQuoteResponse = ( const createMessengerHarness = ( accountAddress: string, selectedChainId: string = '0x1', + keyringType: string = 'HD Key Tree', ): any => { - const transactions: Tx[] = []; + const transactions: TransactionMeta[] = []; const messenger = { registerActionHandler: jest.fn(), @@ -160,7 +153,7 @@ const createMessengerHarness = ( // REQUIRED so isHardwareWallet() doesn't throw return { address: accountAddress, - metadata: { keyring: { type: 'HD Key Tree' } }, + metadata: { keyring: { type: keyringType } }, }; } case 'TransactionController:getState': @@ -186,165 +179,81 @@ const createMessengerHarness = ( return { messenger, transactions }; }; -const loadControllerWithMocks = (): any => { - const submitIntentMock = jest.fn(); - const getOrderStatusMock = jest.fn(); - - const fetchBridgeTxStatusMock = jest.fn(); - const getStatusRequestWithSrcTxHashMock = jest.fn(); - const getStatusRequestParamsMock = jest.fn().mockReturnValue({ - srcChainId: 1, - destChainId: 1, - srcTxHash: '', - }); - - // ADD THIS - const shouldSkipFetchDueToFetchFailuresMock = jest - .fn() - .mockReturnValue(false); - - let BridgeStatusController: any; - - jest.resetModules(); - - jest.isolateModules(() => { - jest.doMock('./utils/intent-api', () => { - const actual = jest.requireActual('./utils/intent-api'); - return { - ...actual, - IntentApiImpl: jest.fn().mockImplementation(() => ({ - submitIntent: submitIntentMock, - getOrderStatus: getOrderStatusMock, - })), - }; - }); - - jest.doMock('./utils/bridge-status', () => { - const actual = jest.requireActual('./utils/bridge-status'); - return { - ...actual, - fetchBridgeTxStatus: fetchBridgeTxStatusMock, - getStatusRequestWithSrcTxHash: getStatusRequestWithSrcTxHashMock, - shouldSkipFetchDueToFetchFailures: - shouldSkipFetchDueToFetchFailuresMock, - }; - }); - - jest.doMock('./utils/transaction', () => { - const actual = jest.requireActual('./utils/transaction'); - return { - ...actual, - generateActionId: jest - .fn() - .mockReturnValue({ toString: () => 'action-id-1' }), - handleApprovalDelay: jest.fn().mockResolvedValue(undefined), - handleMobileHardwareWalletDelay: jest.fn().mockResolvedValue(undefined), - - // keep your existing getStatusRequestParams stub here if you have it - getStatusRequestParams: getStatusRequestParamsMock, - }; - }); - - jest.doMock('./utils/metrics', () => ({ - getFinalizedTxProperties: jest.fn().mockReturnValue({}), - getPriceImpactFromQuote: jest.fn().mockReturnValue({}), - getRequestMetadataFromHistory: jest.fn().mockReturnValue({}), - getRequestParamFromHistory: jest.fn().mockReturnValue({ - chain_id_source: 'eip155:1', - chain_id_destination: 'eip155:10', - token_address_source: '0xsrc', - token_address_destination: '0xdest', - }), - getTradeDataFromHistory: jest.fn().mockReturnValue({}), - getEVMTxPropertiesFromTransactionMeta: jest.fn().mockReturnValue({}), - getTxStatusesFromHistory: jest.fn().mockReturnValue({}), - getPreConfirmationPropertiesFromQuote: jest.fn().mockReturnValue({}), - })); - - /* eslint-disable @typescript-eslint/no-require-imports, n/global-require */ - BridgeStatusController = - require('./bridge-status-controller').BridgeStatusController; - /* eslint-enable @typescript-eslint/no-require-imports, n/global-require */ - }); - - return { - BridgeStatusController, - submitIntentMock, - getOrderStatusMock, - fetchBridgeTxStatusMock, - getStatusRequestWithSrcTxHashMock, - shouldSkipFetchDueToFetchFailuresMock, - getStatusRequestParamsMock, - }; -}; - const setup = (options?: { selectedChainId?: string; approvalStatus?: TransactionStatus; -}): any => { + clientId?: BridgeClientId; + keyringType?: string; + mockTxHistory?: any; +}) => { const accountAddress = '0xAccount1'; const { messenger, transactions } = createMessengerHarness( accountAddress, options?.selectedChainId ?? '0x1', + options?.keyringType, ); - const { - BridgeStatusController, - submitIntentMock, - getOrderStatusMock, - fetchBridgeTxStatusMock, - getStatusRequestWithSrcTxHashMock, - shouldSkipFetchDueToFetchFailuresMock, - getStatusRequestParamsMock, - } = loadControllerWithMocks(); - - const addTransactionFn = jest.fn(async (txParams: any, reqOpts: any) => { - // Approval TX path (submitIntent -> #handleApprovalTx -> #handleEvmTransaction) - if ( - reqOpts?.type === TransactionType.bridgeApproval || - reqOpts?.type === TransactionType.swapApproval - ) { - const hash = '0xapprovalhash1'; - - const approvalTx: Tx = { - id: 'approvalTxId1', - type: reqOpts.type, - status: options?.approvalStatus ?? TransactionStatus.failed, - chainId: txParams.chainId, - hash, + const addTransactionFn = jest.fn( + async (txParams: TransactionParams, reqOpts: any) => { + // Approval TX path (submitIntent -> #handleApprovalTx -> #handleEvmTransaction) + if ( + reqOpts?.type === TransactionType.bridgeApproval || + reqOpts?.type === TransactionType.swapApproval + ) { + const hash = '0xapprovalhash1'; + + const approvalTx = { + id: 'approvalTxId1', + type: reqOpts.type, + status: options?.approvalStatus ?? TransactionStatus.failed, + chainId: txParams.chainId ?? '0x1', + hash, + networkClientId: 'network-client-id-1', + time: Date.now(), + txParams, + }; + transactions.push(approvalTx); + + return { + result: Promise.resolve(hash), + transactionMeta: approvalTx, + }; + } + + // Intent “display tx” path + const intentTx = { + id: 'intentDisplayTxId1', + type: reqOpts?.type, + status: TransactionStatus.submitted, + chainId: txParams.chainId ?? '0x1', + hash: undefined, + networkClientId: 'network-client-id-1', + time: Date.now(), + txParams, }; - transactions.push(approvalTx); + transactions.push(intentTx); return { - result: Promise.resolve(hash), - transactionMeta: approvalTx, + result: Promise.resolve('0xunused'), + transactionMeta: intentTx, }; - } - - // Intent “display tx” path - const intentTx: Tx = { - id: 'intentDisplayTxId1', - type: reqOpts?.type, - status: TransactionStatus.submitted, - chainId: txParams.chainId, - hash: undefined, - }; - transactions.push(intentTx); - - return { - result: Promise.resolve('0xunused'), - transactionMeta: intentTx, - }; - }); + }, + ); + const mockFetchFn = jest.fn(); const controller = new BridgeStatusController({ messenger, - clientId: 'extension', - fetchFn: jest.fn(), + state: { + txHistory: options?.mockTxHistory ?? {}, + }, + clientId: options?.clientId ?? BridgeClientId.EXTENSION, + fetchFn: (...args: any[]) => mockFetchFn(...args), addTransactionFn, addTransactionBatchFn: jest.fn(), updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(async () => ({ estimates: {} })), + estimateGasFeeFn: jest.fn(async () => ({ + estimates: {} as GasFeeEstimates, + })), config: { customBridgeApiBaseUrl: 'http://localhost' }, traceFn: (_req: any, fn?: any): any => fn?.(), }); @@ -353,11 +262,10 @@ const setup = (options?: { .spyOn(controller, 'startPolling') .mockReturnValue('poll-token-1'); - const stopPollingSpy = jest - .spyOn(controller, 'stopPollingByPollingToken') - .mockImplementation(() => undefined); + const stopPollingSpy = jest.spyOn(controller, 'stopPollingByPollingToken'); return { + mockFetchFn, controller, messenger, transactions, @@ -365,34 +273,30 @@ const setup = (options?: { startPollingSpy, stopPollingSpy, accountAddress, - submitIntentMock, - getOrderStatusMock, - fetchBridgeTxStatusMock, - getStatusRequestWithSrcTxHashMock, - shouldSkipFetchDueToFetchFailuresMock, - getStatusRequestParamsMock, }; }; describe('BridgeStatusController (intent swaps)', () => { beforeEach(() => { + jest.restoreAllMocks(); jest.clearAllMocks(); }); it('submitIntent: throws if approval confirmation fails (does not write history or start polling)', async () => { - const { controller, accountAddress, submitIntentMock, startPollingSpy } = - setup(); + const { controller, accountAddress, startPollingSpy } = setup(); const orderUid = 'order-uid-1'; // In the "throw on approval confirmation failure" behavior, we should not reach intent submission, // but keep this here to prove it wasn't used. - submitIntentMock.mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + const submitIntentSpy = jest + .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') + .mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); const quoteResponse = minimalIntentQuoteResponse({ // Include approval to exercise the approval confirmation path. @@ -411,10 +315,8 @@ describe('BridgeStatusController (intent swaps)', () => { quoteResponse, accountAddress, }); - expect(await promise.catch((error: any) => error)).toStrictEqual( - expect.objectContaining({ - message: expect.stringMatching(/approval/iu), - }), + await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot( + `"Approval transaction did not confirm"`, ); // Since we throw before intent order submission succeeds, we should not create the history item @@ -425,21 +327,23 @@ describe('BridgeStatusController (intent swaps)', () => { expect(startPollingSpy).not.toHaveBeenCalled(); // Optional: ensure we never called the intent API submit - expect(submitIntentMock).not.toHaveBeenCalled(); + expect(submitIntentSpy).not.toHaveBeenCalled(); }); it('submitIntent: completes when approval tx confirms', async () => { - const { controller, accountAddress, submitIntentMock } = setup({ + const { controller, accountAddress } = setup({ approvalStatus: TransactionStatus.confirmed, }); - const orderUid = 'order-uid-approve-1'; - submitIntentMock.mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + + const submitIntentSpy = jest + .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') + .mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); const quoteResponse = minimalIntentQuoteResponse({ approval: { @@ -459,21 +363,25 @@ describe('BridgeStatusController (intent swaps)', () => { }), ).resolves.toBeDefined(); - expect(submitIntentMock).toHaveBeenCalled(); + expect(submitIntentSpy).toHaveBeenCalled(); }); it('submitIntent: throws when approval tx is rejected', async () => { - const { controller, accountAddress, submitIntentMock } = setup({ + const { controller, accountAddress } = setup({ approvalStatus: TransactionStatus.rejected, + clientId: BridgeClientId.MOBILE, + keyringType: 'Hardware', }); const orderUid = 'order-uid-approve-2'; - submitIntentMock.mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + const submitIntentSpy = jest + .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') + .mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); const quoteResponse = minimalIntentQuoteResponse({ approval: { @@ -490,35 +398,32 @@ describe('BridgeStatusController (intent swaps)', () => { quoteResponse, accountAddress, }); - expect(await promise.catch((error: any) => error)).toStrictEqual( - expect.objectContaining({ - message: expect.stringMatching(/approval/iu), - }), + await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot( + `"Approval transaction did not confirm"`, ); - expect(submitIntentMock).not.toHaveBeenCalled(); + expect(submitIntentSpy).not.toHaveBeenCalled(); }); it('submitIntent: logs error when history update fails but still returns tx meta', async () => { - const { - controller, - accountAddress, - submitIntentMock, - getStatusRequestParamsMock, - } = setup(); + const { controller, accountAddress } = setup(); const orderUid = 'order-uid-log-1'; - submitIntentMock.mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); - - getStatusRequestParamsMock.mockImplementation(() => { - throw new Error('boom'); - }); + jest + .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') + .mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); + + jest + .spyOn(transactionUtils, 'getStatusRequestParams') + .mockImplementation(() => { + throw new Error('boom'); + }); const quoteResponse = minimalIntentQuoteResponse(); const consoleSpy = jest @@ -540,15 +445,17 @@ describe('BridgeStatusController (intent swaps)', () => { }); it('submitIntent: signs typedData', async () => { - const { controller, messenger, accountAddress, submitIntentMock } = setup(); + const { controller, messenger, accountAddress } = setup(); const orderUid = 'order-uid-signed-in-core-1'; - submitIntentMock.mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + const submitIntentSpy = jest + .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') + .mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); const quoteResponse = minimalIntentQuoteResponse(); quoteResponse.quote.intent.typedData = { @@ -588,26 +495,22 @@ describe('BridgeStatusController (intent swaps)', () => { ]), ); - expect(submitIntentMock.mock.calls[0]?.[0]?.signature).toBe('0xautosigned'); + expect(submitIntentSpy.mock.calls[0]?.[0]?.signature).toBe('0xautosigned'); }); it('intent polling: updates history, merges tx hashes, updates TC tx, and stops polling on COMPLETED', async () => { - const { - controller, - accountAddress, - submitIntentMock, - getOrderStatusMock, - stopPollingSpy, - } = setup(); + const { controller, accountAddress, stopPollingSpy } = setup(); const orderUid = 'order-uid-2'; - submitIntentMock.mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + jest + .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') + .mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); const quoteResponse = minimalIntentQuoteResponse(); @@ -618,12 +521,14 @@ describe('BridgeStatusController (intent swaps)', () => { const historyKey = orderUid; - getOrderStatusMock.mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.COMPLETED, - txHash: '0xnewhash', - metadata: { txHashes: ['0xold1', '0xnewhash'] }, - }); + jest + .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') + .mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.COMPLETED, + txHash: '0xnewhash', + metadata: { txHashes: ['0xold1', '0xnewhash'] }, + }); await controller._executePoll({ bridgeTxMetaId: historyKey }); @@ -634,24 +539,20 @@ describe('BridgeStatusController (intent swaps)', () => { expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); }); - it('intent polling: maps EXPIRED to FAILED, falls back to txHash when metadata hashes empty, and skips TC update if original tx not found', async () => { - const { - controller, - accountAddress, - submitIntentMock, - getOrderStatusMock, - transactions, - stopPollingSpy, - } = setup(); + it('intent polling: maps PENDING to PENDING, falls back to txHash when metadata hashes empty', async () => { + const { controller, accountAddress, transactions, stopPollingSpy } = + setup(); const orderUid = 'order-uid-expired-1'; - submitIntentMock.mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + jest + .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') + .mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); const quoteResponse = minimalIntentQuoteResponse(); @@ -665,39 +566,38 @@ describe('BridgeStatusController (intent swaps)', () => { // Remove TC tx so update branch logs "transaction not found" transactions.splice(0, transactions.length); - getOrderStatusMock.mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.EXPIRED, - txHash: '0xonlyhash', - metadata: { txHashes: [] }, // forces fallback to txHash - }); + jest + .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') + .mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.PENDING, + txHash: '0xonlyhash', + metadata: { txHashes: [] }, // forces fallback to txHash + }); await controller._executePoll({ bridgeTxMetaId: historyKey }); const updated = controller.state.txHistory[historyKey]; - expect(updated.status.status).toBe(StatusTypes.FAILED); + expect(updated.status.status).toBe(StatusTypes.PENDING); expect(updated.status.srcChain.txHash).toBe('0xonlyhash'); - expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); + expect(stopPollingSpy).not.toHaveBeenCalled(); }); - it('intent polling: stops polling when attempts reach MAX_ATTEMPTS', async () => { - const { - controller, - accountAddress, - submitIntentMock, - getOrderStatusMock, - stopPollingSpy, - } = setup(); + it('intent polling: maps EXPIRED to FAILED, falls back to txHash when metadata hashes empty, and skips TC update if original tx not found', async () => { + const { controller, accountAddress, transactions, stopPollingSpy } = + setup(); - const orderUid = 'order-uid-3'; + const orderUid = 'order-uid-expired-1'; - submitIntentMock.mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + jest + .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') + .mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); const quoteResponse = minimalIntentQuoteResponse(); @@ -708,19 +608,67 @@ describe('BridgeStatusController (intent swaps)', () => { const historyKey = orderUid; - // Prime attempts so next failure hits MAX_ATTEMPTS - controller.update((state: any) => { - state.txHistory[historyKey].attempts = { - counter: MAX_ATTEMPTS - 1, - lastAttemptTime: 0, - }; - }); + // Remove TC tx so update branch logs "transaction not found" + transactions.splice(0, transactions.length); - getOrderStatusMock.mockRejectedValue(new Error('boom')); + jest + .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') + .mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.EXPIRED, + txHash: '0xonlyhash', + metadata: { txHashes: [] }, // forces fallback to txHash + }); await controller._executePoll({ bridgeTxMetaId: historyKey }); + const updated = controller.state.txHistory[historyKey]; + expect(updated.status.status).toBe(StatusTypes.FAILED); + expect(updated.status.srcChain.txHash).toBe('0xonlyhash'); + expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); + }); + + it('intent polling: stops polling when attempts reach MAX_ATTEMPTS', async () => { + const orderUid = 'order-uid-3'; + const { controller, stopPollingSpy } = setup({ + mockTxHistory: { + [orderUid]: { + txMetaId: 'order-uid-3', + originalTransactionId: 'order-uid-3', + quote: { + ...minimalIntentQuoteResponse().quote, + }, + attempts: { + counter: MAX_ATTEMPTS - 1, + lastAttemptTime: 0, + }, + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: undefined }, + }, + }, + }, + }); + + jest + .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') + .mockResolvedValue({ + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }); + + const historyKey = orderUid; + + jest + .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') + .mockRejectedValue(new Error('boom')); + + await controller._executePoll({ bridgeTxMetaId: historyKey }); + + expect(stopPollingSpy).toHaveBeenCalledTimes(1); expect(controller.state.txHistory[historyKey].attempts).toStrictEqual( expect.objectContaining({ counter: MAX_ATTEMPTS }), ); @@ -729,29 +677,35 @@ describe('BridgeStatusController (intent swaps)', () => { describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () => { beforeEach(() => { + jest.restoreAllMocks(); + jest.resetModules(); jest.clearAllMocks(); }); it('transactionFailed subscription: marks main tx as FAILED and tracks (non-rejected)', async () => { - const { controller, messenger } = setup(); - - // Seed txHistory with a pending bridge tx - controller.update((state: any) => { - state.txHistory.bridgeTxMetaId1 = { - txMetaId: 'bridgeTxMetaId1', - originalTransactionId: 'bridgeTxMetaId1', - quote: { - srcChainId: 1, - destChainId: 10, - srcAsset: { assetId: 'eip155:1/slip44:60' }, - destAsset: { assetId: 'eip155:10/slip44:60' }, - }, - account: '0xAccount1', - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '0xsrc' }, + const { controller, messenger } = setup({ + mockTxHistory: { + bridgeTxMetaId1: { + txMetaId: 'bridgeTxMetaId1', + originalTransactionId: 'bridgeTxMetaId1', + quote: { + srcChainId: 1, + destChainId: 10, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', + }, + destAsset: { assetId: 'eip155:10/slip44:60' }, + bridges: ['across'], + bridgeId: 'rango', + }, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xsrc' }, + }, }, - }; + }, }); const failedCb = messenger.subscribe.mock.calls.find( @@ -768,6 +722,8 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () }, }); + controller.stopAllPolling(); + expect(controller.state.txHistory.bridgeTxMetaId1.status.status).toBe( StatusTypes.FAILED, ); @@ -783,27 +739,34 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () }); it('transactionFailed subscription: maps approval tx id back to main history item', async () => { - const { controller, messenger } = setup(); - - controller.update((state: any) => { - state.txHistory.mainTx = { - txMetaId: 'mainTx', - originalTransactionId: 'mainTx', - approvalTxId: 'approvalTx', - quote: { - srcChainId: 1, - destChainId: 10, - srcAsset: { assetId: 'eip155:1/slip44:60' }, - destAsset: { assetId: 'eip155:10/slip44:60' }, - }, - account: '0xAccount1', - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '0xsrc' }, + const { controller, messenger } = setup({ + mockTxHistory: { + mainTx: { + txMetaId: 'mainTx', + originalTransactionId: 'mainTx', + approvalTxId: 'approvalTx', + quote: { + srcChainId: 1, + destChainId: 10, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', + }, + destAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:10/slip44:60', + }, + bridges: ['cowswap'], + bridgeId: 'cowswap', + }, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xsrc' }, + }, }, - }; + }, }); - const failedCb = messenger.subscribe.mock.calls.find( ([evt]: [any]) => evt === 'TransactionController:transactionFailed', )?.[1]; @@ -817,31 +780,40 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () }, }); + controller.stopAllPolling(); + expect(controller.state.txHistory.mainTx.status.status).toBe( StatusTypes.FAILED, ); }); it('transactionConfirmed subscription: tracks swap Completed; starts polling on bridge confirmed', async () => { - const { controller, messenger, startPollingSpy } = setup(); - - // Seed history for bridge id so #startPollingForTxId can startPolling() - controller.update((state: any) => { - state.txHistory.bridgeConfirmed1 = { - txMetaId: 'bridgeConfirmed1', - originalTransactionId: 'bridgeConfirmed1', - quote: { - srcChainId: 1, - destChainId: 10, - srcAsset: { assetId: 'eip155:1/slip44:60' }, - destAsset: { assetId: 'eip155:10/slip44:60' }, - }, - account: '0xAccount1', - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '0xsrc' }, + const { messenger, controller, startPollingSpy } = setup({ + mockTxHistory: { + bridgeConfirmed1: { + txMetaId: 'bridgeConfirmed1', + originalTransactionId: 'bridgeConfirmed1', + quote: { + srcChainId: 1, + destChainId: 10, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', + }, + destAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:10/slip44:60', + }, + bridges: ['cowswap'], + bridgeId: 'cowswap', + }, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xsrc' }, + }, }, - }; + }, }); const confirmedCb = messenger.subscribe.mock.calls.find( @@ -863,6 +835,8 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () chainId: '0x1', }); + controller.stopAllPolling(); + expect(startPollingSpy).toHaveBeenCalledWith({ bridgeTxMetaId: 'bridgeConfirmed1', }); @@ -883,55 +857,72 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () }); it('restartPollingForFailedAttempts: resets attempts and restarts polling via txHash lookup (bridge tx only)', async () => { - const { controller, startPollingSpy } = setup(); - - controller.update((state: any) => { - state.txHistory.bridgeTx1 = { - txMetaId: 'bridgeTx1', - originalTransactionId: 'bridgeTx1', - quote: { - srcChainId: 1, - destChainId: 10, - srcAsset: { assetId: 'eip155:1/slip44:60' }, - destAsset: { assetId: 'eip155:10/slip44:60' }, - }, - attempts: { counter: 7, lastAttemptTime: 0 }, - account: '0xAccount1', - status: { - status: StatusTypes.UNKNOWN, - srcChain: { chainId: 1, txHash: '0xhash-find-me' }, + const { controller } = setup({ + mockTxHistory: { + bridgeTx1: { + txMetaId: 'bridgeTx1', + originalTransactionId: 'bridgeTx1', + quote: { + srcChainId: 1, + destChainId: 10, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', + }, + destAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:10/slip44:60', + }, + bridges: ['cowswap'], + bridgeId: 'cowswap', + }, + attempts: { counter: 7, lastAttemptTime: Date.now() }, + account: '0xAccount1', + status: { + status: StatusTypes.UNKNOWN, + srcChain: { chainId: 1, txHash: '0xhash-find-me' }, + }, }, - }; + }, }); + expect(controller.state.txHistory.bridgeTx1.attempts).toStrictEqual( + expect.objectContaining({ counter: 7 }), + ); + + const startPollingSpy = jest.spyOn(controller, 'startPolling'); + + controller.stopAllPolling(); controller.restartPollingForFailedAttempts({ txHash: '0xhash-find-me' }); expect(controller.state.txHistory.bridgeTx1.attempts).toBeUndefined(); expect(startPollingSpy).toHaveBeenCalledWith({ bridgeTxMetaId: 'bridgeTx1', }); + + controller.stopAllPolling(); }); it('restartPollingForFailedAttempts: does not restart polling for same-chain swap tx', async () => { - const { controller, startPollingSpy } = setup(); - - controller.update((state: any) => { - state.txHistory.swapTx1 = { - txMetaId: 'swapTx1', - originalTransactionId: 'swapTx1', - quote: { - srcChainId: 1, - destChainId: 1, - srcAsset: { assetId: 'eip155:1/slip44:60' }, - destAsset: { assetId: 'eip155:1/slip44:60' }, - }, - attempts: { counter: 7, lastAttemptTime: 0 }, - account: '0xAccount1', - status: { - status: StatusTypes.UNKNOWN, - srcChain: { chainId: 1, txHash: '0xhash-samechain' }, + const { controller, startPollingSpy } = setup({ + mockTxHistory: { + swapTx1: { + txMetaId: 'swapTx1', + originalTransactionId: 'swapTx1', + quote: { + srcChainId: 1, + destChainId: 1, + srcAsset: { assetId: 'eip155:1/slip44:60' }, + destAsset: { assetId: 'eip155:1/slip44:60' }, + }, + attempts: { counter: 7, lastAttemptTime: 0 }, + account: '0xAccount1', + status: { + status: StatusTypes.UNKNOWN, + srcChain: { chainId: 1, txHash: '0xhash-samechain' }, + }, }, - }; + }, }); controller.restartPollingForFailedAttempts({ txMetaId: 'swapTx1' }); @@ -950,11 +941,13 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () // Use deprecated method to create history and start polling (so token exists in controller) controller.startPollingForBridgeTxStatus({ accountAddress, - bridgeTxMeta: { id: 'bridgeToWipe1' }, + bridgeTxMeta: { id: 'bridgeToWipe1' } as TransactionMeta, statusRequest: { srcChainId: 1, srcTxHash: '0xsrc', destChainId: 10, + bridgeId: 'across', + bridge: 'socket', }, quoteResponse, slippagePercentage: 0, @@ -972,180 +965,30 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); expect(controller.state.txHistory.bridgeToWipe1).toBeUndefined(); }); - - it('eVM bridge polling: looks up srcTxHash in TC when missing, updates history, stops polling, and publishes completion', async () => { - const { - controller, - transactions, - accountAddress, - fetchBridgeTxStatusMock, - getStatusRequestWithSrcTxHashMock, - stopPollingSpy, - messenger, - } = setup(); - - // Create a history item with missing src tx hash - const quoteResponse = minimalBridgeQuoteResponse(accountAddress); - controller.startPollingForBridgeTxStatus({ - accountAddress, - bridgeTxMeta: { id: 'bridgePoll1' }, - statusRequest: { - srcChainId: 1, - srcTxHash: '', // force TC lookup - destChainId: 10, - }, - quoteResponse, - slippagePercentage: 0, - startTime: Date.now(), - isStxEnabled: false, - }); - - // Seed TC with tx meta id=bridgePoll1 and a hash for lookup - transactions.push({ - id: 'bridgePoll1', - status: TransactionStatus.confirmed, - type: TransactionType.bridge, - chainId: '0x1', - hash: '0xlooked-up-hash', - }); - - getStatusRequestWithSrcTxHashMock.mockReturnValue({ - srcChainId: 1, - srcTxHash: '0xlooked-up-hash', - destChainId: 10, - }); - - fetchBridgeTxStatusMock.mockResolvedValue({ - status: { - status: StatusTypes.COMPLETE, - srcChain: { chainId: 1, txHash: '0xlooked-up-hash' }, - destChain: { chainId: 10, txHash: '0xdesthash' }, - }, - validationFailures: [], - }); - - await controller._executePoll({ bridgeTxMetaId: 'bridgePoll1' }); - - const updated = controller.state.txHistory.bridgePoll1; - - expect(updated.status.status).toBe(StatusTypes.COMPLETE); - expect(updated.status.srcChain.txHash).toBe('0xlooked-up-hash'); - expect(updated.completionTime).toStrictEqual(expect.any(Number)); - - expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); - - expect(messenger.publish).toHaveBeenCalledWith( - 'BridgeStatusController:destinationTransactionCompleted', - quoteResponse.quote.destAsset.assetId, - ); - }); - - it('eVM bridge polling: tracks StatusValidationFailed, increments attempts, and stops polling at MAX_ATTEMPTS', async () => { - const { - controller, - accountAddress, - fetchBridgeTxStatusMock, - getStatusRequestWithSrcTxHashMock, - stopPollingSpy, - } = setup(); - - const quoteResponse = minimalBridgeQuoteResponse(accountAddress); - controller.startPollingForBridgeTxStatus({ - accountAddress, - bridgeTxMeta: { id: 'bridgeValidationFail1' }, - statusRequest: { - srcChainId: 1, - srcTxHash: '0xsrc', - destChainId: 10, - }, - quoteResponse, - slippagePercentage: 0, - startTime: Date.now(), - isStxEnabled: false, - }); - - // Prime attempts to just below MAX so the next failure stops polling - controller.update((state: any) => { - state.txHistory.bridgeValidationFail1.attempts = { - counter: MAX_ATTEMPTS - 1, - lastAttemptTime: 0, - }; - }); - - getStatusRequestWithSrcTxHashMock.mockReturnValue({ - srcChainId: 1, - srcTxHash: '0xsrc', - destChainId: 10, - }); - - fetchBridgeTxStatusMock.mockResolvedValue({ - status: { - status: StatusTypes.UNKNOWN, - srcChain: { chainId: 1, txHash: '0xsrc' }, - }, - validationFailures: ['bad_status_shape'], - }); - - await controller._executePoll({ - bridgeTxMetaId: 'bridgeValidationFail1', - }); - - expect( - controller.state.txHistory.bridgeValidationFail1.attempts, - ).toStrictEqual(expect.objectContaining({ counter: MAX_ATTEMPTS })); - expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1'); - }); - - it('bridge polling: returns early (does not fetch) when srcTxHash cannot be determined', async () => { - const { - controller, - accountAddress, - fetchBridgeTxStatusMock, - getStatusRequestWithSrcTxHashMock, - } = setup(); - - const quoteResponse = minimalBridgeQuoteResponse(accountAddress); - controller.startPollingForBridgeTxStatus({ - accountAddress, - bridgeTxMeta: { id: 'bridgeNoHash1' }, - statusRequest: { - srcChainId: 1, - srcTxHash: '', // missing - destChainId: 10, - }, - quoteResponse, - slippagePercentage: 0, - startTime: Date.now(), - isStxEnabled: false, - }); - - await controller._executePoll({ bridgeTxMetaId: 'bridgeNoHash1' }); - - expect(getStatusRequestWithSrcTxHashMock).not.toHaveBeenCalled(); - expect(fetchBridgeTxStatusMock).not.toHaveBeenCalled(); - }); }); describe('BridgeStatusController (target uncovered branches)', () => { beforeEach(() => { + jest.restoreAllMocks(); + jest.resetModules(); + jest.resetAllMocks(); jest.clearAllMocks(); }); it('transactionFailed: returns early for intent txs (swapMetaData.isIntentTx)', () => { - const { controller, messenger } = setup(); - - // seed a history item that would otherwise be marked FAILED - controller.update((state: any) => { - state.txHistory.tx1 = { - txMetaId: 'tx1', - originalTransactionId: 'tx1', - quote: { srcChainId: 1, destChainId: 10 }, - account: '0xAccount1', - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '0x' }, + const { controller, messenger } = setup({ + mockTxHistory: { + tx1: { + txMetaId: 'tx1', + originalTransactionId: 'tx1', + quote: minimalIntentQuoteResponse().quote, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0x' }, + }, }, - }; + }, }); const failedCb = messenger.subscribe.mock.calls.find( @@ -1155,6 +998,7 @@ describe('BridgeStatusController (target uncovered branches)', () => { failedCb({ transactionMeta: { id: 'tx1', + chainId: '0x1', type: TransactionType.bridge, status: TransactionStatus.failed, swapMetaData: { isIntentTx: true }, // <- triggers early return @@ -1164,17 +1008,13 @@ describe('BridgeStatusController (target uncovered branches)', () => { expect(controller.state.txHistory.tx1.status.status).toBe( StatusTypes.FAILED, ); + controller.stopAllPolling(); }); it('constructor restartPolling: skips items when shouldSkipFetchDueToFetchFailures returns true', () => { const accountAddress = '0xAccount1'; const { messenger } = createMessengerHarness(accountAddress); - const { BridgeStatusController, shouldSkipFetchDueToFetchFailuresMock } = - loadControllerWithMocks(); - - shouldSkipFetchDueToFetchFailuresMock.mockReturnValue(true); - const startPollingProtoSpy = jest .spyOn(BridgeStatusController.prototype, 'startPolling') .mockReturnValue('tok'); @@ -1191,18 +1031,17 @@ describe('BridgeStatusController (target uncovered branches)', () => { status: StatusTypes.PENDING, srcChain: { chainId: 1, txHash: '0xsrc' }, }, - attempts: { counter: 1, lastAttemptTime: 0 }, + attempts: { counter: MAX_ATTEMPTS, lastAttemptTime: Date.now() }, }, }, - }; + } as unknown as BridgeStatusControllerState; // constructor calls #restartPollingForIncompleteHistoryItems() // shouldSkipFetchDueToFetchFailures=true => should NOT call startPolling - // eslint-disable-next-line no-new - new BridgeStatusController({ + const controller = new BridgeStatusController({ messenger, state, - clientId: 'extension', + clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), addTransactionFn: jest.fn(), addTransactionBatchFn: jest.fn(), @@ -1212,6 +1051,10 @@ describe('BridgeStatusController (target uncovered branches)', () => { traceFn: (_r: any, fn?: any): any => fn?.(), }); + expect(controller.state.txHistory.init1.attempts?.counter).toBe( + MAX_ATTEMPTS, + ); + expect(startPollingProtoSpy).not.toHaveBeenCalled(); startPollingProtoSpy.mockRestore(); }); @@ -1257,124 +1100,56 @@ describe('BridgeStatusController (target uncovered branches)', () => { }); it('bridge polling: returns early when shouldSkipFetchDueToFetchFailures returns true', async () => { - const { - controller, - accountAddress, - shouldSkipFetchDueToFetchFailuresMock, - fetchBridgeTxStatusMock, - } = setup(); - - const quoteResponse: any = { - quote: { srcChainId: 1, destChainId: 10, destAsset: { assetId: 'x' } }, - estimatedProcessingTimeInSeconds: 1, - sentAmount: { amount: '0' }, - gasFee: { effective: { amount: '0' } }, - toTokenAmount: { usd: '0' }, - }; - - controller.startPollingForBridgeTxStatus({ - accountAddress, - bridgeTxMeta: { id: 'skipPoll1' }, - statusRequest: { srcChainId: 1, srcTxHash: '0xhash', destChainId: 10 }, - quoteResponse, - slippagePercentage: 0, - startTime: Date.now(), - isStxEnabled: false, - } as any); - - shouldSkipFetchDueToFetchFailuresMock.mockReturnValueOnce(true); - - await controller._executePoll({ bridgeTxMetaId: 'skipPoll1' }); - - expect(fetchBridgeTxStatusMock).not.toHaveBeenCalled(); - }); - - it('bridge polling: final FAILED tracks Failed event', async () => { - const { - controller, - accountAddress, - fetchBridgeTxStatusMock, - getStatusRequestWithSrcTxHashMock, - messenger, - } = setup(); - - const quoteResponse: any = { - quote: { - srcChainId: 1, - destChainId: 10, - destAsset: { assetId: 'dest' }, - srcAsset: { assetId: 'src' }, - }, - estimatedProcessingTimeInSeconds: 1, - sentAmount: { amount: '0' }, - gasFee: { effective: { amount: '0' } }, - toTokenAmount: { usd: '0' }, - }; - - controller.startPollingForBridgeTxStatus({ - accountAddress, - bridgeTxMeta: { id: 'failFinal1' }, - statusRequest: { srcChainId: 1, srcTxHash: '0xhash', destChainId: 10 }, - quoteResponse, - slippagePercentage: 0, - startTime: Date.now(), - isStxEnabled: false, - } as any); - - getStatusRequestWithSrcTxHashMock.mockReturnValue({ - srcChainId: 1, - srcTxHash: '0xhash', - destChainId: 10, - }); - - fetchBridgeTxStatusMock.mockResolvedValue({ - status: { - status: StatusTypes.FAILED, - srcChain: { chainId: 1, txHash: '0xhash' }, + const { controller, accountAddress } = setup({ + mockTxHistory: { + skipPoll1: { + txMetaId: 'skipPoll1', + originalTransactionId: 'skipPoll1', + quote: { + srcChainId: 1, + destChainId: 137, + destAsset: { assetId: 'x' }, + bridges: ['rango'], + }, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xhash' }, + }, + attempts: { + counter: MAX_ATTEMPTS, + lastAttemptTime: Date.now(), + }, + }, }, - validationFailures: [], }); - await controller._executePoll({ bridgeTxMetaId: 'failFinal1' }); - - expect((messenger.call as jest.Mock).mock.calls).toStrictEqual( - expect.arrayContaining([ - expect.arrayContaining([ - 'BridgeController:trackUnifiedSwapBridgeEvent', - UnifiedSwapBridgeEventName.Failed, - expect.any(Object), - ]), - ]), - ); - }); - - it('bridge polling: final COMPLETE with featureId set stops polling but skips tracking', async () => { - const { - controller, - accountAddress, - fetchBridgeTxStatusMock, - getStatusRequestWithSrcTxHashMock, - stopPollingSpy, - messenger, - } = setup(); - const quoteResponse: any = { quote: { srcChainId: 1, destChainId: 10, - destAsset: { assetId: 'dest' }, - srcAsset: { assetId: 'src' }, + destAsset: { assetId: 'x' }, + bridges: ['across'], }, - featureId: 'perps', // <- triggers featureId skip in #fetchBridgeTxStatus estimatedProcessingTimeInSeconds: 1, sentAmount: { amount: '0' }, gasFee: { effective: { amount: '0' } }, toTokenAmount: { usd: '0' }, }; + const fetchBridgeTxStatusSpy = jest + .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') + .mockResolvedValue({ + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xhash' }, + }, + validationFailures: [], + }); + controller.startPollingForBridgeTxStatus({ accountAddress, - bridgeTxMeta: { id: 'perps1' }, + bridgeTxMeta: { id: 'skipPoll1' }, statusRequest: { srcChainId: 1, srcTxHash: '0xhash', destChainId: 10 }, quoteResponse, slippagePercentage: 0, @@ -1382,114 +1157,85 @@ describe('BridgeStatusController (target uncovered branches)', () => { isStxEnabled: false, } as any); - getStatusRequestWithSrcTxHashMock.mockReturnValue({ - srcChainId: 1, - srcTxHash: '0xhash', - destChainId: 10, - }); - - fetchBridgeTxStatusMock.mockResolvedValue({ - status: { - status: StatusTypes.COMPLETE, - srcChain: { chainId: 1, txHash: '0xhash' }, - }, - validationFailures: [], - }); - - await controller._executePoll({ bridgeTxMetaId: 'perps1' }); - - expect(stopPollingSpy).toHaveBeenCalled(); + await controller._executePoll({ bridgeTxMetaId: 'skipPoll1' }); - // should not track Completed because featureId is set - expect((messenger.call as jest.Mock).mock.calls).not.toStrictEqual( - expect.arrayContaining([ - expect.arrayContaining([ - 'BridgeController:trackUnifiedSwapBridgeEvent', - UnifiedSwapBridgeEventName.Completed, - ]), - ]), + expect(fetchBridgeTxStatusSpy).not.toHaveBeenCalledWith( + expect.objectContaining({ + bridge: 'rango', + destChainId: 137, + }), ); }); it('statusValidationFailed event includes refresh_count from attempts', async () => { - const { - controller, - accountAddress, - fetchBridgeTxStatusMock, - getStatusRequestWithSrcTxHashMock, - messenger, - } = setup(); - - const quoteResponse: any = { - quote: { - srcChainId: 1, - destChainId: 10, - destAsset: { assetId: 'dest' }, - srcAsset: { assetId: 'src' }, + const quoteResponse = minimalBridgeQuoteResponse('0xAccount1'); + const { controller, messenger, mockFetchFn } = setup({ + mockTxHistory: { + valFail1: { + txMetaId: 'valFail1', + originalTransactionId: 'valFail1', + quote: quoteResponse.quote, + account: '0xAccount1', + attempts: { counter: 3, lastAttemptTime: Date.now() - 100000000 }, + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xhash' }, + }, + }, }, - estimatedProcessingTimeInSeconds: 1, - sentAmount: { amount: '0' }, - gasFee: { effective: { amount: '0' } }, - toTokenAmount: { usd: '0' }, - }; - - controller.startPollingForBridgeTxStatus({ - accountAddress, - bridgeTxMeta: { id: 'valFail1' }, - statusRequest: { srcChainId: 1, srcTxHash: '0xhash', destChainId: 10 }, - quoteResponse, - slippagePercentage: 0, - startTime: Date.now(), - isStxEnabled: false, - } as any); - - // ensure attempts exists BEFORE validation failure is tracked - controller.update((state: any) => { - state.txHistory.valFail1.attempts = { counter: 5, lastAttemptTime: 0 }; }); - getStatusRequestWithSrcTxHashMock.mockReturnValue({ - srcChainId: 1, - srcTxHash: '0xhash', - destChainId: 10, - }); - - fetchBridgeTxStatusMock.mockResolvedValue({ - status: { - status: StatusTypes.UNKNOWN, - srcChain: { chainId: 1, txHash: '0xhash' }, - }, - validationFailures: ['bad_status'], + mockFetchFn.mockResolvedValueOnce({ + srcChain: { chainId: 1, txHash: '0xhash' }, }); + await controller._executePoll({ bridgeTxMetaId: 'valFail1' }); await controller._executePoll({ bridgeTxMetaId: 'valFail1' }); - expect((messenger.call as jest.Mock).mock.calls).toStrictEqual( - expect.arrayContaining([ - expect.arrayContaining([ - 'BridgeController:trackUnifiedSwapBridgeEvent', - UnifiedSwapBridgeEventName.StatusValidationFailed, - expect.objectContaining({ refresh_count: 5 }), - ]), - ]), + expect(controller.state.txHistory.valFail1.attempts).toStrictEqual( + expect.objectContaining({ counter: 4 }), ); + + expect(messenger.call.mock.calls).toMatchInlineSnapshot(` + [ + [ + "AuthenticationController:getBearerToken", + ], + [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Status Failed Validation", + { + "action_type": "swapbridge-v1", + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:1", + "failures": [ + "across|status", + ], + "location": "Main View", + "refresh_count": 3, + "token_address_destination": "eip155:10/slip44:60", + "token_address_source": "eip155:1/slip44:60", + }, + ], + ] + `); }); it('track event: history has featureId => #trackUnifiedSwapBridgeEvent returns early (skip tracking)', () => { - const { controller, messenger } = setup(); - - controller.update((state: any) => { - state.txHistory.feat1 = { - txMetaId: 'feat1', - originalTransactionId: 'feat1', - quote: { srcChainId: 1, destChainId: 10 }, - account: '0xAccount1', - featureId: 'perps', - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '0x' }, + const { controller, messenger } = setup({ + mockTxHistory: { + feat1: { + txMetaId: 'feat1', + originalTransactionId: 'feat1', + quote: minimalBridgeQuoteResponse('0xAccount1').quote, + account: '0xAccount1', + featureId: 'perps', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0x' }, + }, }, - }; + }, }); const failedCb = messenger.subscribe.mock.calls.find( @@ -1501,6 +1247,7 @@ describe('BridgeStatusController (target uncovered branches)', () => { id: 'feat1', type: TransactionType.bridge, status: TransactionStatus.failed, + chainId: '0x1', }, }); @@ -1512,83 +1259,152 @@ describe('BridgeStatusController (target uncovered branches)', () => { ]), ]), ); - }); - - it('submitTx: throws when multichain account is undefined', async () => { - const { controller } = setup(); - - await expect( - controller.submitTx( - '0xNotKnownByHarness', - { featureId: undefined } as any, - false, - ), - ).rejects.toThrow(/undefined multichain account/u); + controller.stopAllPolling(); }); it('intent order PENDING maps to bridge PENDING', async () => { - const { controller, getOrderStatusMock } = setup(); + const { controller } = setup({ + mockTxHistory: { + 'order-1': { + txMetaId: 'order-1', + originalTransactionId: 'order-1', + quote: minimalIntentQuoteResponse().quote, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xhash' }, + }, + }, + }, + }); - seedIntentHistory(controller); + jest + .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') + .mockImplementation( + jest.fn().mockResolvedValue({ + id: 'order-1', + status: IntentOrderStatus.PENDING, + txHash: undefined, + metadata: { txHashes: [] }, + }), + ); - getOrderStatusMock.mockResolvedValueOnce({ - id: 'order-1', - status: IntentOrderStatus.PENDING, - txHash: undefined, - metadata: { txHashes: [] }, + controller.startPolling({ + bridgeTxMetaId: 'order-1', }); - await controller._executePoll({ bridgeTxMetaId: 'order-1' }); - expect(controller.state.txHistory['order-1'].status.status).toBe( StatusTypes.PENDING, ); + controller.stopAllPolling(); }); it('intent order SUBMITTED maps to bridge SUBMITTED', async () => { - const { controller, getOrderStatusMock } = setup(); - - seedIntentHistory(controller); - - getOrderStatusMock.mockResolvedValueOnce({ - id: 'order-1', - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, + const getOrderStatusSpy = jest + .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') + .mockImplementation( + jest.fn().mockResolvedValueOnce({ + id: 'order-1', + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }), + ); + + const { controller } = setup({ + mockTxHistory: { + 'order-1': { + txMetaId: 'order-1', + originalTransactionId: 'order-1', + quote: minimalIntentQuoteResponse().quote, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xhash' }, + }, + }, + }, }); + jest + .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') + .mockImplementation( + jest.fn().mockResolvedValue({ + id: 'order-1', + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }), + ); await controller._executePoll({ bridgeTxMetaId: 'order-1' }); + expect(getOrderStatusSpy).toHaveBeenCalledWith( + 'order-1', + 'cowswap', + '1', + 'extension', + ); expect(controller.state.txHistory['order-1'].status.status).toBe( StatusTypes.SUBMITTED, ); + controller.stopAllPolling(); }); it('unknown intent order status maps to bridge UNKNOWN', async () => { - const { controller, getOrderStatusMock } = setup(); - - seedIntentHistory(controller); - - getOrderStatusMock.mockResolvedValueOnce({ - id: 'order-1', - status: 'SOME_NEW_STATUS' as any, // force UNKNOWN branch - txHash: undefined, - metadata: { txHashes: [] }, + const { controller } = setup({ + mockTxHistory: { + 'order-1': { + txMetaId: 'order-1', + originalTransactionId: 'order-1', + quote: minimalIntentQuoteResponse().quote, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xhash' }, + }, + }, + }, }); + jest + .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') + .mockImplementation( + jest.fn().mockResolvedValue({ + id: 'order-1', + status: 'SOME_NEW_STATUS' as any, // force UNKNOWN branch + txHash: undefined, + metadata: { txHashes: [] }, + }), + ); + await controller._executePoll({ bridgeTxMetaId: 'order-1' }); expect(controller.state.txHistory['order-1'].status.status).toBe( StatusTypes.UNKNOWN, ); + + controller.stopAllPolling(); }); it('intent polling: handles fetch failure when getIntentTransactionStatus returns undefined (e.g. non-Error rejection)', async () => { - const { controller, getOrderStatusMock } = setup(); - - seedIntentHistory(controller); + const { controller } = setup({ + mockTxHistory: { + 'order-1': { + txMetaId: 'order-1', + originalTransactionId: 'order-1', + quote: minimalIntentQuoteResponse().quote, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xhash' }, + }, + }, + }, + }); - getOrderStatusMock.mockRejectedValueOnce('non-Error rejection'); + jest + .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') + .mockImplementation(jest.fn().mockRejectedValue('non-Error rejection')); await controller._executePoll({ bridgeTxMetaId: 'order-1' }); @@ -1596,13 +1412,17 @@ describe('BridgeStatusController (target uncovered branches)', () => { StatusTypes.PENDING, ); expect(controller.state.txHistory['order-1'].attempts).toBeUndefined(); + controller.stopAllPolling(); }); it('bridge polling: returns early when history item is missing', async () => { - const { controller, fetchBridgeTxStatusMock } = setup(); - + const { controller } = setup(); + const fetchBridgeTxStatusSpy = jest.spyOn( + bridgeStatusUtils, + 'fetchBridgeTxStatus', + ); await controller._executePoll({ bridgeTxMetaId: 'missing-history' }); - expect(fetchBridgeTxStatusMock).not.toHaveBeenCalled(); + expect(fetchBridgeTxStatusSpy).not.toHaveBeenCalled(); }); }); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent.ts index 4099a4d0623..bc00a905228 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.ts @@ -12,7 +12,7 @@ import { IntentSubmissionParams, translateIntentOrderToBridgeStatus, } from './utils/intent-api'; -import { IntentOrder, IntentOrderStatus } from './utils/validators'; +import { SubmitIntentResponse, IntentOrderStatus } from './utils/validators'; type IntentStatuses = { orderStatus: IntentOrderStatus; @@ -59,14 +59,14 @@ export class IntentManager { #setIntentStatuses( bridgeTxMetaId: string, - order: IntentOrder, + order: SubmitIntentResponse, srcChainId: number, txHash: string, ): IntentStatuses { const bridgeStatus = translateIntentOrderToBridgeStatus( order, srcChainId, - txHash.toString(), + txHash, ); const intentStatuses: IntentStatuses = { orderStatus: order.status, @@ -87,16 +87,11 @@ export class IntentManager { getIntentTransactionStatus = async ( bridgeTxMetaId: string, - historyItem: BridgeHistoryItem, + srcChainId: number, + protocol: string, clientId: string, + txHash: string = '', ): Promise => { - const { - status: statusObj, - quote: { srcChainId, intent }, - } = historyItem; - const txHash = statusObj?.srcChain?.txHash ?? ''; - const protocol = intent?.protocol ?? ''; - try { const orderStatus = await this.intentApi.getOrderStatus( bridgeTxMetaId, @@ -214,7 +209,7 @@ export class IntentManager { submitIntent = async ( submissionParams: IntentSubmissionParams, clientId: string, - ): Promise => { + ): Promise => { return this.intentApi.submitIntent(submissionParams, clientId); }; } 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 c336673ce2a..a8129b5a024 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -3502,6 +3502,7 @@ describe('BridgeStatusController', () => { expect(addTransactionFn).not.toHaveBeenCalled(); expect(addTransactionBatchFn).toHaveBeenCalledTimes(1); expect(mockMessengerCall).toHaveBeenCalledTimes(6); + expect(controller.state.txHistory[result.id]).toMatchSnapshot(); }); it('should successfully submit an EVM swap transaction with no approval', async () => { @@ -3529,6 +3530,7 @@ describe('BridgeStatusController', () => { { ...quoteWithoutApproval, quote: { ...quoteWithoutApproval.quote, destAsset: erc20Token }, + gasFee: undefined as never, }, false, ); @@ -3576,6 +3578,70 @@ describe('BridgeStatusController', () => { }, }, }, + sentAmount: { amount: null, valueInCurrency: null, usd: null }, + } as never, + false, // isStxEnabledOnClient = FALSE (key for this test) + ); + controller.stopAllPolling(); + + // Should use single tx path (addTransactionFn), NOT batch path + expect(addTransactionFn).toHaveBeenCalledTimes(1); + expect(addTransactionBatchFn).not.toHaveBeenCalled(); + + // Should NOT estimate gas (uses quote's txFee instead) + expect(estimateGasFeeFn).not.toHaveBeenCalled(); + + // Verify the tx params have hex-converted gas fees from quote + const txParams = addTransactionFn.mock.calls[0][0]; + expect(txParams.maxFeePerGas).toBe('0x154a94'); // toHex(1395348) + expect(txParams.maxPriorityFeePerGas).toBe('0xf4241'); // toHex(1000001) + expect(txParams.gas).toBe('0x5208'); + + expect(result).toMatchSnapshot(); + expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); + expect(controller.state.txHistory[result.id]).toMatchSnapshot(); + }); + + it('should use quote txFee when gasIncluded is true and STX is off (undefined gasLimit)', async () => { + setupEventTrackingMocks(mockMessengerCall); + // Setup for single tx path - no gas estimation needed since gasIncluded=true + mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); + mockMessengerCall.mockReturnValueOnce('arbitrum'); + // Skip GasFeeController mock since we use quote's txFee directly + addTransactionFn.mockResolvedValueOnce({ + transactionMeta: mockEvmTxMeta, + result: Promise.resolve('0xevmTxHash'), + }); + mockMessengerCall.mockReturnValueOnce({ + transactions: [mockEvmTxMeta], + }); + mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); + + const { controller, startPollingForBridgeTxStatusSpy } = + getController(mockMessengerCall); + + const { approval, ...quoteWithoutApproval } = mockEvmQuoteResponse; + const result = await controller.submitTx( + (mockEvmQuoteResponse.trade as TxData).from, + { + ...quoteWithoutApproval, + quote: { + ...quoteWithoutApproval.quote, + gasIncluded: true, + gasIncluded7702: false, + feeData: { + ...quoteWithoutApproval.quote.feeData, + txFee: { + maxFeePerGas: '1395348', // Decimal string from quote + maxPriorityFeePerGas: '1000001', + }, + }, + }, + trade: { + ...quoteWithoutApproval.trade, + gasLimit: undefined, + }, + sentAmount: { amount: null, valueInCurrency: null, usd: null }, } as never, false, // isStxEnabledOnClient = FALSE (key for this test) ); @@ -3592,6 +3658,7 @@ describe('BridgeStatusController', () => { const txParams = addTransactionFn.mock.calls[0][0]; expect(txParams.maxFeePerGas).toBe('0x154a94'); // toHex(1395348) expect(txParams.maxPriorityFeePerGas).toBe('0xf4241'); // toHex(1000001) + expect(txParams.gas).toBeUndefined(); expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); @@ -3671,6 +3738,46 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); }); + it('should use batch path when gasIncluded7702 is true regardless of STX setting (with approval)', async () => { + setupEventTrackingMocks(mockMessengerCall); + mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); + mockMessengerCall.mockReturnValueOnce('arbitrum'); + addTransactionBatchFn.mockResolvedValueOnce({ + batchId: 'batchId1', + }); + mockMessengerCall.mockReturnValueOnce({ + transactions: [ + { ...mockApprovalTxMeta, batchId: 'batchId1' }, + { ...mockEvmTxMeta, batchId: 'batchId1' }, + ], + }); + + const { controller } = getController(mockMessengerCall); + + const result = await controller.submitTx( + (mockEvmQuoteResponse.trade as TxData).from, + { + ...mockEvmQuoteResponse, + quote: { + ...mockEvmQuoteResponse.quote, + gasIncluded: true, + gasIncluded7702: true, // 7702 takes precedence → batch path + feeData: { + ...mockEvmQuoteResponse.quote.feeData, + txFee: { + maxFeePerGas: '1395348', + maxPriorityFeePerGas: '1000001', + }, + }, + }, + } as never, + false, // STX off, but gasIncluded7702 = true forces batch path + ); + controller.stopAllPolling(); + + expect(result).toMatchSnapshot(); + }); + it('should handle smart transactions', async () => { setupEventTrackingMocks(mockMessengerCall); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 144a68f470a..020f075d152 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -6,6 +6,7 @@ import type { TxData, QuoteResponse, Trade, + TronTradeData, } from '@metamask/bridge-controller'; import { formatChainIdToHex, @@ -567,7 +568,7 @@ export class BridgeStatusController extends StaticIntervalPollingController => { - if (approval) { + if (approval && isEvmTxData(approval)) { const approveTx = async (): Promise => { await this.#handleUSDTAllowanceReset(resetApproval); @@ -1217,8 +1218,8 @@ export class BridgeStatusController extends StaticIntervalPollingController { + error.failures().forEach(({ path }) => { const aggregatorId = - branch?.[0]?.quote?.bridgeId ?? - branch?.[0]?.quote?.bridges?.[0] ?? (rawTxStatus as StatusResponse)?.bridge ?? (statusRequest.bridge || statusRequest.bridgeId) ?? ('unknown' as string); diff --git a/packages/bridge-status-controller/src/utils/intent-api.ts b/packages/bridge-status-controller/src/utils/intent-api.ts index 7be6c6047e7..88e111f1802 100644 --- a/packages/bridge-status-controller/src/utils/intent-api.ts +++ b/packages/bridge-status-controller/src/utils/intent-api.ts @@ -2,9 +2,9 @@ import { getClientHeaders, StatusTypes } from '@metamask/bridge-controller'; import { TransactionStatus } from '@metamask/transaction-controller'; import { - IntentOrder, + SubmitIntentResponse, IntentOrderStatus, - validateIntentOrderResponse, + validateSubmitIntentResponse, } from './validators'; import type { FetchFunction, StatusResponse } from '../types'; @@ -21,13 +21,13 @@ export type IntentApi = { submitIntent( params: IntentSubmissionParams, clientId: string, - ): Promise; + ): Promise; getOrderStatus( orderId: string, aggregatorId: string, srcChainId: string, clientId: string, - ): Promise; + ): Promise; }; export type GetJwtFn = () => Promise; @@ -48,7 +48,7 @@ export class IntentApiImpl implements IntentApi { async submitIntent( params: IntentSubmissionParams, clientId: string, - ): Promise { + ): Promise { const endpoint = `${this.#baseUrl}/submitOrder`; try { const jwt = await this.#getJwt(); @@ -60,7 +60,7 @@ export class IntentApiImpl implements IntentApi { }, body: JSON.stringify(params), }); - if (!validateIntentOrderResponse(response)) { + if (!validateSubmitIntentResponse(response)) { throw new Error('Invalid submitOrder response'); } return response; @@ -77,7 +77,7 @@ export class IntentApiImpl implements IntentApi { aggregatorId: string, srcChainId: string, clientId: string, - ): Promise { + ): Promise { const endpoint = `${this.#baseUrl}/getOrderStatus?orderId=${orderId}&aggregatorId=${encodeURIComponent(aggregatorId)}&srcChainId=${srcChainId}`; try { const jwt = await this.#getJwt(); @@ -85,7 +85,7 @@ export class IntentApiImpl implements IntentApi { method: 'GET', headers: getClientHeaders({ clientId, jwt }), }); - if (!validateIntentOrderResponse(response)) { + if (!validateSubmitIntentResponse(response)) { throw new Error('Invalid getOrderStatus response'); } return response; @@ -105,7 +105,7 @@ export type IntentBridgeStatus = { }; export const translateIntentOrderToBridgeStatus = ( - intentOrder: IntentOrder, + intentOrder: SubmitIntentResponse, srcChainId: number, fallbackTxHash?: string, ): IntentBridgeStatus => { diff --git a/packages/bridge-status-controller/src/utils/validators.ts b/packages/bridge-status-controller/src/utils/validators.ts index e0054f3a0fc..6110e973738 100644 --- a/packages/bridge-status-controller/src/utils/validators.ts +++ b/packages/bridge-status-controller/src/utils/validators.ts @@ -70,16 +70,7 @@ export enum IntentOrderStatus { EXPIRED = 'expired', } -export type IntentOrder = { - id: string; - status: IntentOrderStatus; - txHash?: string; - metadata: { - txHashes?: string[] | string; - }; -}; - -export const IntentOrderResponseSchema = type({ +const SubmitIntentResponseSchema = type({ id: string(), status: enums(Object.values(IntentOrderStatus)), txHash: optional(string()), @@ -88,8 +79,10 @@ export const IntentOrderResponseSchema = type({ }), }); -export const validateIntentOrderResponse = ( +export type SubmitIntentResponse = Infer; + +export const validateSubmitIntentResponse = ( data: unknown, -): data is Infer => { - return is(data, IntentOrderResponseSchema); +): data is SubmitIntentResponse => { + return is(data, SubmitIntentResponseSchema); }; From 4d35140dbde70bb92c4667026e172460468c1942 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 11 Mar 2026 14:10:54 -0700 Subject: [PATCH 02/23] fix: undo some changes --- .../bridge-controller/src/utils/validators.ts | 2 +- .../bridge-status-controller.test.ts.snap | 4 +- .../bridge-status-controller.intent.test.ts | 512 +++++++++--------- .../src/bridge-status-controller.intent.ts | 14 +- .../src/bridge-status-controller.test.ts | 2 +- .../src/bridge-status-controller.ts | 14 +- .../bridge-status-controller/src/types.ts | 2 +- .../src/utils/intent-api.ts | 39 +- .../src/utils/validators.ts | 10 +- 9 files changed, 313 insertions(+), 286 deletions(-) diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 555d6dae3f2..f2e85cb1bb8 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -339,7 +339,7 @@ export const IntentSchema = type({ /** * Optional settlement contract address used for execution. */ - settlementContract: HexAddressSchema, + settlementContract: optional(HexAddressSchema), /** * Optional EIP-712 typed data payload for signing. 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 2d7201b2f42..a07635096a8 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 @@ -3616,8 +3616,8 @@ exports[`BridgeStatusController submitTx: EVM swap should use quote txFee when g "location": "Main View", "originalTransactionId": "test-tx-id", "pricingData": { - "amountSent": "0", - "amountSentInUsd": undefined, + "amountSent": "1.234", + "amountSentInUsd": "1.01", "quotedGasAmount": ".00055", "quotedGasInUsd": "2.5778", "quotedReturnInUsd": "0.134214", diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts index d2b24e94824..ab479eb9055 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts @@ -289,14 +289,15 @@ describe('BridgeStatusController (intent swaps)', () => { // In the "throw on approval confirmation failure" behavior, we should not reach intent submission, // but keep this here to prove it wasn't used. + const intentStatusResponse = { + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }; const submitIntentSpy = jest .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') - .mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + .mockResolvedValue(intentStatusResponse); const quoteResponse = minimalIntentQuoteResponse({ // Include approval to exercise the approval confirmation path. @@ -335,15 +336,15 @@ describe('BridgeStatusController (intent swaps)', () => { approvalStatus: TransactionStatus.confirmed, }); const orderUid = 'order-uid-approve-1'; - + const intentStatusResponse = { + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }; const submitIntentSpy = jest .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') - .mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + .mockResolvedValue(intentStatusResponse); const quoteResponse = minimalIntentQuoteResponse({ approval: { @@ -374,14 +375,15 @@ describe('BridgeStatusController (intent swaps)', () => { }); const orderUid = 'order-uid-approve-2'; + const intentStatusResponse = { + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }; const submitIntentSpy = jest .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') - .mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + .mockResolvedValue(intentStatusResponse); const quoteResponse = minimalIntentQuoteResponse({ approval: { @@ -410,14 +412,15 @@ describe('BridgeStatusController (intent swaps)', () => { const orderUid = 'order-uid-log-1'; + const intentStatusResponse = { + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }; jest .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') - .mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + .mockResolvedValue(intentStatusResponse); jest .spyOn(transactionUtils, 'getStatusRequestParams') @@ -448,14 +451,16 @@ describe('BridgeStatusController (intent swaps)', () => { const { controller, messenger, accountAddress } = setup(); const orderUid = 'order-uid-signed-in-core-1'; + + const intentStatusResponse = { + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }; const submitIntentSpy = jest .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') - .mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + .mockResolvedValue(intentStatusResponse); const quoteResponse = minimalIntentQuoteResponse(); quoteResponse.quote.intent.typedData = { @@ -503,14 +508,15 @@ describe('BridgeStatusController (intent swaps)', () => { const orderUid = 'order-uid-2'; + const intentStatusResponse = { + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }; jest .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') - .mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + .mockResolvedValue(intentStatusResponse); const quoteResponse = minimalIntentQuoteResponse(); @@ -521,14 +527,15 @@ describe('BridgeStatusController (intent swaps)', () => { const historyKey = orderUid; + const intentStatusResponseCompleted = { + id: orderUid, + status: IntentOrderStatus.COMPLETED, + txHash: '0xnewhash', + metadata: { txHashes: ['0xold1', '0xnewhash'] }, + }; jest .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') - .mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.COMPLETED, - txHash: '0xnewhash', - metadata: { txHashes: ['0xold1', '0xnewhash'] }, - }); + .mockResolvedValue(intentStatusResponseCompleted); await controller._executePoll({ bridgeTxMetaId: historyKey }); @@ -545,14 +552,15 @@ describe('BridgeStatusController (intent swaps)', () => { const orderUid = 'order-uid-expired-1'; + const intentStatusResponse = { + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }; jest .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') - .mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + .mockResolvedValue(intentStatusResponse); const quoteResponse = minimalIntentQuoteResponse(); @@ -566,14 +574,15 @@ describe('BridgeStatusController (intent swaps)', () => { // Remove TC tx so update branch logs "transaction not found" transactions.splice(0, transactions.length); + const intentStatusResponsePending = { + id: orderUid, + status: IntentOrderStatus.PENDING, + txHash: '0xonlyhash', + metadata: { txHashes: [] }, // forces fallback to txHash + }; jest .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') - .mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.PENDING, - txHash: '0xonlyhash', - metadata: { txHashes: [] }, // forces fallback to txHash - }); + .mockResolvedValue(intentStatusResponsePending); await controller._executePoll({ bridgeTxMetaId: historyKey }); @@ -590,14 +599,15 @@ describe('BridgeStatusController (intent swaps)', () => { const orderUid = 'order-uid-expired-1'; + const intentStatusResponse = { + id: orderUid, + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }; jest .spyOn(intentApi.IntentApiImpl.prototype, 'submitIntent') - .mockResolvedValue({ - id: orderUid, - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }); + .mockResolvedValue(intentStatusResponse); const quoteResponse = minimalIntentQuoteResponse(); @@ -683,29 +693,30 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () }); it('transactionFailed subscription: marks main tx as FAILED and tracks (non-rejected)', async () => { - const { controller, messenger } = setup({ - mockTxHistory: { - bridgeTxMetaId1: { - txMetaId: 'bridgeTxMetaId1', - originalTransactionId: 'bridgeTxMetaId1', - quote: { - srcChainId: 1, - destChainId: 10, - srcAsset: { - address: '0x0000000000000000000000000000000000000000', - assetId: 'eip155:1/slip44:60', - }, - destAsset: { assetId: 'eip155:10/slip44:60' }, - bridges: ['across'], - bridgeId: 'rango', - }, - account: '0xAccount1', - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '0xsrc' }, + const mockTxHistory = { + bridgeTxMetaId1: { + txMetaId: 'bridgeTxMetaId1', + originalTransactionId: 'bridgeTxMetaId1', + quote: { + srcChainId: 1, + destChainId: 10, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', }, + destAsset: { assetId: 'eip155:10/slip44:60' }, + bridges: ['across'], + bridgeId: 'rango', + }, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xsrc' }, }, }, + }; + const { controller, messenger } = setup({ + mockTxHistory, }); const failedCb = messenger.subscribe.mock.calls.find( @@ -739,33 +750,34 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () }); it('transactionFailed subscription: maps approval tx id back to main history item', async () => { - const { controller, messenger } = setup({ - mockTxHistory: { - mainTx: { - txMetaId: 'mainTx', - originalTransactionId: 'mainTx', - approvalTxId: 'approvalTx', - quote: { - srcChainId: 1, - destChainId: 10, - srcAsset: { - address: '0x0000000000000000000000000000000000000000', - assetId: 'eip155:1/slip44:60', - }, - destAsset: { - address: '0x0000000000000000000000000000000000000000', - assetId: 'eip155:10/slip44:60', - }, - bridges: ['cowswap'], - bridgeId: 'cowswap', + const mockTxHistory = { + mainTx: { + txMetaId: 'mainTx', + originalTransactionId: 'mainTx', + approvalTxId: 'approvalTx', + quote: { + srcChainId: 1, + destChainId: 10, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', }, - account: '0xAccount1', - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '0xsrc' }, + destAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:10/slip44:60', }, + bridges: ['cowswap'], + bridgeId: 'cowswap', + }, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xsrc' }, }, }, + }; + const { controller, messenger } = setup({ + mockTxHistory, }); const failedCb = messenger.subscribe.mock.calls.find( ([evt]: [any]) => evt === 'TransactionController:transactionFailed', @@ -788,32 +800,33 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () }); it('transactionConfirmed subscription: tracks swap Completed; starts polling on bridge confirmed', async () => { - const { messenger, controller, startPollingSpy } = setup({ - mockTxHistory: { - bridgeConfirmed1: { - txMetaId: 'bridgeConfirmed1', - originalTransactionId: 'bridgeConfirmed1', - quote: { - srcChainId: 1, - destChainId: 10, - srcAsset: { - address: '0x0000000000000000000000000000000000000000', - assetId: 'eip155:1/slip44:60', - }, - destAsset: { - address: '0x0000000000000000000000000000000000000000', - assetId: 'eip155:10/slip44:60', - }, - bridges: ['cowswap'], - bridgeId: 'cowswap', + const mockTxHistory = { + bridgeConfirmed1: { + txMetaId: 'bridgeConfirmed1', + originalTransactionId: 'bridgeConfirmed1', + quote: { + srcChainId: 1, + destChainId: 10, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', }, - account: '0xAccount1', - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '0xsrc' }, + destAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:10/slip44:60', }, + bridges: ['cowswap'], + bridgeId: 'cowswap', + }, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xsrc' }, }, }, + }; + const { messenger, controller, startPollingSpy } = setup({ + mockTxHistory, }); const confirmedCb = messenger.subscribe.mock.calls.find( @@ -857,33 +870,34 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () }); it('restartPollingForFailedAttempts: resets attempts and restarts polling via txHash lookup (bridge tx only)', async () => { - const { controller } = setup({ - mockTxHistory: { - bridgeTx1: { - txMetaId: 'bridgeTx1', - originalTransactionId: 'bridgeTx1', - quote: { - srcChainId: 1, - destChainId: 10, - srcAsset: { - address: '0x0000000000000000000000000000000000000000', - assetId: 'eip155:1/slip44:60', - }, - destAsset: { - address: '0x0000000000000000000000000000000000000000', - assetId: 'eip155:10/slip44:60', - }, - bridges: ['cowswap'], - bridgeId: 'cowswap', + const mockTxHistory = { + bridgeTx1: { + txMetaId: 'bridgeTx1', + originalTransactionId: 'bridgeTx1', + quote: { + srcChainId: 1, + destChainId: 10, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', }, - attempts: { counter: 7, lastAttemptTime: Date.now() }, - account: '0xAccount1', - status: { - status: StatusTypes.UNKNOWN, - srcChain: { chainId: 1, txHash: '0xhash-find-me' }, + destAsset: { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:10/slip44:60', }, + bridges: ['cowswap'], + bridgeId: 'cowswap', + }, + attempts: { counter: 7, lastAttemptTime: Date.now() }, + account: '0xAccount1', + status: { + status: StatusTypes.UNKNOWN, + srcChain: { chainId: 1, txHash: '0xhash-find-me' }, }, }, + }; + const { controller } = setup({ + mockTxHistory, }); expect(controller.state.txHistory.bridgeTx1.attempts).toStrictEqual( @@ -904,25 +918,26 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () }); it('restartPollingForFailedAttempts: does not restart polling for same-chain swap tx', async () => { - const { controller, startPollingSpy } = setup({ - mockTxHistory: { - swapTx1: { - txMetaId: 'swapTx1', - originalTransactionId: 'swapTx1', - quote: { - srcChainId: 1, - destChainId: 1, - srcAsset: { assetId: 'eip155:1/slip44:60' }, - destAsset: { assetId: 'eip155:1/slip44:60' }, - }, - attempts: { counter: 7, lastAttemptTime: 0 }, - account: '0xAccount1', - status: { - status: StatusTypes.UNKNOWN, - srcChain: { chainId: 1, txHash: '0xhash-samechain' }, - }, + const mockTxHistory = { + swapTx1: { + txMetaId: 'swapTx1', + originalTransactionId: 'swapTx1', + quote: { + srcChainId: 1, + destChainId: 1, + srcAsset: { assetId: 'eip155:1/slip44:60' }, + destAsset: { assetId: 'eip155:1/slip44:60' }, + }, + attempts: { counter: 7, lastAttemptTime: 0 }, + account: '0xAccount1', + status: { + status: StatusTypes.UNKNOWN, + srcChain: { chainId: 1, txHash: '0xhash-samechain' }, }, }, + }; + const { controller, startPollingSpy } = setup({ + mockTxHistory, }); controller.restartPollingForFailedAttempts({ txMetaId: 'swapTx1' }); @@ -976,19 +991,20 @@ describe('BridgeStatusController (target uncovered branches)', () => { }); it('transactionFailed: returns early for intent txs (swapMetaData.isIntentTx)', () => { - const { controller, messenger } = setup({ - mockTxHistory: { - tx1: { - txMetaId: 'tx1', - originalTransactionId: 'tx1', - quote: minimalIntentQuoteResponse().quote, - account: '0xAccount1', - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '0x' }, - }, + const mockTxHistory = { + tx1: { + txMetaId: 'tx1', + originalTransactionId: 'tx1', + quote: minimalIntentQuoteResponse().quote, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0x' }, }, }, + }; + const { controller, messenger } = setup({ + mockTxHistory, }); const failedCb = messenger.subscribe.mock.calls.find( @@ -1100,28 +1116,29 @@ describe('BridgeStatusController (target uncovered branches)', () => { }); it('bridge polling: returns early when shouldSkipFetchDueToFetchFailures returns true', async () => { - const { controller, accountAddress } = setup({ - mockTxHistory: { - skipPoll1: { - txMetaId: 'skipPoll1', - originalTransactionId: 'skipPoll1', - quote: { - srcChainId: 1, - destChainId: 137, - destAsset: { assetId: 'x' }, - bridges: ['rango'], - }, - account: '0xAccount1', - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '0xhash' }, - }, - attempts: { - counter: MAX_ATTEMPTS, - lastAttemptTime: Date.now(), - }, + const mockTxHistory = { + valFail1: { + txMetaId: 'valFail1', + originalTransactionId: 'valFail1', + quote: { + srcChainId: 1, + destChainId: 137, + destAsset: { assetId: 'x' }, + bridges: ['rango'], + }, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xhash' }, + }, + attempts: { + counter: MAX_ATTEMPTS, + lastAttemptTime: Date.now(), }, }, + }; + const { controller, accountAddress } = setup({ + mockTxHistory, }); const quoteResponse: any = { @@ -1137,19 +1154,20 @@ describe('BridgeStatusController (target uncovered branches)', () => { toTokenAmount: { usd: '0' }, }; + const statusResponse = { + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xhash' }, + }, + validationFailures: [], + }; const fetchBridgeTxStatusSpy = jest .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') - .mockResolvedValue({ - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '0xhash' }, - }, - validationFailures: [], - }); + .mockResolvedValue(statusResponse); controller.startPollingForBridgeTxStatus({ accountAddress, - bridgeTxMeta: { id: 'skipPoll1' }, + bridgeTxMeta: { id: 'valFail1' }, statusRequest: { srcChainId: 1, srcTxHash: '0xhash', destChainId: 10 }, quoteResponse, slippagePercentage: 0, @@ -1157,7 +1175,7 @@ describe('BridgeStatusController (target uncovered branches)', () => { isStxEnabled: false, } as any); - await controller._executePoll({ bridgeTxMetaId: 'skipPoll1' }); + await controller._executePoll({ bridgeTxMetaId: 'valFail1' }); expect(fetchBridgeTxStatusSpy).not.toHaveBeenCalledWith( expect.objectContaining({ @@ -1189,7 +1207,6 @@ describe('BridgeStatusController (target uncovered branches)', () => { srcChain: { chainId: 1, txHash: '0xhash' }, }); - await controller._executePoll({ bridgeTxMetaId: 'valFail1' }); await controller._executePoll({ bridgeTxMetaId: 'valFail1' }); expect(controller.state.txHistory.valFail1.attempts).toStrictEqual( @@ -1219,23 +1236,25 @@ describe('BridgeStatusController (target uncovered branches)', () => { ], ] `); + controller.stopAllPolling(); }); it('track event: history has featureId => #trackUnifiedSwapBridgeEvent returns early (skip tracking)', () => { - const { controller, messenger } = setup({ - mockTxHistory: { - feat1: { - txMetaId: 'feat1', - originalTransactionId: 'feat1', - quote: minimalBridgeQuoteResponse('0xAccount1').quote, - account: '0xAccount1', - featureId: 'perps', - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '0x' }, - }, + const mockTxHistory = { + feat1: { + txMetaId: 'feat1', + originalTransactionId: 'feat1', + quote: minimalBridgeQuoteResponse('0xAccount1').quote, + account: '0xAccount1', + featureId: 'perps', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0x' }, }, }, + }; + const { controller, messenger } = setup({ + mockTxHistory, }); const failedCb = messenger.subscribe.mock.calls.find( @@ -1263,19 +1282,20 @@ describe('BridgeStatusController (target uncovered branches)', () => { }); it('intent order PENDING maps to bridge PENDING', async () => { - const { controller } = setup({ - mockTxHistory: { - 'order-1': { - txMetaId: 'order-1', - originalTransactionId: 'order-1', - quote: minimalIntentQuoteResponse().quote, - account: '0xAccount1', - status: { - status: StatusTypes.PENDING, - srcChain: { chainId: 1, txHash: '0xhash' }, - }, + const mockTxHistory = { + 'order-1': { + txMetaId: 'order-1', + originalTransactionId: 'order-1', + quote: minimalIntentQuoteResponse().quote, + account: '0xAccount1', + status: { + status: StatusTypes.PENDING, + srcChain: { chainId: 1, txHash: '0xhash' }, }, }, + }; + const { controller } = setup({ + mockTxHistory, }); jest @@ -1300,15 +1320,16 @@ describe('BridgeStatusController (target uncovered branches)', () => { }); it('intent order SUBMITTED maps to bridge SUBMITTED', async () => { + const orderStatusResponseSubmitted = { + id: 'order-1', + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }; const getOrderStatusSpy = jest .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') .mockImplementation( - jest.fn().mockResolvedValueOnce({ - id: 'order-1', - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }), + jest.fn().mockResolvedValueOnce(orderStatusResponseSubmitted), ); const { controller } = setup({ @@ -1319,29 +1340,28 @@ describe('BridgeStatusController (target uncovered branches)', () => { quote: minimalIntentQuoteResponse().quote, account: '0xAccount1', status: { - status: StatusTypes.PENDING, + status: StatusTypes.SUBMITTED, srcChain: { chainId: 1, txHash: '0xhash' }, }, }, }, }); + const orderStatusResponse = { + id: 'order-1', + status: IntentOrderStatus.SUBMITTED, + txHash: undefined, + metadata: { txHashes: [] }, + }; jest .spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus') - .mockImplementation( - jest.fn().mockResolvedValue({ - id: 'order-1', - status: IntentOrderStatus.SUBMITTED, - txHash: undefined, - metadata: { txHashes: [] }, - }), - ); + .mockImplementation(jest.fn().mockResolvedValue(orderStatusResponse)); await controller._executePoll({ bridgeTxMetaId: 'order-1' }); expect(getOrderStatusSpy).toHaveBeenCalledWith( 'order-1', 'cowswap', - '1', + 1, 'extension', ); expect(controller.state.txHistory['order-1'].status.status).toBe( diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent.ts index bc00a905228..4eb0bc3ed0f 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.ts @@ -1,4 +1,4 @@ -import { StatusTypes } from '@metamask/bridge-controller'; +import { BridgeClientId, StatusTypes } from '@metamask/bridge-controller'; import type { TransactionController } from '@metamask/transaction-controller'; import { TransactionMeta } from '@metamask/transaction-controller'; @@ -12,7 +12,7 @@ import { IntentSubmissionParams, translateIntentOrderToBridgeStatus, } from './utils/intent-api'; -import { SubmitIntentResponse, IntentOrderStatus } from './utils/validators'; +import { IntentStatusResponse, IntentOrderStatus } from './utils/validators'; type IntentStatuses = { orderStatus: IntentOrderStatus; @@ -59,7 +59,7 @@ export class IntentManager { #setIntentStatuses( bridgeTxMetaId: string, - order: SubmitIntentResponse, + order: IntentStatusResponse, srcChainId: number, txHash: string, ): IntentStatuses { @@ -89,14 +89,14 @@ export class IntentManager { bridgeTxMetaId: string, srcChainId: number, protocol: string, - clientId: string, + clientId: BridgeClientId, txHash: string = '', ): Promise => { try { const orderStatus = await this.intentApi.getOrderStatus( bridgeTxMetaId, protocol, - srcChainId.toString(), + srcChainId, clientId, ); @@ -208,8 +208,8 @@ export class IntentManager { */ submitIntent = async ( submissionParams: IntentSubmissionParams, - clientId: string, - ): Promise => { + clientId: BridgeClientId, + ): Promise => { return this.intentApi.submitIntent(submissionParams, clientId); }; } 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 a8129b5a024..38b8e64e288 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -18,6 +18,7 @@ import { FeatureId, getQuotesReceivedProperties, } from '@metamask/bridge-controller'; +// eslint-disable-next-line import-x/no-extraneous-dependencies import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { MessengerActions, @@ -3578,7 +3579,6 @@ describe('BridgeStatusController', () => { }, }, }, - sentAmount: { amount: null, valueInCurrency: null, usd: null }, } as never, false, // isStxEnabledOnClient = FALSE (key for this test) ); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 020f075d152..1d1ce898a29 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -568,7 +568,7 @@ export class BridgeStatusController extends StaticIntervalPollingController; + clientId: BridgeClientId, + ): Promise; getOrderStatus( orderId: string, aggregatorId: string, - srcChainId: string, - clientId: string, - ): Promise; + srcChainId: ChainId, + clientId: BridgeClientId, + ): Promise; }; export type GetJwtFn = () => Promise; @@ -47,8 +52,8 @@ export class IntentApiImpl implements IntentApi { async submitIntent( params: IntentSubmissionParams, - clientId: string, - ): Promise { + clientId: BridgeClientId, + ): Promise { const endpoint = `${this.#baseUrl}/submitOrder`; try { const jwt = await this.#getJwt(); @@ -60,7 +65,7 @@ export class IntentApiImpl implements IntentApi { }, body: JSON.stringify(params), }); - if (!validateSubmitIntentResponse(response)) { + if (!validateIntentStatusResponse(response)) { throw new Error('Invalid submitOrder response'); } return response; @@ -75,9 +80,9 @@ export class IntentApiImpl implements IntentApi { async getOrderStatus( orderId: string, aggregatorId: string, - srcChainId: string, - clientId: string, - ): Promise { + srcChainId: ChainId, + clientId: BridgeClientId, + ): Promise { const endpoint = `${this.#baseUrl}/getOrderStatus?orderId=${orderId}&aggregatorId=${encodeURIComponent(aggregatorId)}&srcChainId=${srcChainId}`; try { const jwt = await this.#getJwt(); @@ -85,7 +90,7 @@ export class IntentApiImpl implements IntentApi { method: 'GET', headers: getClientHeaders({ clientId, jwt }), }); - if (!validateSubmitIntentResponse(response)) { + if (!validateIntentStatusResponse(response)) { throw new Error('Invalid getOrderStatus response'); } return response; @@ -105,7 +110,7 @@ export type IntentBridgeStatus = { }; export const translateIntentOrderToBridgeStatus = ( - intentOrder: SubmitIntentResponse, + intentOrder: IntentStatusResponse, srcChainId: number, fallbackTxHash?: string, ): IntentBridgeStatus => { diff --git a/packages/bridge-status-controller/src/utils/validators.ts b/packages/bridge-status-controller/src/utils/validators.ts index 6110e973738..5437597498f 100644 --- a/packages/bridge-status-controller/src/utils/validators.ts +++ b/packages/bridge-status-controller/src/utils/validators.ts @@ -70,7 +70,7 @@ export enum IntentOrderStatus { EXPIRED = 'expired', } -const SubmitIntentResponseSchema = type({ +const IntentStatusResponseSchema = type({ id: string(), status: enums(Object.values(IntentOrderStatus)), txHash: optional(string()), @@ -79,10 +79,10 @@ const SubmitIntentResponseSchema = type({ }), }); -export type SubmitIntentResponse = Infer; +export type IntentStatusResponse = Infer; -export const validateSubmitIntentResponse = ( +export const validateIntentStatusResponse = ( data: unknown, -): data is SubmitIntentResponse => { - return is(data, SubmitIntentResponseSchema); +): data is IntentStatusResponse => { + return is(data, IntentStatusResponseSchema); }; From 2922251fe4e2ae9f384e4dc8c4e963e2e6347cb8 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 11 Mar 2026 14:18:08 -0700 Subject: [PATCH 03/23] chore: changelog --- packages/bridge-status-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 481dde43fcb..7d4b8a2fe96 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] +### Added + +- Added more unit test coverage for intents and EVM transactions. Also refactored some mocks and code blocks to improve testability ([#8186](https://github.com/MetaMask/core/pull/8186)) + ## [68.1.0] ### Added From 208dfa1434e50c792ea8e0e06bd77579681b0f38 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 10 Mar 2026 15:56:14 -0700 Subject: [PATCH 04/23] chore: call transactioncontroller handlers --- .../bridge-status-controller.test.ts.snap | 1209 ++++++++++++----- .../src/bridge-status-controller.test.ts | 382 +++--- .../src/bridge-status-controller.ts | 47 +- .../bridge-status-controller/src/types.ts | 8 + .../bridge-status-controller/src/utils/gas.ts | 11 +- .../src/utils/transaction.ts | 14 +- .../src/TransactionController.ts | 11 + packages/transaction-controller/src/index.ts | 1 + 8 files changed, 1097 insertions(+), 586 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 a07635096a8..e81fe673585 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 @@ -590,6 +590,43 @@ exports[`BridgeStatusController submitTx: EVM bridge should call handleMobileHar [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum-client-id", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xtokenContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xtokenContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum-client-id", + "origin": "metamask", + "requireApproval": true, + "type": "bridgeApproval", + }, + ], [ "TransactionController:getState", ], @@ -604,6 +641,43 @@ exports[`BridgeStatusController submitTx: EVM bridge should call handleMobileHar [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xbridgeContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xbridgeContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": true, + "type": "bridge", + }, + ], [ "TransactionController:getState", ], @@ -829,6 +903,43 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum-client-id", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xtokenContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xtokenContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum-client-id", + "origin": "metamask", + "requireApproval": false, + "type": "bridgeApproval", + }, + ], [ "TransactionController:getState", ], @@ -843,6 +954,40 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "to": "0xbridgeContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": undefined, + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xbridgeContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": false, + "type": "bridge", + }, + ], [ "TransactionController:getState", ], @@ -1068,6 +1213,43 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum-client-id", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xtokenContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xtokenContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum-client-id", + "origin": "metamask", + "requireApproval": false, + "type": "bridgeApproval", + }, + ], [ "TransactionController:getState", ], @@ -1082,6 +1264,40 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "to": "0xbridgeContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": undefined, + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xbridgeContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": false, + "type": "bridge", + }, + ], [ "TransactionController:getState", ], @@ -1263,58 +1479,6 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac `; exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions and include quotesReceivedContext 3`] = ` -[ - [ - { - "chainId": "0xa4b1", - "networkClientId": "arbitrum", - "transactionParams": { - "data": "0xdata", - "from": "0xaccount1", - "gas": "21000", - "to": "0xbridgeContract", - "value": "0x0", - }, - }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions and include quotesReceivedContext 4`] = ` -[ - [ - { - "disable7702": true, - "from": "0xaccount1", - "isGasFeeIncluded": false, - "isGasFeeSponsored": false, - "networkClientId": "arbitrum", - "origin": "metamask", - "requireApproval": false, - "transactions": [ - { - "assetsFiatValues": { - "receiving": "2.9999", - "sending": "2.00", - }, - "params": { - "data": "0xdata", - "from": "0xaccount1", - "gas": "0x5208", - "maxFeePerGas": "0x0", - "maxPriorityFeePerGas": "0x0", - "to": "0xbridgeContract", - "value": "0x0", - }, - "type": "bridge", - }, - ], - }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions and include quotesReceivedContext 5`] = ` [ [ "BridgeController:stopPollingForQuotes", @@ -1375,12 +1539,81 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac "GasFeeController:getState", ], [ - "TransactionController:getState", - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobileHardwareWalletDelay on extension 1`] = ` + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum", + "transactionParams": { + "data": "0xdata", + "from": "0xaccount1", + "gas": "21000", + "to": "0xbridgeContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransactionBatch", + { + "disable7702": true, + "from": "0xaccount1", + "isGasFeeIncluded": false, + "isGasFeeSponsored": false, + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": false, + "transactions": [ + { + "assetsFiatValues": { + "receiving": "2.9999", + "sending": "2.00", + }, + "params": { + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "bridge", + }, + ], + }, + ], + [ + "TransactionController:getState", + ], + [ + "TransactionController:updateTransaction", + { + "batchId": "batchId1", + "chainId": "0xa4b1", + "hash": "0xevmTxHash", + "id": "test-tx-id", + "status": "unapproved", + "time": 1234567890, + "txParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gasLimit": "0x5208", + "to": "0xbridgeContract", + "value": "0x0", + }, + "txReceipt": { + "effectiveGasPrice": "0x1880a", + "gasUsed": "0x2c92a", + }, + "type": "bridge", + }, + "Update tx type to bridge", + ], +] +`; + +exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobileHardwareWalletDelay on extension 1`] = ` { "chainId": "0xa4b1", "hash": "0xevmTxHash", @@ -1574,6 +1807,43 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum-client-id", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xtokenContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xtokenContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum-client-id", + "origin": "metamask", + "requireApproval": false, + "type": "bridgeApproval", + }, + ], [ "TransactionController:getState", ], @@ -1588,6 +1858,43 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xbridgeContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xbridgeContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": false, + "type": "bridge", + }, + ], [ "TransactionController:getState", ], @@ -1813,6 +2120,43 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum-client-id", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xtokenContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xtokenContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum-client-id", + "origin": "metamask", + "requireApproval": false, + "type": "bridgeApproval", + }, + ], [ "TransactionController:getState", ], @@ -1827,6 +2171,43 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xbridgeContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xbridgeContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": false, + "type": "bridge", + }, + ], [ "TransactionController:getState", ], @@ -2009,6 +2390,51 @@ exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 3`] = ` [ [ + "BridgeController:stopPollingForQuotes", + "Transaction submitted", + undefined, + ], + [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Submitted", + { + "action_type": "swapbridge-v1", + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:42161", + "custom_slippage": false, + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "location": "Main View", + "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, + }, + ], + [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + [ + "NetworkController:findNetworkClientIdByChainId", + "0x1", + ], + [ + "GasFeeController:getState", + ], + [ + "TransactionController:estimateGasFee", { "chainId": "0x1", "networkClientId": "arbitrum-client-id", @@ -2024,41 +2450,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance }, ], [ - { - "chainId": "0xa4b1", - "networkClientId": "arbitrum-client-id", - "transactionParams": { - "chainId": "0xa4b1", - "data": "0xapprovalData", - "from": "0xaccount1", - "gas": "21000", - "gasLimit": "21000", - "to": "0xtokenContract", - "value": "0x0", - }, - }, - ], - [ - { - "chainId": "0xa4b1", - "networkClientId": "arbitrum", - "transactionParams": { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gas": "21000", - "gasLimit": "21000", - "to": "0xbridgeContract", - "value": "0x0", - }, - }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 4`] = ` -[ - [ + "TransactionController:addTransaction", { "chainId": "0x1", "data": "0x095ea7b3000000000000000000000000881d40237659c251811cec9c364ef91dc08d300c0000000000000000000000000000000000000000000000000000000000000000", @@ -2079,94 +2471,56 @@ exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance }, ], [ + "TransactionController:getState", + ], + [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + [ + "NetworkController:findNetworkClientIdByChainId", + "0xa4b1", + ], + [ + "GasFeeController:getState", + ], + [ + "TransactionController:estimateGasFee", { "chainId": "0xa4b1", - "data": "0xapprovalData", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xtokenContract", - "value": "0x0", - }, - { - "actionId": "1234567890.456", "networkClientId": "arbitrum-client-id", - "origin": "metamask", - "requireApproval": false, - "type": "bridgeApproval", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xtokenContract", + "value": "0x0", + }, }, ], [ + "TransactionController:addTransaction", { "chainId": "0xa4b1", - "data": "0xdata", + "data": "0xapprovalData", "from": "0xaccount1", "gas": "0x5208", "gasLimit": "21000", "maxFeePerGas": undefined, "maxPriorityFeePerGas": undefined, - "to": "0xbridgeContract", + "to": "0xtokenContract", "value": "0x0", }, { "actionId": "1234567890.456", - "networkClientId": "arbitrum", + "networkClientId": "arbitrum-client-id", "origin": "metamask", "requireApproval": false, - "type": "bridge", - }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 5`] = ` -[ - [ - "BridgeController:stopPollingForQuotes", - "Transaction submitted", - undefined, - ], - [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Submitted", - { - "action_type": "swapbridge-v1", - "chain_id_destination": "eip155:10", - "chain_id_source": "eip155:42161", - "custom_slippage": false, - "gas_included": false, - "gas_included_7702": false, - "is_hardware_wallet": false, - "location": "Main View", - "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, + "type": "bridgeApproval", }, ], - [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - [ - "NetworkController:findNetworkClientIdByChainId", - "0x1", - ], - [ - "GasFeeController:getState", - ], [ "TransactionController:getState", ], @@ -2182,18 +2536,41 @@ exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance "GasFeeController:getState", ], [ - "TransactionController:getState", - ], - [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - [ - "NetworkController:findNetworkClientIdByChainId", - "0xa4b1", + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xbridgeContract", + "value": "0x0", + }, + }, ], [ - "GasFeeController:getState", + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xbridgeContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": false, + "type": "bridge", + }, ], [ "TransactionController:getState", @@ -2350,51 +2727,6 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit `; exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with approval 3`] = ` -[ - [ - { - "chainId": "0xa4b1", - "data": "0xapprovalData", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xtokenContract", - "value": "0x0", - }, - { - "actionId": "1234567890.456", - "networkClientId": "arbitrum-client-id", - "origin": "metamask", - "requireApproval": false, - "type": "bridgeApproval", - }, - ], - [ - { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xbridgeContract", - "value": "0x0", - }, - { - "actionId": "1234567890.456", - "networkClientId": "arbitrum", - "origin": "metamask", - "requireApproval": false, - "type": "bridge", - }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with approval 4`] = ` [ [ "BridgeController:stopPollingForQuotes", @@ -2440,6 +2772,43 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum-client-id", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xtokenContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xtokenContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum-client-id", + "origin": "metamask", + "requireApproval": false, + "type": "bridgeApproval", + }, + ], [ "TransactionController:getState", ], @@ -2454,6 +2823,43 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xbridgeContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xbridgeContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": false, + "type": "bridge", + }, + ], [ "TransactionController:getState", ], @@ -2609,51 +3015,6 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit `; exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 3`] = ` -[ - [ - { - "chainId": "0xa4b1", - "networkClientId": "arbitrum", - "transactionParams": { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gas": "21000", - "gasLimit": "21000", - "to": "0xbridgeContract", - "value": "0x0", - }, - }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 4`] = ` -[ - [ - { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xbridgeContract", - "value": "0x0", - }, - { - "actionId": "1234567890.456", - "networkClientId": "arbitrum", - "origin": "metamask", - "requireApproval": false, - "type": "bridge", - }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 5`] = ` [ [ "BridgeController:stopPollingForQuotes", @@ -2700,37 +3061,49 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit "GasFeeController:getState", ], [ - "TransactionController:getState", + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xbridgeContract", + "value": "0x0", + }, + }, ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should throw an error if approval tx fails 1`] = ` -[ [ + "TransactionController:addTransaction", { "chainId": "0xa4b1", - "data": "0xapprovalData", + "data": "0xdata", "from": "0xaccount1", "gas": "0x5208", "gasLimit": "21000", "maxFeePerGas": undefined, "maxPriorityFeePerGas": undefined, - "to": "0xtokenContract", + "to": "0xbridgeContract", "value": "0x0", }, { "actionId": "1234567890.456", - "networkClientId": "arbitrum-client-id", + "networkClientId": "arbitrum", "origin": "metamask", "requireApproval": false, - "type": "bridgeApproval", + "type": "bridge", }, ], + [ + "TransactionController:getState", + ], ] `; -exports[`BridgeStatusController submitTx: EVM bridge should throw an error if approval tx fails 2`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should throw an error if approval tx fails 1`] = ` [ [ "BridgeController:stopPollingForQuotes", @@ -2776,12 +3149,24 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap [ "GasFeeController:getState", ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should throw an error if approval tx meta does not exist 1`] = ` -[ [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum-client-id", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xtokenContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", { "chainId": "0xa4b1", "data": "0xapprovalData", @@ -2804,7 +3189,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap ] `; -exports[`BridgeStatusController submitTx: EVM bridge should throw an error if approval tx meta does not exist 2`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should throw an error if approval tx meta does not exist 1`] = ` [ [ "BridgeController:stopPollingForQuotes", @@ -2850,6 +3235,43 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum-client-id", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xtokenContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xtokenContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum-client-id", + "origin": "metamask", + "requireApproval": false, + "type": "bridgeApproval", + }, + ], [ "TransactionController:getState", ], @@ -3138,6 +3560,51 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti exports[`BridgeStatusController submitTx: EVM swap should handle smart transactions 3`] = ` [ [ + "BridgeController:stopPollingForQuotes", + "Transaction submitted", + undefined, + ], + [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Submitted", + { + "action_type": "swapbridge-v1", + "chain_id_destination": "eip155:42161", + "chain_id_source": "eip155:42161", + "custom_slippage": false, + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "location": "Main View", + "price_impact": 0, + "provider": "lifi_across", + "quoted_time_minutes": 0, + "stx_enabled": true, + "swap_type": "single_chain", + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", + "usd_amount_source": 1.01, + "usd_quoted_gas": 2.5778, + "usd_quoted_return": 0, + }, + ], + [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + [ + "NetworkController:findNetworkClientIdByChainId", + "0xa4b1", + ], + [ + "GasFeeController:getState", + ], + [ + "TransactionController:estimateGasFee", { "chainId": "0xa4b1", "networkClientId": "arbitrum", @@ -3151,6 +3618,10 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti }, ], [ + "GasFeeController:getState", + ], + [ + "TransactionController:estimateGasFee", { "chainId": "0xa4b1", "networkClientId": "arbitrum", @@ -3163,12 +3634,8 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti }, }, ], -] -`; - -exports[`BridgeStatusController submitTx: EVM swap should handle smart transactions 4`] = ` -[ [ + "TransactionController:addTransactionBatch", { "disable7702": true, "from": "0xaccount1", @@ -3209,60 +3676,29 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti ], }, ], -] -`; - -exports[`BridgeStatusController submitTx: EVM swap should handle smart transactions 5`] = ` -[ - [ - "BridgeController:stopPollingForQuotes", - "Transaction submitted", - undefined, - ], [ - "AccountsController:getAccountByAddress", - "0xaccount1", + "TransactionController:getState", ], [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Submitted", + "TransactionController:updateTransaction", { - "action_type": "swapbridge-v1", - "chain_id_destination": "eip155:42161", - "chain_id_source": "eip155:42161", - "custom_slippage": false, - "gas_included": false, - "gas_included_7702": false, - "is_hardware_wallet": false, - "location": "Main View", - "price_impact": 0, - "provider": "lifi_across", - "quoted_time_minutes": 0, - "stx_enabled": true, - "swap_type": "single_chain", - "token_symbol_destination": "ETH", - "token_symbol_source": "ETH", - "usd_amount_source": 1.01, - "usd_quoted_gas": 2.5778, - "usd_quoted_return": 0, + "batchId": "batchId1", + "chainId": "0xa4b1", + "hash": "0xevmTxHash", + "id": "test-tx-id", + "status": "unapproved", + "time": 1234567890, + "txParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gasLimit": "0x5208", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "swap", }, - ], - [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - [ - "NetworkController:findNetworkClientIdByChainId", - "0xa4b1", - ], - [ - "GasFeeController:getState", - ], - [ - "GasFeeController:getState", - ], - [ - "TransactionController:getState", + "Update tx type to swap", ], ] `; @@ -3327,6 +3763,43 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum-client-id", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xtokenContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xtokenContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum-client-id", + "origin": "metamask", + "requireApproval": false, + "type": "swapApproval", + }, + ], [ "TransactionController:getState", ], @@ -3341,6 +3814,43 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xbridgeContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xbridgeContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": false, + "type": "swap", + }, + ], [ "TransactionController:getState", ], @@ -3537,6 +4047,43 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an [ "GasFeeController:getState", ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xbridgeContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xbridgeContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": false, + "type": "swap", + }, + ], [ "TransactionController:getState", ], 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 38b8e64e288..92b87fc69df 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -13,7 +13,6 @@ import { ChainId, FeeType, StatusTypes, - BridgeController, getNativeAssetForChainId, FeatureId, getQuotesReceivedProperties, @@ -609,10 +608,6 @@ const executePollingWithPendingStatus = async () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), config: {}, }); const startPollingSpy = jest.spyOn(bridgeStatusController, 'startPolling'); @@ -649,11 +644,6 @@ const mockSelectedAccount = { }, }; -const addTransactionFn = jest.fn(); -const addTransactionBatchFn = jest.fn(); -const updateTransactionFn = jest.fn(); -const estimateGasFeeFn = jest.fn(); - const getController = ( call: jest.Mock, traceFn?: jest.Mock, @@ -670,10 +660,6 @@ const getController = ( } as never, clientId, fetchFn: mockFetchFn, - addTransactionFn, - addTransactionBatchFn, - estimateGasFeeFn, - updateTransactionFn, traceFn, }); @@ -697,10 +683,6 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); expect(bridgeStatusController.state).toStrictEqual(EMPTY_INIT_STATE); expect(mockMessengerSubscribe.mock.calls).toMatchSnapshot(); @@ -712,10 +694,6 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), state: { txHistory: MockTxHistory.getPending(), }, @@ -749,10 +727,6 @@ describe('BridgeStatusController', () => { .fn() .mockResolvedValueOnce(MockStatusResponse.getPending()) .mockResolvedValueOnce(MockStatusResponse.getComplete()), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); jest.advanceTimersByTime(10000); await flushPromises(); @@ -793,10 +767,6 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: mockFetchFn, - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); // Execution @@ -854,10 +824,6 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: failedFetch, - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); // Execution @@ -931,10 +897,6 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); const argsWithoutId = getMockStartPollingForBridgeTxStatusArgs(); @@ -955,10 +917,6 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); const argsWithoutMeta = getMockStartPollingForBridgeTxStatusArgs(); @@ -982,10 +940,6 @@ describe('BridgeStatusController', () => { fetchFn: jest .fn() .mockResolvedValueOnce(MockStatusResponse.getPending()), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); // Execution @@ -1025,10 +979,6 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest.spyOn( bridgeStatusUtils, @@ -1112,10 +1062,6 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); // Start polling with args that have no srcTxHash @@ -1153,10 +1099,6 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest @@ -1202,10 +1144,6 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); // Execution @@ -1272,10 +1210,6 @@ describe('BridgeStatusController', () => { fetchFn: jest .fn() .mockResolvedValueOnce(MockStatusResponse.getPending()), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), traceFn: jest.fn(), }); @@ -1352,10 +1286,6 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), state: EMPTY_INIT_STATE, }); @@ -1371,10 +1301,6 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), state: { txHistory: { bridgeTxMetaId1: { @@ -1467,10 +1393,6 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') @@ -1573,10 +1495,6 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') @@ -1692,10 +1610,6 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') @@ -2547,8 +2461,8 @@ describe('BridgeStatusController', () => { mockCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockResolvedValueOnce({ transactionMeta: mockApprovalTxMeta, result: Promise.resolve('0xapprovalTxHash'), }); @@ -2563,8 +2477,8 @@ describe('BridgeStatusController', () => { mockCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionFn.mockResolvedValueOnce({ + mockCall.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockCall.mockResolvedValueOnce({ transactionMeta: mockEvmTxMeta, result: Promise.resolve('0xevmTxHash'), }); @@ -2585,8 +2499,8 @@ describe('BridgeStatusController', () => { mockCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionBatchFn.mockResolvedValueOnce({ + mockCall.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockCall.mockResolvedValueOnce({ batchId: 'batchId1', }); mockCall.mockReturnValueOnce({ @@ -2616,7 +2530,6 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); expect(controller.state.txHistory[result.id]).toMatchSnapshot(); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); controller.stopAllPolling(); }); @@ -2653,15 +2566,13 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); expect(controller.state.txHistory[result.id]).toMatchSnapshot(); - expect(estimateGasFeeFn.mock.calls).toMatchSnapshot(); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); }); it('should handle smart transactions and include quotesReceivedContext', async () => { setupEventTrackingMocks(mockMessengerCall); setupBridgeStxMocks(mockMessengerCall); - addTransactionBatchFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce({ batchId: 'batchId1', }); @@ -2679,9 +2590,6 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); expect(controller.state.txHistory[result.id]).toMatchSnapshot(); - expect(estimateGasFeeFn.mock.calls).toMatchSnapshot(); - expect(addTransactionFn).not.toHaveBeenCalled(); - expect(addTransactionBatchFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); }); @@ -2705,7 +2613,10 @@ describe('BridgeStatusController', () => { controller.stopAllPolling(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(addTransactionFn).not.toHaveBeenCalled(); + const addTransactionCall = mockMessengerCall.mock.calls.find( + (call) => call[0] === 'TransactionController:addTransaction', + ); + expect(addTransactionCall).toBeUndefined(); }); it('should throw an error if EVM trade data is not valid', async () => { @@ -2731,7 +2642,10 @@ describe('BridgeStatusController', () => { controller.stopAllPolling(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(addTransactionFn).not.toHaveBeenCalled(); + const addTransactionCall = mockMessengerCall.mock.calls.find( + (call) => call[0] === 'TransactionController:addTransaction', + ); + expect(addTransactionCall).toBeUndefined(); }); it('should throw an error if Solana trade data is not valid', async () => { @@ -2760,7 +2674,10 @@ describe('BridgeStatusController', () => { controller.stopAllPolling(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(addTransactionFn).not.toHaveBeenCalled(); + const addTransactionCall = mockMessengerCall.mock.calls.find( + (call) => call[0] === 'TransactionController:addTransaction', + ); + expect(addTransactionCall).toBeUndefined(); }); it('should reset USDT allowance', async () => { @@ -2798,8 +2715,6 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); expect(controller.state.txHistory[result.id]).toMatchSnapshot(); - expect(estimateGasFeeFn.mock.calls).toMatchSnapshot(); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); }); @@ -2810,16 +2725,16 @@ describe('BridgeStatusController', () => { mockMessengerCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); mockMessengerCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); mockMessengerCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionBatchFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockResolvedValueOnce({ batchId: 'batchId1', }); mockMessengerCall.mockReturnValueOnce({ @@ -2852,10 +2767,28 @@ describe('BridgeStatusController', () => { expect(quote).toBeDefined(); expect(txMetaId).toBe(result.id); expect(batchId).toBe('batchId1'); - expect(estimateGasFeeFn).toHaveBeenCalledTimes(3); - expect(addTransactionFn).not.toHaveBeenCalled(); - expect(addTransactionBatchFn).toHaveBeenCalledTimes(1); - expect(mockMessengerCall).toHaveBeenCalledTimes(9); + const mockCalls = mockMessengerCall.mock.calls; + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:estimateGasFee', + ), + ).toHaveLength(3); + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:addTransaction', + ), + ).toHaveLength(0); + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:addTransactionBatch', + ), + ).toHaveLength(1); + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:updateTransaction', + ), + ).toHaveLength(1); + expect(mockMessengerCall).toHaveBeenCalledTimes(14); }); it('should throw an error if approval tx fails', async () => { @@ -2865,8 +2798,8 @@ describe('BridgeStatusController', () => { mockMessengerCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionFn.mockRejectedValueOnce(new Error('Approval tx failed')); + mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockRejectedValueOnce(new Error('Approval tx failed')); const { controller, startPollingForBridgeTxStatusSpy } = getController(mockMessengerCall); @@ -2880,7 +2813,6 @@ describe('BridgeStatusController', () => { ).rejects.toThrow('Approval tx failed'); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); }); @@ -2891,8 +2823,8 @@ describe('BridgeStatusController', () => { mockMessengerCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockResolvedValueOnce({ transactionMeta: undefined, result: new Promise((resolve) => resolve('0xevmTxHash')), }); @@ -2915,7 +2847,6 @@ describe('BridgeStatusController', () => { ); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); }); @@ -3188,7 +3119,7 @@ describe('BridgeStatusController', () => { mockMessengerCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce({ estimates: { high: { suggestedMaxFeePerGas: '0x1234', @@ -3198,7 +3129,7 @@ describe('BridgeStatusController', () => { }); // Trade tx fails during submission - addTransactionFn.mockRejectedValueOnce( + mockMessengerCall.mockRejectedValueOnce( new Error('Trade tx submission failed'), ); @@ -3373,8 +3304,8 @@ describe('BridgeStatusController', () => { mockMessengerCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockResolvedValueOnce({ transactionMeta: mockApprovalTxMeta, result: Promise.resolve('0xapprovalTxHash'), }); @@ -3389,8 +3320,8 @@ describe('BridgeStatusController', () => { mockMessengerCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockResolvedValueOnce({ transactionMeta: mockEvmTxMeta, result: Promise.resolve('0xevmTxHash'), }); @@ -3399,9 +3330,6 @@ describe('BridgeStatusController', () => { }); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); - // mockMessengerCall.mockReturnValueOnce({ - // transactions: [mockEvmTxMeta], - // }); }; it('should successfully submit an EVM swap transaction with approval', async () => { @@ -3422,8 +3350,12 @@ describe('BridgeStatusController', () => { expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); const { approvalTxId } = controller.state.txHistory[result.id]; expect(approvalTxId).toBe('test-approval-tx-id'); - expect(addTransactionFn).toHaveBeenCalledTimes(2); - expect(mockMessengerCall).toHaveBeenCalledTimes(11); + expect( + mockMessengerCall.mock.calls.filter( + ([action]) => action === 'TransactionController:addTransaction', + ), + ).toHaveLength(2); + expect(mockMessengerCall).toHaveBeenCalledTimes(15); }); it('should successfully submit an EVM swap transaction with featureId', async () => { @@ -3450,8 +3382,6 @@ describe('BridgeStatusController', () => { expect(controller.state.txHistory[result.id].featureId).toBe( FeatureId.PERPS, ); - expect(addTransactionFn).toHaveBeenCalledTimes(2); - expect(mockMessengerCall).toHaveBeenCalledTimes(10); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); }); @@ -3459,7 +3389,7 @@ describe('BridgeStatusController', () => { setupEventTrackingMocks(mockMessengerCall); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); mockMessengerCall.mockReturnValueOnce('arbitrum'); - addTransactionBatchFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce({ batchId: 'batchId1', }); mockMessengerCall.mockReturnValueOnce({ @@ -3500,9 +3430,12 @@ describe('BridgeStatusController', () => { } `); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(addTransactionFn).not.toHaveBeenCalled(); - expect(addTransactionBatchFn).toHaveBeenCalledTimes(1); - expect(mockMessengerCall).toHaveBeenCalledTimes(6); + expect( + mockMessengerCall.mock.calls.filter( + ([action]) => action === 'TransactionController:addTransaction', + ), + ).toHaveLength(0); + expect(mockMessengerCall).toHaveBeenCalledTimes(8); expect(controller.state.txHistory[result.id]).toMatchSnapshot(); }); @@ -3539,8 +3472,6 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); expect(controller.state.txHistory[result.id]).toMatchSnapshot(); - expect(estimateGasFeeFn).toHaveBeenCalledTimes(1); - expect(addTransactionFn).toHaveBeenCalledTimes(1); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); }); @@ -3550,7 +3481,7 @@ describe('BridgeStatusController', () => { mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); mockMessengerCall.mockReturnValueOnce('arbitrum'); // Skip GasFeeController mock since we use quote's txFee directly - addTransactionFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce({ transactionMeta: mockEvmTxMeta, result: Promise.resolve('0xevmTxHash'), }); @@ -3584,15 +3515,21 @@ describe('BridgeStatusController', () => { ); controller.stopAllPolling(); - // Should use single tx path (addTransactionFn), NOT batch path - expect(addTransactionFn).toHaveBeenCalledTimes(1); - expect(addTransactionBatchFn).not.toHaveBeenCalled(); + const mockCalls = mockMessengerCall.mock.calls; + // Should use single tx path (addTransactionFn), NOT batch path + const addTransactionCalls = mockCalls.filter( + ([action]) => action === 'TransactionController:addTransaction', + ); + expect(addTransactionCalls).toHaveLength(1); // Should NOT estimate gas (uses quote's txFee instead) - expect(estimateGasFeeFn).not.toHaveBeenCalled(); + const estimateGasFeeCalls = mockCalls.filter( + ([action]) => action === 'TransactionController:estimateGasFee', + ); + expect(estimateGasFeeCalls).toHaveLength(0); // Verify the tx params have hex-converted gas fees from quote - const txParams = addTransactionFn.mock.calls[0][0]; + const txParams = addTransactionCalls[0]?.[1]; expect(txParams.maxFeePerGas).toBe('0x154a94'); // toHex(1395348) expect(txParams.maxPriorityFeePerGas).toBe('0xf4241'); // toHex(1000001) expect(txParams.gas).toBe('0x5208'); @@ -3608,7 +3545,7 @@ describe('BridgeStatusController', () => { mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); mockMessengerCall.mockReturnValueOnce('arbitrum'); // Skip GasFeeController mock since we use quote's txFee directly - addTransactionFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce({ transactionMeta: mockEvmTxMeta, result: Promise.resolve('0xevmTxHash'), }); @@ -3647,15 +3584,27 @@ describe('BridgeStatusController', () => { ); controller.stopAllPolling(); - // Should use single tx path (addTransactionFn), NOT batch path - expect(addTransactionFn).toHaveBeenCalledTimes(1); - expect(addTransactionBatchFn).not.toHaveBeenCalled(); + const mockCalls = mockMessengerCall.mock.calls; // Should NOT estimate gas (uses quote's txFee instead) - expect(estimateGasFeeFn).not.toHaveBeenCalled(); + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:estimateGasFee', + ), + ).toHaveLength(0); + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:addTransactionBatch', + ), + ).toHaveLength(0); + // Should use single tx path (addTransactionFn), NOT batch path + const addTransactionCalls = mockCalls.filter( + ([action]) => action === 'TransactionController:addTransaction', + ); + expect(addTransactionCalls).toHaveLength(1); // Verify the tx params have hex-converted gas fees from quote - const txParams = addTransactionFn.mock.calls[0][0]; + const txParams = addTransactionCalls[0]?.[1]; expect(txParams.maxFeePerGas).toBe('0x154a94'); // toHex(1395348) expect(txParams.maxPriorityFeePerGas).toBe('0xf4241'); // toHex(1000001) expect(txParams.gas).toBeUndefined(); @@ -3688,9 +3637,22 @@ describe('BridgeStatusController', () => { controller.stopAllPolling(); // Should estimate gas since gasIncluded is false - expect(estimateGasFeeFn).toHaveBeenCalledTimes(1); - expect(addTransactionFn).toHaveBeenCalledTimes(1); - expect(addTransactionBatchFn).not.toHaveBeenCalled(); + const mockCalls = mockMessengerCall.mock.calls; + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:estimateGasFee', + ), + ).toHaveLength(1); + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:addTransaction', + ), + ).toHaveLength(1); + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:addTransactionBatch', + ), + ).toHaveLength(0); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); expect(result).toMatchSnapshot(); }); @@ -3699,7 +3661,7 @@ describe('BridgeStatusController', () => { setupEventTrackingMocks(mockMessengerCall); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); mockMessengerCall.mockReturnValueOnce('arbitrum'); - addTransactionBatchFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce({ batchId: 'batchId1', }); mockMessengerCall.mockReturnValueOnce({ @@ -3732,8 +3694,17 @@ describe('BridgeStatusController', () => { controller.stopAllPolling(); // Should use batch path because gasIncluded7702 = true - expect(addTransactionBatchFn).toHaveBeenCalledTimes(1); - expect(addTransactionFn).not.toHaveBeenCalled(); + const mockCalls = mockMessengerCall.mock.calls; + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:addTransactionBatch', + ), + ).toHaveLength(1); + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:addTransaction', + ), + ).toHaveLength(0); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); expect(result).toMatchSnapshot(); }); @@ -3742,7 +3713,7 @@ describe('BridgeStatusController', () => { setupEventTrackingMocks(mockMessengerCall); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); mockMessengerCall.mockReturnValueOnce('arbitrum'); - addTransactionBatchFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce({ batchId: 'batchId1', }); mockMessengerCall.mockReturnValueOnce({ @@ -3785,12 +3756,12 @@ describe('BridgeStatusController', () => { mockMessengerCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); mockMessengerCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionBatchFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockResolvedValueOnce({ batchId: 'batchId1', }); mockMessengerCall.mockReturnValueOnce({ @@ -3809,9 +3780,6 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); expect(controller.state.txHistory[result.id]).toMatchSnapshot(); - expect(estimateGasFeeFn.mock.calls).toMatchSnapshot(); - expect(addTransactionFn).not.toHaveBeenCalled(); - expect(addTransactionBatchFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); }); @@ -3833,9 +3801,22 @@ describe('BridgeStatusController', () => { controller.stopAllPolling(); expect(startPollingForBridgeTxStatusSpy).not.toHaveBeenCalled(); - expect(estimateGasFeeFn).not.toHaveBeenCalled(); - expect(addTransactionFn).not.toHaveBeenCalled(); - expect(addTransactionBatchFn).not.toHaveBeenCalled(); + const mockCalls = mockMessengerCall.mock.calls; + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:estimateGasFee', + ), + ).toHaveLength(0); + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:addTransaction', + ), + ).toHaveLength(0); + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:addTransactionBatch', + ), + ).toHaveLength(0); expect(mockMessengerCall).toHaveBeenCalledTimes(4); }); @@ -3846,12 +3827,12 @@ describe('BridgeStatusController', () => { mockMessengerCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); mockMessengerCall.mockReturnValueOnce({ gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionBatchFn.mockResolvedValueOnce({ + mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockResolvedValueOnce({ batchId: 'batchId1', }); mockMessengerCall.mockReturnValueOnce({ @@ -3872,10 +3853,23 @@ describe('BridgeStatusController', () => { controller.stopAllPolling(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(estimateGasFeeFn).toHaveBeenCalledTimes(2); - expect(addTransactionFn).not.toHaveBeenCalled(); - expect(addTransactionBatchFn).toHaveBeenCalledTimes(1); - expect(mockMessengerCall).toHaveBeenCalledTimes(8); + const mockCalls = mockMessengerCall.mock.calls; + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:estimateGasFee', + ), + ).toHaveLength(2); + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:addTransaction', + ), + ).toHaveLength(0); + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:addTransactionBatch', + ), + ).toHaveLength(1); + expect(mockMessengerCall).toHaveBeenCalledTimes(11); }); }); @@ -3889,10 +3883,6 @@ describe('BridgeStatusController', () => { messenger: mockMessenger, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), state: { txHistory: { ...MockTxHistory.getPending({ @@ -3915,10 +3905,6 @@ describe('BridgeStatusController', () => { messenger: mockMessenger, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), state: { txHistory: { bridgeTxMetaId1: { @@ -3955,10 +3941,6 @@ describe('BridgeStatusController', () => { messenger: mockMessenger, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), state: { txHistory: { bridgeTxMetaId1: { @@ -3995,10 +3977,6 @@ describe('BridgeStatusController', () => { messenger: mockMessenger, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), state: { txHistory: { bridgeTxMetaId1: { @@ -4063,10 +4041,6 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), state: { txHistory: { bridgeTxMetaId1: { @@ -4165,10 +4139,6 @@ describe('BridgeStatusController', () => { messenger: mockMessenger, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), state: { txHistory: { noHashTx: { @@ -4272,14 +4242,14 @@ describe('BridgeStatusController', () => { parent: mockMessenger, }); mockTrackEventFn = jest.fn(); - new BridgeController({ - messenger: mockBridgeMessenger, - clientId: BridgeClientId.EXTENSION, - fetchFn: jest.fn(), - trackMetaMetricsFn: mockTrackEventFn, - getLayer1GasFee: jest.fn(), - clientVersion: '13.4.0', - }); + // const bridgeController = new BridgeController({ + // messenger: mockBridgeMessenger, + // clientId: BridgeClientId.EXTENSION, + // fetchFn: jest.fn(), + // trackMetaMetricsFn: mockTrackEventFn, + // getLayer1GasFee: jest.fn(), + // clientVersion: '13.4.0', + // }); mockFetchFn = jest .fn() @@ -4292,10 +4262,6 @@ describe('BridgeStatusController', () => { messenger: mockBridgeStatusMessenger, clientId: BridgeClientId.EXTENSION, fetchFn: mockFetchFn, - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), state: { txHistory: { ...MockTxHistory.getPending(), diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 1d1ce898a29..295d49cab55 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -129,14 +129,6 @@ export class BridgeStatusController extends StaticIntervalPollingController; clientId: BridgeClientId; fetchFn: FetchFunction; - addTransactionFn: typeof TransactionController.prototype.addTransaction; - addTransactionBatchFn: typeof TransactionController.prototype.addTransactionBatch; - updateTransactionFn: typeof TransactionController.prototype.updateTransaction; - estimateGasFeeFn: typeof TransactionController.prototype.estimateGasFee; config?: { customBridgeApiBaseUrl?: string; }; @@ -177,10 +161,6 @@ export class BridgeStatusController extends StaticIntervalPollingController fn?.()) as TraceCallback); this.#intentManager = new IntentManager({ messenger: this.messenger, - updateTransactionFn: this.#updateTransactionFn, customBridgeApiBaseUrl: this.#config.customBridgeApiBaseUrl, fetchFn: this.#fetchFn, getJwt: this.#getJwt, @@ -1135,8 +1114,7 @@ export class BridgeStatusController extends StaticIntervalPollingController => { // Use provided actionId (for pre-submission history) or generate one - const actionId = providedActionId ?? generateActionId().toString(); - + const actionId = providedActionId ?? generateActionId(); const selectedAccount = this.messenger.call( 'AccountsController:getAccountByAddress', trade.from, @@ -1184,7 +1162,8 @@ export class BridgeStatusController extends StaticIntervalPollingController => { const transactionParams = await getAddTransactionBatchParams({ messenger: this.messenger, - estimateGasFeeFn: this.#estimateGasFeeFn, ...args, }); const txDataByType = { @@ -1285,11 +1262,13 @@ export class BridgeStatusController extends StaticIntervalPollingController | BridgeControllerAction | GetGasFeeState diff --git a/packages/bridge-status-controller/src/utils/gas.ts b/packages/bridge-status-controller/src/utils/gas.ts index 3f0e2aa2ab5..6eb852fdcf6 100644 --- a/packages/bridge-status-controller/src/utils/gas.ts +++ b/packages/bridge-status-controller/src/utils/gas.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ import { BRIDGE_PREFERRED_GAS_ESTIMATE } from '@metamask/bridge-controller'; import type { TokenAmountValues, TxData } from '@metamask/bridge-controller'; import { toHex } from '@metamask/controller-utils'; @@ -65,7 +66,6 @@ export const getTxGasEstimates = ({ export const calculateGasFees = async ( disable7702: boolean, messenger: BridgeStatusControllerMessenger, - estimateGasFeeFn: typeof TransactionController.prototype.estimateGasFee, { chainId: _, gasLimit, ...trade }: TxData, networkClientId: string, chainId: Hex, @@ -85,11 +85,10 @@ export const calculateGasFees = async ( value: trade.value as `0x${string}`, }; const { gasFeeEstimates } = messenger.call('GasFeeController:getState'); - const { estimates: txGasFeeEstimates } = await estimateGasFeeFn({ - transactionParams, - chainId, - networkClientId, - }); + const { estimates: txGasFeeEstimates } = await messenger.call( + 'TransactionController:estimateGasFee', + { transactionParams, chainId, networkClientId }, + ); const { maxFeePerGas, maxPriorityFeePerGas } = getTxGasEstimates({ networkGasFeeEstimates: gasFeeEstimates, txGasFeeEstimates, diff --git a/packages/bridge-status-controller/src/utils/transaction.ts b/packages/bridge-status-controller/src/utils/transaction.ts index 5341222a5f6..de0cb8e0904 100644 --- a/packages/bridge-status-controller/src/utils/transaction.ts +++ b/packages/bridge-status-controller/src/utils/transaction.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ import type { AccountsControllerState } from '@metamask/accounts-controller'; import { ChainId, @@ -343,14 +344,12 @@ export const getAddTransactionBatchParams = async ({ toTokenAmount, }, requireApproval = false, - estimateGasFeeFn, }: { messenger: BridgeStatusControllerMessenger; isBridgeTx: boolean; trade: TxData; quoteResponse: Omit & Partial; - estimateGasFeeFn: typeof TransactionController.prototype.estimateGasFee; approval?: TxData; resetApproval?: TxData; requireApproval?: boolean; @@ -379,7 +378,6 @@ export const getAddTransactionBatchParams = async ({ const gasFees = await calculateGasFees( disable7702, messenger, - estimateGasFeeFn, resetApproval, networkClientId, hexChainId, @@ -396,7 +394,6 @@ export const getAddTransactionBatchParams = async ({ const gasFees = await calculateGasFees( disable7702, messenger, - estimateGasFeeFn, approval, networkClientId, hexChainId, @@ -412,7 +409,6 @@ export const getAddTransactionBatchParams = async ({ const gasFees = await calculateGasFees( disable7702, messenger, - estimateGasFeeFn, trade, networkClientId, hexChainId, @@ -444,12 +440,10 @@ export const getAddTransactionBatchParams = async ({ export const findAndUpdateTransactionsInBatch = ({ messenger, - updateTransactionFn, batchId, txDataByType, }: { messenger: BridgeStatusControllerMessenger; - updateTransactionFn: typeof TransactionController.prototype.updateTransaction; batchId: string; txDataByType: { [key in TransactionType]?: string }; }) => { @@ -502,7 +496,11 @@ export const findAndUpdateTransactionsInBatch = ({ if (txMeta) { const updatedTx = { ...txMeta, type: txType as TransactionType }; - updateTransactionFn(updatedTx, `Update tx type to ${txType}`); + messenger.call( + 'TransactionController:updateTransaction', + updatedTx, + `Update tx type to ${txType}`, + ); txBatch[ [TransactionType.bridgeApproval, TransactionType.swapApproval].includes( txType as TransactionType, diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index cc6e47cb5e2..b7eae6b70f2 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -325,6 +325,11 @@ export type TransactionControllerEstimateGasBatchAction = { handler: TransactionController['estimateGasBatch']; }; +export type TransactionControllerEstimateGasFeeAction = { + type: `${typeof controllerName}:estimateGasFee`; + handler: TransactionController['estimateGasFee']; +}; + /** * Adds external provided transaction to state as confirmed transaction. * @@ -416,6 +421,7 @@ export type TransactionControllerActions = | TransactionControllerConfirmExternalTransactionAction | TransactionControllerEstimateGasAction | TransactionControllerEstimateGasBatchAction + | TransactionControllerEstimateGasFeeAction | TransactionControllerGetGasFeeTokensAction | TransactionControllerGetNonceLockAction | TransactionControllerGetStateAction @@ -4633,6 +4639,11 @@ export class TransactionController extends BaseController< this.estimateGasBatch.bind(this), ); + this.messenger.registerActionHandler( + `${controllerName}:estimateGasFee`, + this.estimateGasFee.bind(this), + ); + this.messenger.registerActionHandler( `${controllerName}:getGasFeeTokens`, this.#getGasFeeTokensAction.bind(this), diff --git a/packages/transaction-controller/src/index.ts b/packages/transaction-controller/src/index.ts index fe1dc7ad587..3a468a42168 100644 --- a/packages/transaction-controller/src/index.ts +++ b/packages/transaction-controller/src/index.ts @@ -10,6 +10,7 @@ export type { TransactionControllerEvents, TransactionControllerEstimateGasAction, TransactionControllerEstimateGasBatchAction, + TransactionControllerEstimateGasFeeAction, TransactionControllerGetGasFeeTokensAction, TransactionControllerGetNonceLockAction, TransactionControllerGetStateAction, From 5a962e2bb542b5150dc79be3e3b3bbbf1c041f43 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 11 Mar 2026 10:47:05 -0700 Subject: [PATCH 05/23] chore: pass update to intent --- .../src/bridge-status-controller.intent.ts | 9 +++++++-- .../src/bridge-status-controller.ts | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent.ts index 4eb0bc3ed0f..5a6e2991d8c 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.ts @@ -1,6 +1,8 @@ import { BridgeClientId, StatusTypes } from '@metamask/bridge-controller'; -import type { TransactionController } from '@metamask/transaction-controller'; -import { TransactionMeta } from '@metamask/transaction-controller'; +import type { + TransactionController, + TransactionMeta, +} from '@metamask/transaction-controller'; import type { BridgeStatusControllerMessenger, FetchFunction } from './types'; import type { BridgeHistoryItem } from './types'; @@ -22,6 +24,9 @@ type IntentStatuses = { export class IntentManager { readonly #messenger: BridgeStatusControllerMessenger; + /** + * @deprecated use the messenger to call 'TransactionController:updateTransaction' instead + */ readonly #updateTransactionFn: typeof TransactionController.prototype.updateTransaction; readonly intentApi: IntentApi; diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 295d49cab55..dfbbc4cd47a 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -170,6 +170,12 @@ export class BridgeStatusController extends StaticIntervalPollingController + ): ReturnType => + this.messenger.call('TransactionController:updateTransaction', ...args), getJwt: this.#getJwt, }); From b8c4c28846b94e52473d149010104b04464be7bc Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 11 Mar 2026 14:33:51 -0700 Subject: [PATCH 06/23] fix: lint error --- .../src/bridge-status-controller.test.ts | 1 - 1 file changed, 1 deletion(-) 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 38b8e64e288..323f4dc5d19 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -18,7 +18,6 @@ import { FeatureId, getQuotesReceivedProperties, } from '@metamask/bridge-controller'; -// eslint-disable-next-line import-x/no-extraneous-dependencies import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { MessengerActions, From 91f55aeac3972a19810893362ec752be3af55dfa Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 11 Mar 2026 16:16:45 -0700 Subject: [PATCH 07/23] fix: intent.manager unit tests --- .../bridge-status-controller/jest.config.js | 14 ++- ...e-status-controller.intent-manager.test.ts | 105 ++++++++++++------ 2 files changed, 87 insertions(+), 32 deletions(-) diff --git a/packages/bridge-status-controller/jest.config.js b/packages/bridge-status-controller/jest.config.js index a81dd6fed09..8c8f26dfad4 100644 --- a/packages/bridge-status-controller/jest.config.js +++ b/packages/bridge-status-controller/jest.config.js @@ -18,8 +18,20 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { + './src/bridge-status-controller.ts': { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + './src/bridge-status-controller.intent.ts': { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, global: { - branches: 93.31, + branches: 96.5, functions: 100, lines: 100, statements: 100, diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts index 12acada5e26..6761dcb8452 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { StatusTypes } from '@metamask/bridge-controller'; +import { BridgeClientId, StatusTypes } from '@metamask/bridge-controller'; import { TransactionStatus } from '@metamask/transaction-controller'; import { IntentManager } from './bridge-status-controller.intent'; @@ -81,10 +81,18 @@ describe('IntentManager', () => { .spyOn(console, 'error') .mockImplementation(() => undefined); + const { + quote: { srcChainId, intent }, + status: { + srcChain: { txHash }, + }, + } = makeHistoryItem({ originalTransactionId: 'tx-1' }); await manager.getIntentTransactionStatus( 'order-2', - makeHistoryItem({ originalTransactionId: 'tx-1' }), - 'client-id', + srcChainId, + intent?.protocol ?? '', + BridgeClientId.MOBILE, + txHash, ); manager.syncTransactionFromIntentStatus( 'order-2', @@ -129,8 +137,10 @@ describe('IntentManager', () => { }); await manager.getIntentTransactionStatus( 'order-3', - historyItem, - 'client-id', + historyItem.quote.srcChainId, + historyItem.quote.intent?.protocol ?? '', + BridgeClientId.MOBILE, + historyItem.status.srcChain.txHash, ); manager.syncTransactionFromIntentStatus('order-3', historyItem); @@ -154,10 +164,16 @@ describe('IntentManager', () => { .spyOn(manager.intentApi, 'getOrderStatus') .mockRejectedValue('non-Error rejection'); + const { + quote: { srcChainId, intent }, + status, + } = makeHistoryItem(); const result = await manager.getIntentTransactionStatus( 'order-1', - makeHistoryItem(), - 'client-id', + srcChainId, + intent?.protocol ?? '', + BridgeClientId.MOBILE, + status.srcChain.txHash, ); expect(result).toBeUndefined(); @@ -172,11 +188,19 @@ describe('IntentManager', () => { ); let thrown: unknown; + const { + quote: { srcChainId, intent }, + status: { + srcChain: { txHash }, + }, + } = makeHistoryItem(); try { await manager.getIntentTransactionStatus( 'order-1', - makeHistoryItem(), - 'client-id', + srcChainId, + intent?.protocol ?? '', + BridgeClientId.MOBILE, + txHash, ); } catch (error) { thrown = error; @@ -207,8 +231,10 @@ describe('IntentManager', () => { const result = await manager.getIntentTransactionStatus( 'order-1', - historyItem, - 'client-id', + historyItem.quote.srcChainId, + historyItem.quote.intent?.protocol ?? '', + BridgeClientId.MOBILE, + historyItem.status.srcChain.txHash, ); expect(result).toBeDefined(); @@ -231,10 +257,15 @@ describe('IntentManager', () => { status: { status: StatusTypes.PENDING } as BridgeHistoryItem['status'], }); + const { + quote: { srcChainId, intent }, + } = historyItemWithoutSrcChain; const result = await manager.getIntentTransactionStatus( 'order-1', - historyItemWithoutSrcChain, - 'client-id', + srcChainId, + intent?.protocol ?? '', + BridgeClientId.MOBILE, + undefined, ); expect(result).toBeDefined(); @@ -273,8 +304,10 @@ describe('IntentManager', () => { }); await manager.getIntentTransactionStatus( 'order-3', - historyItem, - 'client-id', + historyItem.quote.srcChainId, + historyItem.quote.intent?.protocol ?? '', + BridgeClientId.MOBILE, + historyItem.status.srcChain.txHash, ); manager.syncTransactionFromIntentStatus('order-3', historyItem); @@ -317,8 +350,10 @@ describe('IntentManager', () => { }); await manager.getIntentTransactionStatus( 'order-3', - historyItem, - 'client-id', + historyItem.quote.srcChainId, + historyItem.quote.intent?.protocol ?? '', + BridgeClientId.MOBILE, + historyItem.status.srcChain.txHash, ); manager.syncTransactionFromIntentStatus('order-3', historyItem); @@ -346,15 +381,17 @@ describe('IntentManager', () => { }), ); + const historyItem = makeHistoryItem({ + originalTransactionId: 'tx-missing', + }); await manager.getIntentTransactionStatus( 'order-1', - makeHistoryItem({ originalTransactionId: 'tx-missing' }), - 'client-id', - ); - manager.syncTransactionFromIntentStatus( - 'order-1', - makeHistoryItem({ originalTransactionId: 'tx-missing' }), + historyItem.quote.srcChainId, + historyItem.quote.intent?.protocol ?? '', + BridgeClientId.MOBILE, + historyItem.status.srcChain.txHash, ); + manager.syncTransactionFromIntentStatus('order-1', historyItem); expect(warnSpy).toHaveBeenCalledWith( '[Intent polling] Skipping update, transaction not found', @@ -395,8 +432,10 @@ describe('IntentManager', () => { }); await manager.getIntentTransactionStatus( 'order-3', - historyItem, - 'client-id', + historyItem.quote.srcChainId, + historyItem.quote.intent?.protocol ?? '', + BridgeClientId.MOBILE, + historyItem.status.srcChain.txHash, ); manager.syncTransactionFromIntentStatus('order-3', historyItem); @@ -442,8 +481,10 @@ describe('IntentManager', () => { }); await manager.getIntentTransactionStatus( 'order-3', - historyItem, - 'client-id', + historyItem.quote.srcChainId, + historyItem.quote.intent?.protocol ?? '', + BridgeClientId.MOBILE, + historyItem.status.srcChain.txHash, ); manager.syncTransactionFromIntentStatus('order-3', historyItem); @@ -487,8 +528,10 @@ describe('IntentManager', () => { }); await manager.getIntentTransactionStatus( 'order-3', - historyItem, - 'client-id', + historyItem.quote.srcChainId, + historyItem.quote.intent?.protocol ?? '', + BridgeClientId.MOBILE, + historyItem.status.srcChain.txHash, ); manager.syncTransactionFromIntentStatus('order-3', historyItem); @@ -513,7 +556,7 @@ describe('IntentManager', () => { const manager = new IntentManager(createManagerOptions({ fetchFn })); const params = { - srcChainId: '1', + srcChainId: 1, quoteId: 'quote-1', signature: '0xsig', order: { some: 'order' }, @@ -521,7 +564,7 @@ describe('IntentManager', () => { aggregatorId: 'cowswap', }; - const result = await manager.submitIntent(params, 'client-id'); + const result = await manager.submitIntent(params, BridgeClientId.EXTENSION); expect(result).toStrictEqual(expectedOrder); }); From cba88ca0963d5ab159b3cf6963fea59e1a0ac3ef Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 11 Mar 2026 14:50:20 -0700 Subject: [PATCH 08/23] chore: changelog --- packages/transaction-controller/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 3496ae5b8c1..1ee8751bed7 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add optional `sourceHash` field to `MetamaskPayMetadata` for tracking source chain transaction hashes when no local transaction exists ([#8133](https://github.com/MetaMask/core/pull/8133)) - Add `predictDepositAndOrder` to `TransactionType` ([#8135](https://github.com/MetaMask/core/pull/8135)) +- Export `TransactionControllerEstimateGasFeeAction` so the `estimateGasFee` handler can be called through the controller messenger ([#8188](https://github.com/MetaMask/core/pull/8188)) ### Changed From 5410cbb74c755718d5341e8f037906f2da9c30a3 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 11 Mar 2026 16:17:54 -0700 Subject: [PATCH 09/23] fix: intent tests --- ...e-status-controller.intent-manager.test.ts | 148 ++++++++--- .../bridge-status-controller.intent.test.ts | 107 ++++---- .../src/bridge-status-controller.intent.ts | 16 +- .../src/bridge-status-controller.test.ts | 22 +- .../src/bridge-status-controller.ts | 7 - .../src/utils/gas.test.ts | 5 +- .../src/utils/transaction.test.ts | 245 +++++++++++------- 7 files changed, 314 insertions(+), 236 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts index 6761dcb8452..c22b837c8df 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts @@ -36,7 +36,6 @@ const createManagerOptions = (overrides?: { fetchFn?: ReturnType; }): IntentManagerConstructorOptions => ({ messenger: overrides?.messenger ?? { call: jest.fn() }, - updateTransactionFn: overrides?.updateTransactionFn ?? jest.fn(), customBridgeApiBaseUrl: 'https://example.com', fetchFn: overrides?.fetchFn ?? jest.fn(), getJwt: jest.fn().mockResolvedValue(undefined), @@ -55,7 +54,6 @@ describe('IntentManager', () => { }), ); - expect(options.updateTransactionFn).not.toHaveBeenCalled(); expect(options.messenger.call).not.toHaveBeenCalled(); }); @@ -111,19 +109,24 @@ describe('IntentManager', () => { status: TransactionStatus.submitted, txReceipt: { status: '0x0' }, }; - const updateTransactionFn = jest.fn(); const completedOrder = { id: 'order-3', status: IntentOrderStatus.COMPLETED, txHash: '0xhash', metadata: {}, }; + const mockCall = jest.fn((...args: unknown[]) => { + const [method] = args; + if (method === 'TransactionController:updateTransaction') { + return { transactions: [existingTxMeta] }; + } + return { transactions: [existingTxMeta] }; + }); const manager = new IntentManager( createManagerOptions({ messenger: { - call: jest.fn(() => ({ transactions: [existingTxMeta] })), + call: (...args: unknown[]) => mockCall(...args), }, - updateTransactionFn, fetchFn: jest.fn().mockResolvedValue(completedOrder), }), ); @@ -144,17 +147,8 @@ describe('IntentManager', () => { ); manager.syncTransactionFromIntentStatus('order-3', historyItem); - expect(updateTransactionFn).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'tx-2', - status: TransactionStatus.confirmed, - hash: '0xhash', - txReceipt: expect.objectContaining({ - transactionHash: '0xhash', - status: '0x1', - }), - }), - expect.stringContaining('Intent order status updated'), + expect(mockCall.mock.calls[0][0]).toMatchInlineSnapshot( + `"TransactionController:getState"`, ); }); @@ -278,19 +272,24 @@ describe('IntentManager', () => { status: TransactionStatus.submitted, txReceipt: { status: '0x0' }, }; - const updateTransactionFn = jest.fn(); const completedOrder = { id: 'order-3', status: IntentOrderStatus.COMPLETED, txHash: '0xhash', metadata: {}, }; + const mockCall = jest.fn((...args: unknown[]) => { + const [method] = args; + if (method === 'TransactionController:updateTransaction') { + return undefined; + } + return { transactions: [existingTxMeta] }; + }); const manager = new IntentManager( createManagerOptions({ messenger: { - call: jest.fn(() => ({ transactions: [existingTxMeta] })), + call: (...args: unknown[]) => mockCall(...args), }, - updateTransactionFn, fetchFn: jest.fn().mockResolvedValue(completedOrder), }), ); @@ -311,11 +310,30 @@ describe('IntentManager', () => { ); manager.syncTransactionFromIntentStatus('order-3', historyItem); - expect(updateTransactionFn).toHaveBeenCalledTimes(1); + expect(mockCall).toHaveBeenCalledTimes(2); manager.syncTransactionFromIntentStatus('order-3', historyItem); - expect(updateTransactionFn).toHaveBeenCalledTimes(1); + expect(mockCall.mock.calls).toMatchInlineSnapshot(` + [ + [ + "TransactionController:getState", + ], + [ + "TransactionController:updateTransaction", + { + "hash": "0xhash", + "id": "tx-2", + "status": "confirmed", + "txReceipt": { + "status": "0x1", + "transactionHash": "0xhash", + }, + }, + "BridgeStatusController - Intent order status updated: completed", + ], + ] + `); }); it('syncTransactionFromIntentStatus cleans up intent statuses map when order has failed', async () => { @@ -324,19 +342,24 @@ describe('IntentManager', () => { status: TransactionStatus.submitted, txReceipt: { status: '0x0' }, }; - const updateTransactionFn = jest.fn(); const failedOrder = { id: 'order-3', status: IntentOrderStatus.FAILED, txHash: '0xhash', metadata: {}, }; + const mockCall = jest.fn((...args: unknown[]) => { + const [method] = args; + if (method === 'TransactionController:updateTransaction') { + return undefined; + } + return { transactions: [existingTxMeta] }; + }); const manager = new IntentManager( createManagerOptions({ messenger: { - call: jest.fn(() => ({ transactions: [existingTxMeta] })), + call: (...args: unknown[]) => mockCall(...args), }, - updateTransactionFn, fetchFn: jest.fn().mockResolvedValue(failedOrder), }), ); @@ -357,11 +380,30 @@ describe('IntentManager', () => { ); manager.syncTransactionFromIntentStatus('order-3', historyItem); - expect(updateTransactionFn).toHaveBeenCalledTimes(1); + expect(mockCall).toHaveBeenCalledTimes(2); manager.syncTransactionFromIntentStatus('order-3', historyItem); - expect(updateTransactionFn).toHaveBeenCalledTimes(1); + expect(mockCall.mock.calls).toMatchInlineSnapshot(` + [ + [ + "TransactionController:getState", + ], + [ + "TransactionController:updateTransaction", + { + "hash": "0xhash", + "id": "tx-2", + "status": "failed", + "txReceipt": { + "status": "0x0", + "transactionHash": "0xhash", + }, + }, + "BridgeStatusController - Intent order status updated: failed", + ], + ] + `); }); it('syncTransactionFromIntentStatus logs warn when transaction is not found', async () => { @@ -406,19 +448,24 @@ describe('IntentManager', () => { status: TransactionStatus.submitted, txReceipt: { status: '0x0' }, }; - const updateTransactionFn = jest.fn(); const submittedOrder = { id: 'order-3', status: IntentOrderStatus.SUBMITTED, txHash: '0xhash', metadata: {}, }; + const mockCall = jest.fn((...args: unknown[]) => { + const [method] = args; + if (method === 'TransactionController:updateTransaction') { + return { transactions: [existingTxMeta] }; + } + return { transactions: [existingTxMeta] }; + }); const manager = new IntentManager( createManagerOptions({ messenger: { - call: jest.fn(() => ({ transactions: [existingTxMeta] })), + call: (...args: unknown[]) => mockCall(...args), }, - updateTransactionFn, fetchFn: jest.fn().mockResolvedValue(submittedOrder), }), ); @@ -439,7 +486,8 @@ describe('IntentManager', () => { ); manager.syncTransactionFromIntentStatus('order-3', historyItem); - expect(updateTransactionFn).toHaveBeenCalledWith( + expect(mockCall).toHaveBeenCalledWith( + 'TransactionController:updateTransaction', expect.objectContaining({ id: 'tx-2', txReceipt: expect.objectContaining({ @@ -457,18 +505,23 @@ describe('IntentManager', () => { status: TransactionStatus.submitted, hash: undefined, }; - const updateTransactionFn = jest.fn(); const orderWithoutTxHash = { id: 'order-3', status: IntentOrderStatus.SUBMITTED, metadata: {}, }; + const mockCall = jest.fn((...args: unknown[]) => { + const [method] = args; + if (method === 'TransactionController:updateTransaction') { + return { transactions: [existingTxMeta] }; + } + return { transactions: [existingTxMeta] }; + }); const manager = new IntentManager( createManagerOptions({ messenger: { - call: jest.fn(() => ({ transactions: [existingTxMeta] })), + call: (...args: unknown[]) => mockCall(...args), }, - updateTransactionFn, fetchFn: jest.fn().mockResolvedValue(orderWithoutTxHash), }), ); @@ -488,8 +541,22 @@ describe('IntentManager', () => { ); manager.syncTransactionFromIntentStatus('order-3', historyItem); - const call = updateTransactionFn.mock.calls[0][0]; - expect(call.hash).toBeUndefined(); + expect(mockCall.mock.calls).toMatchInlineSnapshot(` + [ + [ + "TransactionController:getState", + ], + [ + "TransactionController:updateTransaction", + { + "hash": undefined, + "id": "tx-2", + "status": "submitted", + }, + "BridgeStatusController - Intent order status updated: submitted", + ], + ] + `); }); it('syncTransactionFromIntentStatus logs error when updateTransactionFn throws', async () => { @@ -498,18 +565,19 @@ describe('IntentManager', () => { status: TransactionStatus.submitted, txReceipt: {}, }; - const updateTransactionFn = jest.fn().mockImplementation(() => { - throw new Error('update failed'); - }); const errorSpy = jest .spyOn(console, 'error') .mockImplementation(() => undefined); const manager = new IntentManager( createManagerOptions({ messenger: { - call: jest.fn(() => ({ transactions: [existingTxMeta] })), + call: jest.fn((method) => { + if (method === 'TransactionController:updateTransaction') { + throw new Error('update failed'); + } + return { transactions: [existingTxMeta] }; + }), }, - updateTransactionFn, fetchFn: jest.fn().mockResolvedValue({ id: 'order-3', status: IntentOrderStatus.COMPLETED, diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts index ab479eb9055..b27d2baa3d0 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts @@ -5,7 +5,6 @@ import { BridgeClientId, StatusTypes } from '@metamask/bridge-controller'; import type { GasFeeEstimates, TransactionMeta, - TransactionParams, } from '@metamask/transaction-controller'; import { TransactionStatus, @@ -134,6 +133,7 @@ const createMessengerHarness = ( accountAddress: string, selectedChainId: string = '0x1', keyringType: string = 'HD Key Tree', + approvalStatus?: TransactionStatus, ): any => { const transactions: TransactionMeta[] = []; @@ -158,6 +158,52 @@ const createMessengerHarness = ( } case 'TransactionController:getState': return { transactions }; + case 'TransactionController:estimateGasFee': + return { estimates: {} as GasFeeEstimates }; + case 'TransactionController:addTransaction': { + // Approval TX path (submitIntent -> #handleApprovalTx -> #handleEvmTransaction) + if ( + args[1]?.type === TransactionType.bridgeApproval || + args[1]?.type === TransactionType.swapApproval + ) { + const hash = '0xapprovalhash1'; + + const approvalTx = { + id: 'approvalTxId1', + type: args[1]?.type, + status: approvalStatus ?? TransactionStatus.failed, + chainId: args[0]?.chainId ?? '0x1', + hash, + networkClientId: 'network-client-id-1', + time: Date.now(), + txParams: args[0], + }; + transactions.push(approvalTx); + + return { + result: Promise.resolve(hash), + transactionMeta: approvalTx, + }; + } + + // Intent “display tx” path + const intentTx = { + id: 'intentDisplayTxId1', + type: args[1]?.type, + status: TransactionStatus.submitted, + chainId: args[0]?.chainId ?? '0x1', + hash: undefined, + networkClientId: 'network-client-id-1', + time: Date.now(), + txParams: args[0], + }; + transactions.push(intentTx); + + return { + result: Promise.resolve('0xunused'), + transactionMeta: intentTx, + }; + } case 'NetworkController:findNetworkClientIdByChainId': return 'network-client-id-1'; case 'NetworkController:getState': @@ -191,53 +237,7 @@ const setup = (options?: { accountAddress, options?.selectedChainId ?? '0x1', options?.keyringType, - ); - - const addTransactionFn = jest.fn( - async (txParams: TransactionParams, reqOpts: any) => { - // Approval TX path (submitIntent -> #handleApprovalTx -> #handleEvmTransaction) - if ( - reqOpts?.type === TransactionType.bridgeApproval || - reqOpts?.type === TransactionType.swapApproval - ) { - const hash = '0xapprovalhash1'; - - const approvalTx = { - id: 'approvalTxId1', - type: reqOpts.type, - status: options?.approvalStatus ?? TransactionStatus.failed, - chainId: txParams.chainId ?? '0x1', - hash, - networkClientId: 'network-client-id-1', - time: Date.now(), - txParams, - }; - transactions.push(approvalTx); - - return { - result: Promise.resolve(hash), - transactionMeta: approvalTx, - }; - } - - // Intent “display tx” path - const intentTx = { - id: 'intentDisplayTxId1', - type: reqOpts?.type, - status: TransactionStatus.submitted, - chainId: txParams.chainId ?? '0x1', - hash: undefined, - networkClientId: 'network-client-id-1', - time: Date.now(), - txParams, - }; - transactions.push(intentTx); - - return { - result: Promise.resolve('0xunused'), - transactionMeta: intentTx, - }; - }, + options?.approvalStatus, ); const mockFetchFn = jest.fn(); @@ -248,12 +248,6 @@ const setup = (options?: { }, clientId: options?.clientId ?? BridgeClientId.EXTENSION, fetchFn: (...args: any[]) => mockFetchFn(...args), - addTransactionFn, - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(async () => ({ - estimates: {} as GasFeeEstimates, - })), config: { customBridgeApiBaseUrl: 'http://localhost' }, traceFn: (_req: any, fn?: any): any => fn?.(), }); @@ -269,7 +263,6 @@ const setup = (options?: { controller, messenger, transactions, - addTransactionFn, startPollingSpy, stopPollingSpy, accountAddress, @@ -1059,10 +1052,6 @@ describe('BridgeStatusController (target uncovered branches)', () => { state, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), - addTransactionBatchFn: jest.fn(), - updateTransactionFn: jest.fn(), - estimateGasFeeFn: jest.fn(), config: { customBridgeApiBaseUrl: 'http://localhost' }, traceFn: (_r: any, fn?: any): any => fn?.(), }); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent.ts index 5a6e2991d8c..3f82dca357d 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.ts @@ -1,8 +1,5 @@ import { BridgeClientId, StatusTypes } from '@metamask/bridge-controller'; -import type { - TransactionController, - TransactionMeta, -} from '@metamask/transaction-controller'; +import type { TransactionMeta } from '@metamask/transaction-controller'; import type { BridgeStatusControllerMessenger, FetchFunction } from './types'; import type { BridgeHistoryItem } from './types'; @@ -24,11 +21,6 @@ type IntentStatuses = { export class IntentManager { readonly #messenger: BridgeStatusControllerMessenger; - /** - * @deprecated use the messenger to call 'TransactionController:updateTransaction' instead - */ - readonly #updateTransactionFn: typeof TransactionController.prototype.updateTransaction; - readonly intentApi: IntentApi; readonly #intentStatusesByBridgeTxMetaId: Map = @@ -36,19 +28,16 @@ export class IntentManager { constructor({ messenger, - updateTransactionFn, customBridgeApiBaseUrl, fetchFn, getJwt, }: { messenger: BridgeStatusControllerMessenger; - updateTransactionFn: typeof TransactionController.prototype.updateTransaction; customBridgeApiBaseUrl: string; fetchFn: FetchFunction; getJwt: GetJwtFn; }) { this.#messenger = messenger; - this.#updateTransactionFn = updateTransactionFn; this.intentApi = new IntentApiImpl(customBridgeApiBaseUrl, fetchFn, getJwt); } @@ -187,7 +176,8 @@ export class IntentManager { ...txReceiptUpdate, } as TransactionMeta; - this.#updateTransactionFn( + this.#messenger.call( + 'TransactionController:updateTransaction', updatedTxMeta, `BridgeStatusController - Intent order status updated: ${orderStatus}`, ); 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 1fde93c0aec..17192b32ab2 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -3574,7 +3574,7 @@ describe('BridgeStatusController', () => { }, }, trade: { - ...quoteWithoutApproval.trade, + ...(quoteWithoutApproval.trade as TxData), gasLimit: undefined, }, sentAmount: { amount: null, valueInCurrency: null, usd: null }, @@ -4194,7 +4194,6 @@ describe('BridgeStatusController', () => { MessengerEvents, RootMessenger >; - let mockTrackEventFn: jest.Mock; let bridgeStatusController: BridgeStatusController; let mockFetchFn: jest.Mock; @@ -4231,25 +4230,6 @@ describe('BridgeStatusController', () => { return Promise.resolve(); }); - const mockBridgeMessenger = new Messenger< - 'BridgeController', - MessengerActions, - MessengerEvents, - RootMessenger - >({ - namespace: 'BridgeController', - parent: mockMessenger, - }); - mockTrackEventFn = jest.fn(); - // const bridgeController = new BridgeController({ - // messenger: mockBridgeMessenger, - // clientId: BridgeClientId.EXTENSION, - // fetchFn: jest.fn(), - // trackMetaMetricsFn: mockTrackEventFn, - // getLayer1GasFee: jest.fn(), - // clientVersion: '13.4.0', - // }); - mockFetchFn = jest .fn() .mockResolvedValueOnce(MockStatusResponse.getPending()); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index dfbbc4cd47a..c783c39af3b 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -170,12 +170,6 @@ export class BridgeStatusController extends StaticIntervalPollingController - ): ReturnType => - this.messenger.call('TransactionController:updateTransaction', ...args), getJwt: this.#getJwt, }); @@ -1638,7 +1632,6 @@ export class BridgeStatusController extends StaticIntervalPollingController { const result = await calculateGasFees( false, null as never, - jest.fn(), mockTrade, 'mainnet', '0x1', @@ -136,7 +135,6 @@ describe('gas calculation utils', () => { const result = await calculateGasFees( true, null as never, - jest.fn(), mockTrade, 'mainnet', '0x1', @@ -169,7 +167,7 @@ describe('gas calculation utils', () => { estimatedBaseFee: '0x1234', }, }); - const mockEstimateGasFeeFn = jest.fn().mockResolvedValueOnce({ + mockCall.mockResolvedValueOnce({ estimates: { [GasFeeEstimateLevel.Medium]: { maxFeePerGas: '0x1234567890', @@ -180,7 +178,6 @@ describe('gas calculation utils', () => { const result = await calculateGasFees( true, { call: mockCall } as never, - mockEstimateGasFeeFn, { ...mockTrade, gasLimit }, 'mainnet', '0x1', diff --git a/packages/bridge-status-controller/src/utils/transaction.test.ts b/packages/bridge-status-controller/src/utils/transaction.test.ts index edea3e636db..e34e8732f1e 100644 --- a/packages/bridge-status-controller/src/utils/transaction.test.ts +++ b/packages/bridge-status-controller/src/utils/transaction.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ import { ChainId, FeeType, @@ -1851,6 +1852,11 @@ describe('Bridge Status Controller Transaction Utils', () => { }, }; } + if (method === 'TransactionController:estimateGasFee') { + return { + estimates: {}, + }; + } return undefined; }), }); @@ -1872,7 +1878,6 @@ describe('Bridge Status Controller Transaction Utils', () => { isBridgeTx: true, trade: mockQuoteResponse.trade, approval: mockQuoteResponse.approval, - estimateGasFeeFn: jest.fn().mockResolvedValue({}), }); expect(result.disable7702).toBe(false); @@ -1894,7 +1899,6 @@ describe('Bridge Status Controller Transaction Utils', () => { messenger: mockMessagingSystem, isBridgeTx: false, trade: mockQuoteResponse.trade, - estimateGasFeeFn: jest.fn().mockResolvedValue({}), }); expect(result.disable7702).toBe(true); @@ -1916,7 +1920,6 @@ describe('Bridge Status Controller Transaction Utils', () => { isBridgeTx: false, trade: mockQuoteResponse.trade, approval: mockQuoteResponse.approval, - estimateGasFeeFn: jest.fn().mockResolvedValue({}), }); expect(result.transactions).toHaveLength(2); @@ -1937,7 +1940,6 @@ describe('Bridge Status Controller Transaction Utils', () => { isBridgeTx: true, trade: mockQuoteResponse.trade, resetApproval: mockQuoteResponse.resetApproval, - estimateGasFeeFn: jest.fn().mockResolvedValue({}), }); expect(result.disable7702).toBe(true); @@ -1959,7 +1961,6 @@ describe('Bridge Status Controller Transaction Utils', () => { messenger: mockMessagingSystem, isBridgeTx: false, trade: mockQuoteResponse.trade, - estimateGasFeeFn: jest.fn().mockResolvedValue({}), }); expect(result.isGasFeeIncluded).toBe(false); @@ -1976,7 +1977,6 @@ describe('Bridge Status Controller Transaction Utils', () => { messenger: mockMessagingSystem, isBridgeTx: false, trade: mockQuoteResponse.trade, - estimateGasFeeFn: jest.fn().mockResolvedValue({}), }); expect(result.isGasFeeIncluded).toBe(true); @@ -1993,7 +1993,6 @@ describe('Bridge Status Controller Transaction Utils', () => { messenger: mockMessagingSystem, isBridgeTx: false, trade: mockQuoteResponse.trade, - estimateGasFeeFn: jest.fn().mockResolvedValue({}), }); expect(result.isGasFeeIncluded).toBe(false); @@ -2002,9 +2001,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }); describe('findAndUpdateTransactionsInBatch', () => { - const mockUpdateTransactionFn = jest.fn(); const batchId = 'test-batch-id'; - let mockMessagingSystem: BridgeStatusControllerMessenger; const createMockTransaction = (overrides: { id: string; @@ -2029,11 +2026,23 @@ describe('Bridge Status Controller Transaction Utils', () => { }); // Helper function to create mock messaging system with transactions - const createMockMessagingSystemWithTxs = ( + const mockMessengerCallHandlers = ( txs: ReturnType[], - ) => ({ - call: jest.fn().mockReturnValue({ transactions: txs }), - }); + ) => { + return { + call: jest.fn((method: string, ..._args: unknown[]) => { + if (method === 'TransactionController:getState') { + return { transactions: txs }; + } + if (method === 'TransactionController:updateTransaction') { + return { + transactionMeta: { id: 'tx1', type: TransactionType.swap }, + }; + } + return undefined; + }), + }; + }; beforeEach(() => { jest.clearAllMocks(); @@ -2052,10 +2061,7 @@ describe('Bridge Status Controller Transaction Utils', () => { data: '0xapprovalData', }), ]; - - mockMessagingSystem = createMockMessagingSystemWithTxs( - txs, - ) as unknown as BridgeStatusControllerMessenger; + const mockMessenger = mockMessengerCallHandlers(txs); const txDataByType = { [TransactionType.swap]: '0xswapData', @@ -2063,29 +2069,46 @@ describe('Bridge Status Controller Transaction Utils', () => { }; findAndUpdateTransactionsInBatch({ - messenger: mockMessagingSystem, + messenger: mockMessenger as unknown as BridgeStatusControllerMessenger, batchId, txDataByType, - updateTransactionFn: mockUpdateTransactionFn, }); - // Should update the 7702 batch transaction to swap type - expect(mockUpdateTransactionFn).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'tx1', - type: TransactionType.swap, - }), - 'Update tx type to swap', - ); - - // Should update the approval transaction - expect(mockUpdateTransactionFn).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'tx2', - type: TransactionType.swapApproval, - }), - 'Update tx type to swapApproval', - ); + expect( + mockMessenger.call.mock.calls.filter( + ([action]) => action === 'TransactionController:updateTransaction', + ), + ).toMatchInlineSnapshot(` + [ + [ + "TransactionController:updateTransaction", + { + "batchId": "test-batch-id", + "id": "tx1", + "txParams": { + "authorizationList": [ + "0xAuth1", + ], + "data": "0xbatchExecuteData", + }, + "type": "swap", + }, + "Update tx type to swap", + ], + [ + "TransactionController:updateTransaction", + { + "batchId": "test-batch-id", + "id": "tx2", + "txParams": { + "data": "0xapprovalData", + }, + "type": "swapApproval", + }, + "Update tx type to swapApproval", + ], + ] + `); }); it('should handle 7702 transactions with delegationAddress', () => { @@ -2098,29 +2121,38 @@ describe('Bridge Status Controller Transaction Utils', () => { }), ]; - mockMessagingSystem = createMockMessagingSystemWithTxs( - txs, - ) as unknown as BridgeStatusControllerMessenger; + const mockMessenger = mockMessengerCallHandlers(txs); const txDataByType = { [TransactionType.swap]: '0xswapData', }; findAndUpdateTransactionsInBatch({ - messenger: mockMessagingSystem, + messenger: mockMessenger as unknown as BridgeStatusControllerMessenger, batchId, txDataByType, - updateTransactionFn: mockUpdateTransactionFn, }); // Should identify and update 7702 transaction with delegationAddress - expect(mockUpdateTransactionFn).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'tx1', - type: TransactionType.swap, - }), - 'Update tx type to swap', - ); + expect( + mockMessenger.call.mock.calls.find( + (call) => call[0] === 'TransactionController:updateTransaction', + ), + ).toMatchInlineSnapshot(` + [ + "TransactionController:updateTransaction", + { + "batchId": "test-batch-id", + "delegationAddress": "0xDelegationAddress", + "id": "tx1", + "txParams": { + "data": "0xbatchData", + }, + "type": "swap", + }, + "Update tx type to swap", + ] + `); }); it('should handle 7702 approval transactions', () => { @@ -2132,29 +2164,42 @@ describe('Bridge Status Controller Transaction Utils', () => { }), ]; - mockMessagingSystem = createMockMessagingSystemWithTxs( - txs, - ) as unknown as BridgeStatusControllerMessenger; + const mockMessenger = mockMessengerCallHandlers(txs); const txDataByType = { [TransactionType.swapApproval]: '0xapprovalData', }; findAndUpdateTransactionsInBatch({ - messenger: mockMessagingSystem, + messenger: mockMessenger as unknown as BridgeStatusControllerMessenger, batchId, txDataByType, - updateTransactionFn: mockUpdateTransactionFn, }); // Should match 7702 approval transaction by data - expect(mockUpdateTransactionFn).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'tx1', - type: TransactionType.swapApproval, - }), - 'Update tx type to swapApproval', - ); + expect( + mockMessenger.call.mock.calls.filter( + (call) => call[0] === 'TransactionController:updateTransaction', + ), + ).toMatchInlineSnapshot(` + [ + [ + "TransactionController:updateTransaction", + { + "batchId": "test-batch-id", + "id": "tx1", + "txParams": { + "authorizationList": [ + "0xAuth1", + ], + "data": "0xapprovalData", + }, + "type": "swapApproval", + }, + "Update tx type to swapApproval", + ], + ] + `); }); it('should handle non-7702 transactions normally', () => { @@ -2169,9 +2214,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }), ]; - mockMessagingSystem = createMockMessagingSystemWithTxs( - txs, - ) as unknown as BridgeStatusControllerMessenger; + const mockMessenger = mockMessengerCallHandlers(txs); const txDataByType = { [TransactionType.bridge]: '0xswapData', @@ -2179,28 +2222,44 @@ describe('Bridge Status Controller Transaction Utils', () => { }; findAndUpdateTransactionsInBatch({ - messenger: mockMessagingSystem, + messenger: mockMessenger as unknown as BridgeStatusControllerMessenger, batchId, txDataByType, - updateTransactionFn: mockUpdateTransactionFn, }); // Should update regular transactions by matching data - expect(mockUpdateTransactionFn).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'tx1', - type: TransactionType.bridge, - }), - 'Update tx type to bridge', - ); - - expect(mockUpdateTransactionFn).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'tx2', - type: TransactionType.bridgeApproval, - }), - 'Update tx type to bridgeApproval', - ); + expect( + mockMessenger.call.mock.calls.filter( + (call) => call[0] === 'TransactionController:updateTransaction', + ), + ).toMatchInlineSnapshot(` + [ + [ + "TransactionController:updateTransaction", + { + "batchId": "test-batch-id", + "id": "tx1", + "txParams": { + "data": "0xswapData", + }, + "type": "bridge", + }, + "Update tx type to bridge", + ], + [ + "TransactionController:updateTransaction", + { + "batchId": "test-batch-id", + "id": "tx2", + "txParams": { + "data": "0xapprovalData", + }, + "type": "bridgeApproval", + }, + "Update tx type to bridgeApproval", + ], + ] + `); }); it('should not update transactions without matching batchId', () => { @@ -2212,23 +2271,24 @@ describe('Bridge Status Controller Transaction Utils', () => { }), ]; - mockMessagingSystem = createMockMessagingSystemWithTxs( - txs, - ) as unknown as BridgeStatusControllerMessenger; + const mockMessenger = mockMessengerCallHandlers(txs); const txDataByType = { [TransactionType.swap]: '0xswapData', }; findAndUpdateTransactionsInBatch({ - messenger: mockMessagingSystem, + messenger: mockMessenger as unknown as BridgeStatusControllerMessenger, batchId, txDataByType, - updateTransactionFn: mockUpdateTransactionFn, }); // Should not update transactions with different batchId - expect(mockUpdateTransactionFn).not.toHaveBeenCalled(); + expect( + mockMessenger.call.mock.calls.filter( + (call) => call[0] === 'TransactionController:updateTransaction', + ), + ).toHaveLength(0); }); it('should handle 7702 bridge transactions', () => { @@ -2241,9 +2301,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }), ]; - mockMessagingSystem = createMockMessagingSystemWithTxs( - txs, - ) as unknown as BridgeStatusControllerMessenger; + const mockMessenger = mockMessengerCallHandlers(txs); const txDataByType = { [TransactionType.bridge]: '0xbridgeData', @@ -2251,14 +2309,17 @@ describe('Bridge Status Controller Transaction Utils', () => { // Test with bridge transaction (not swap) findAndUpdateTransactionsInBatch({ - messenger: mockMessagingSystem, + messenger: mockMessenger as unknown as BridgeStatusControllerMessenger, batchId, txDataByType, - updateTransactionFn: mockUpdateTransactionFn, }); // Should not match since it's looking for bridge but finds batch type - expect(mockUpdateTransactionFn).not.toHaveBeenCalled(); + expect( + mockMessenger.call.mock.calls.filter( + (call) => call[0] === 'TransactionController:updateTransaction', + ), + ).toHaveLength(0); }); }); From 3852f8115ba7f85c8a3f54a5e50b39ebe9c95a18 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Mar 2026 08:45:28 -0700 Subject: [PATCH 10/23] chore: undo transaction changes --- packages/transaction-controller/CHANGELOG.md | 1 - packages/transaction-controller/src/index.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index df5cc75ad95..89bbf8dab99 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -42,7 +42,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add optional `sourceHash` field to `MetamaskPayMetadata` for tracking source chain transaction hashes when no local transaction exists ([#8133](https://github.com/MetaMask/core/pull/8133)) - Add `predictDepositAndOrder` to `TransactionType` ([#8135](https://github.com/MetaMask/core/pull/8135)) -- Export `TransactionControllerEstimateGasFeeAction` so the `estimateGasFee` handler can be called through the controller messenger ([#8188](https://github.com/MetaMask/core/pull/8188)) ### Changed diff --git a/packages/transaction-controller/src/index.ts b/packages/transaction-controller/src/index.ts index 97109b77c9d..2c453180533 100644 --- a/packages/transaction-controller/src/index.ts +++ b/packages/transaction-controller/src/index.ts @@ -3,7 +3,6 @@ export type { Result, TransactionControllerActions, TransactionControllerEvents, - TransactionControllerEstimateGasFeeAction, TransactionControllerGetStateAction, TransactionControllerIncomingTransactionsReceivedEvent, TransactionControllerPostTransactionBalanceUpdatedEvent, From 3ea45bf9c3c7c2e8224f4afe9ae648a7e2682dd0 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Mar 2026 08:51:55 -0700 Subject: [PATCH 11/23] fix: lint errors --- .../bridge-controller/src/utils/validators.ts | 18 +++++++++--------- .../src/bridge-status-controller.ts | 2 +- .../bridge-status-controller/src/utils/gas.ts | 6 +++--- .../src/utils/transaction.ts | 10 +++++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index f2e85cb1bb8..c0f0834b9f7 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -40,23 +40,23 @@ export enum ActionTypes { REFUEL = 'refuel', } -const HexAddressSchema = define('HexAddress', (v: unknown) => - isValidHexAddress(v as string, { allowNonPrefixed: false }), +const HexAddressSchema = define<`0x${string}`>('HexAddress', (data: unknown) => + isValidHexAddress(data as string, { allowNonPrefixed: false }), ); -const HexStringSchema = define('HexString', (v: unknown) => - isStrictHexString(v as string), +const HexStringSchema = define<`0x${string}`>('HexString', (data: unknown) => + isStrictHexString(data as string), ); const VersionStringSchema = define( 'VersionString', - (v: unknown) => - typeof v === 'string' && - /^(\d+\.*){2}\d+$/u.test(v) && - v.split('.').length === 3, + (data: unknown) => + typeof data === 'string' && + /^(\d+\.*){2}\d+$/u.test(data) && + data.split('.').length === 3, ); -export const truthyString = (s: string) => Boolean(s?.length); +export const truthyString = (data: string): boolean => Boolean(data?.length); const TruthyDigitStringSchema = pattern(string(), /^\d+$/u); const ChainIdSchema = number(); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index a116da8b8f7..510fd2ada24 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1469,7 +1469,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { const params = { ...trade, - data: trade.data as `0x${string}`, - to: trade.to as `0x${string}`, - value: trade.value as `0x${string}`, + data: trade.data, + to: trade.to, + value: trade.value, }; if (skipGasFields) { return params; @@ -436,7 +436,7 @@ export const getAddTransactionBatchParams = async ({ networkClientId, requireApproval, origin: 'metamask', - from: trade.from as `0x${string}`, + from: trade.from, transactions, }; @@ -470,7 +470,7 @@ export const findAndUpdateTransactionsInBatch = ({ } // Find transaction by batchId and either matching data or delegation characteristics - const txMeta = txs.find((tx) => { + const txMeta = txs.find((tx: TransactionMeta) => { if (tx.batchId !== batchId) { return false; } From 8640c98a928912e969df444c0aec4b223321ea9c Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Mar 2026 09:46:43 -0700 Subject: [PATCH 12/23] fix: unit tests --- .../bridge-controller/src/utils/bridge.ts | 2 +- .../src/bridge-status-controller.test.ts | 10 ++-- .../bridge-status-controller/src/types.ts | 2 +- .../src/utils/transaction.test.ts | 57 ++++++++++++------- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index c9b5be725f8..a77653fb513 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -126,7 +126,7 @@ export const getEthUsdtResetData = ( '0', ]); - return data; + return data as Hex; }; export const isEthUsdt = ( 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 95ac2ebed68..119a9ea7607 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -2788,7 +2788,7 @@ describe('BridgeStatusController', () => { ([action]) => action === 'TransactionController:updateTransaction', ), ).toHaveLength(1); - expect(mockMessengerCall).toHaveBeenCalledTimes(14); + expect(mockMessengerCall).toHaveBeenCalledTimes(15); }); it('should throw an error if approval tx fails', async () => { @@ -3358,7 +3358,7 @@ describe('BridgeStatusController', () => { ([action]) => action === 'TransactionController:addTransaction', ), ).toHaveLength(2); - expect(mockMessengerCall).toHaveBeenCalledTimes(15); + expect(mockMessengerCall).toHaveBeenCalledTimes(16); }); it('should successfully submit an EVM swap transaction with featureId', async () => { @@ -3439,7 +3439,7 @@ describe('BridgeStatusController', () => { ([action]) => action === 'TransactionController:addTransaction', ), ).toHaveLength(0); - expect(mockMessengerCall).toHaveBeenCalledTimes(8); + expect(mockMessengerCall).toHaveBeenCalledTimes(9); expect(controller.state.txHistory[result.id]).toMatchSnapshot(); }); @@ -3821,7 +3821,7 @@ describe('BridgeStatusController', () => { ([action]) => action === 'TransactionController:addTransactionBatch', ), ).toHaveLength(0); - expect(mockMessengerCall).toHaveBeenCalledTimes(4); + expect(mockMessengerCall).toHaveBeenCalledTimes(5); }); it('should throw error if batched tx is not found', async () => { @@ -3873,7 +3873,7 @@ describe('BridgeStatusController', () => { ([action]) => action === 'TransactionController:addTransactionBatch', ), ).toHaveLength(1); - expect(mockMessengerCall).toHaveBeenCalledTimes(11); + expect(mockMessengerCall).toHaveBeenCalledTimes(12); }); }); diff --git a/packages/bridge-status-controller/src/types.ts b/packages/bridge-status-controller/src/types.ts index 14d22509fc6..4629a047a99 100644 --- a/packages/bridge-status-controller/src/types.ts +++ b/packages/bridge-status-controller/src/types.ts @@ -28,6 +28,7 @@ import type { Infer } from '@metamask/superstruct'; import type { TransactionControllerAddTransactionAction, TransactionControllerAddTransactionBatchAction, + TransactionControllerEstimateGasFeeAction, TransactionControllerGetStateAction, TransactionControllerIsAtomicBatchSupportedAction, TransactionControllerTransactionConfirmedEvent, @@ -40,7 +41,6 @@ import type { CaipAssetType } from '@metamask/utils'; import type { BridgeStatusController } from './bridge-status-controller'; import { BRIDGE_STATUS_CONTROLLER_NAME } from './constants'; import type { StatusResponseSchema } from './utils/validators'; -import { TransactionControllerEstimateGasFeeAction } from '../../transaction-controller/src/TransactionController'; // All fields need to be types not interfaces, same with their children fields // o/w you get a type error diff --git a/packages/bridge-status-controller/src/utils/transaction.test.ts b/packages/bridge-status-controller/src/utils/transaction.test.ts index ef6a402e506..1d41d4f5aba 100644 --- a/packages/bridge-status-controller/src/utils/transaction.test.ts +++ b/packages/bridge-status-controller/src/utils/transaction.test.ts @@ -1731,7 +1731,7 @@ describe('Bridge Status Controller Transaction Utils', () => { from: '0x1', value: '0x1', }; - const result = toBatchTxParams(true, mockTrade, {}); + const result = toBatchTxParams(true, mockTrade as TxData, {}); expect(result).toStrictEqual({ data: '0x1', from: '0x1', @@ -1938,7 +1938,6 @@ describe('Bridge Status Controller Transaction Utils', () => { isBridgeTx: false, trade: mockQuoteResponse.trade, resetApproval: mockQuoteResponse.resetApproval, - estimateGasFeeFn: jest.fn().mockResolvedValue({}), }); expect(result.transactions).toHaveLength(2); @@ -2038,15 +2037,19 @@ describe('Bridge Status Controller Transaction Utils', () => { isBridgeTx: true, trade: mockQuoteResponse.trade, isDelegatedAccount: true, - estimateGasFeeFn: mockEstimateGasFeeFn, }); // 7702 should be enabled for delegated accounts expect(result.disable7702).toBe(false); // Gas is NOT sponsored expect(result.isGasFeeIncluded).toBe(false); - // Gas estimation should have been called (not skipped) - expect(mockEstimateGasFeeFn).toHaveBeenCalled(); + expect(mockMessagingSystem.call).toHaveBeenCalledWith( + 'TransactionController:estimateGasFee', + { + chainId: mockQuoteResponse.trade.chainId, + txParams: mockQuoteResponse.trade, + }, + ); // Transaction params should include gas fields expect(result.transactions).toHaveLength(1); expect(result.transactions[0].params).toHaveProperty('gas'); @@ -2114,7 +2117,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }); // Helper function to create mock messaging system with transactions - const mockMessengerCallHandlers = ( + const createMockMessagingSystemWithTxs = ( txs: ReturnType[], ) => { return { @@ -2149,7 +2152,7 @@ describe('Bridge Status Controller Transaction Utils', () => { data: '0xapprovalData', }), ]; - const mockMessenger = mockMessengerCallHandlers(txs); + const mockMessenger = createMockMessagingSystemWithTxs(txs); const txDataByType = { [TransactionType.swap]: '0xswapData', @@ -2209,7 +2212,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }), ]; - const mockMessenger = mockMessengerCallHandlers(txs); + const mockMessenger = createMockMessagingSystemWithTxs(txs); const txDataByType = { [TransactionType.swap]: '0xswapData', @@ -2252,7 +2255,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }), ]; - const mockMessenger = mockMessengerCallHandlers(txs); + const mockMessenger = createMockMessagingSystemWithTxs(txs); const txDataByType = { [TransactionType.swapApproval]: '0xapprovalData', @@ -2302,7 +2305,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }), ]; - const mockMessenger = mockMessengerCallHandlers(txs); + const mockMessenger = createMockMessagingSystemWithTxs(txs); const txDataByType = { [TransactionType.bridge]: '0xswapData', @@ -2359,7 +2362,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }), ]; - const mockMessenger = mockMessengerCallHandlers(txs); + const mockMessenger = createMockMessagingSystemWithTxs(txs); const txDataByType = { [TransactionType.swap]: '0xswapData', @@ -2389,7 +2392,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }), ]; - const mockMessenger = mockMessengerCallHandlers(txs); + const mockMessenger = createMockMessagingSystemWithTxs(txs); const txDataByType = { [TransactionType.bridge]: '0xbridgeData', @@ -2397,7 +2400,7 @@ describe('Bridge Status Controller Transaction Utils', () => { // Test with bridge transaction — should match batch type for 7702 const result = findAndUpdateTransactionsInBatch({ - messenger: mockMessagingSystem, + messenger: mockMessenger, batchId, txDataByType, }); @@ -2409,8 +2412,17 @@ describe('Bridge Status Controller Transaction Utils', () => { ), ).toHaveLength(0); // Should match since 7702 bridge transactions use batch type - expect(mockUpdateTransactionFn).toHaveBeenCalledWith( - expect.objectContaining({ id: 'tx1', type: TransactionType.bridge }), + expect(mockMessenger.call).toHaveBeenCalledWith( + 'TransactionController:updateTransaction', + { + batchId, + id: 'tx1', + txParams: { + authorizationList: ['0xAuth1'], + data: '0xbatchData', + }, + type: TransactionType.bridge, + }, 'Update tx type to bridge', ); expect(result.tradeMeta).toStrictEqual( @@ -2428,7 +2440,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }), ]; - mockMessagingSystem = createMockMessagingSystemWithTxs( + const mockMessagingSystem = createMockMessagingSystemWithTxs( txs, ) as unknown as BridgeStatusControllerMessenger; @@ -2440,14 +2452,19 @@ describe('Bridge Status Controller Transaction Utils', () => { messenger: mockMessagingSystem, batchId, txDataByType, - updateTransactionFn: mockUpdateTransactionFn, }); - expect(mockUpdateTransactionFn).toHaveBeenCalledWith( - expect.objectContaining({ + expect(mockMessagingSystem.call).toHaveBeenCalledWith( + 'TransactionController:updateTransaction', + { + batchId, id: 'tx1', + txParams: { + authorizationList: ['0xAuth1'], + data: '0xapprovalData', + }, type: TransactionType.bridgeApproval, - }), + }, 'Update tx type to bridgeApproval', ); expect(result.approvalMeta).toStrictEqual( From 1f8b1d9c50020539e0b1009362afd3ce1cf85bdb Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Mar 2026 10:07:57 -0700 Subject: [PATCH 13/23] fix: unit tests --- .../src/utils/transaction.test.ts | 175 ++++++++++-------- 1 file changed, 100 insertions(+), 75 deletions(-) diff --git a/packages/bridge-status-controller/src/utils/transaction.test.ts b/packages/bridge-status-controller/src/utils/transaction.test.ts index 1d41d4f5aba..af52f413051 100644 --- a/packages/bridge-status-controller/src/utils/transaction.test.ts +++ b/packages/bridge-status-controller/src/utils/transaction.test.ts @@ -1823,47 +1823,52 @@ describe('Bridge Status Controller Transaction Utils', () => { }, }) as never; - const createMockMessagingSystem = () => ({ - call: jest.fn().mockImplementation((method: string) => { - if (method === 'AccountsController:getAccountByAddress') { - return mockAccount; - } - if (method === 'NetworkController:getNetworkConfiguration') { - return { - chainId: '0x1', - rpcUrl: 'https://mainnet.infura.io/v3/API_KEY', - }; - } - if (method === 'GasFeeController:getState') { - return { - gasFeeEstimates: { - low: { - suggestedMaxFeePerGas: '20', - suggestedMaxPriorityFeePerGas: '1', - }, - medium: { - suggestedMaxFeePerGas: '30', - suggestedMaxPriorityFeePerGas: '2', - }, - high: { - suggestedMaxFeePerGas: '40', - suggestedMaxPriorityFeePerGas: '3', + const createMockMessagingSystem = ( + estimateGasFeeOverrides: Record = {}, + ) => + ({ + call: jest.fn().mockImplementation((method: string) => { + if (method === 'AccountsController:getAccountByAddress') { + return mockAccount; + } + if (method === 'NetworkController:getNetworkConfiguration') { + return { + chainId: '0x1', + rpcUrl: 'https://mainnet.infura.io/v3/API_KEY', + }; + } + if (method === 'GasFeeController:getState') { + return { + gasFeeEstimates: { + low: { + suggestedMaxFeePerGas: '20', + suggestedMaxPriorityFeePerGas: '1', + }, + medium: { + suggestedMaxFeePerGas: '30', + suggestedMaxPriorityFeePerGas: '2', + }, + high: { + suggestedMaxFeePerGas: '40', + suggestedMaxPriorityFeePerGas: '3', + }, }, - }, - }; - } - if (method === 'TransactionController:estimateGasFee') { - return { - estimates: {}, - }; - } - return undefined; - }), - }); + }; + } + if (method === 'TransactionController:estimateGasFee') { + return ( + estimateGasFeeOverrides ?? { + estimates: {}, + } + ); + } + return undefined; + }), + }) as unknown as BridgeStatusControllerMessenger; beforeEach(() => { - mockMessagingSystem = - createMockMessagingSystem() as unknown as BridgeStatusControllerMessenger; + jest.clearAllMocks(); + mockMessagingSystem = createMockMessagingSystem(); }); it('should handle gasIncluded7702 flag set to true', async () => { @@ -2022,7 +2027,7 @@ describe('Bridge Status Controller Transaction Utils', () => { gasIncluded7702: false, }); - const mockEstimateGasFeeFn = jest.fn().mockResolvedValue({ + const mockMessenger = createMockMessagingSystem({ estimates: { medium: { maxFeePerGas: '0xabc', @@ -2030,10 +2035,11 @@ describe('Bridge Status Controller Transaction Utils', () => { }, }, }); + const callSpy = jest.spyOn(mockMessenger, 'call'); const result = await getAddTransactionBatchParams({ quoteResponse: mockQuoteResponse, - messenger: mockMessagingSystem, + messenger: mockMessenger, isBridgeTx: true, trade: mockQuoteResponse.trade, isDelegatedAccount: true, @@ -2043,13 +2049,35 @@ describe('Bridge Status Controller Transaction Utils', () => { expect(result.disable7702).toBe(false); // Gas is NOT sponsored expect(result.isGasFeeIncluded).toBe(false); - expect(mockMessagingSystem.call).toHaveBeenCalledWith( - 'TransactionController:estimateGasFee', - { - chainId: mockQuoteResponse.trade.chainId, - txParams: mockQuoteResponse.trade, - }, - ); + expect(callSpy.mock.calls).toMatchInlineSnapshot(` + [ + [ + "AccountsController:getAccountByAddress", + "0xUserAddress", + ], + [ + "NetworkController:findNetworkClientIdByChainId", + "0x1", + ], + [ + "GasFeeController:getState", + ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0x1", + "networkClientId": undefined, + "transactionParams": { + "data": "0xbridgeData", + "from": "0xUserAddress", + "gas": "21000", + "to": "0xBridgeContract", + "value": "0x1000", + }, + }, + ], + ] + `); // Transaction params should include gas fields expect(result.transactions).toHaveLength(1); expect(result.transactions[0].params).toHaveProperty('gas'); @@ -2064,15 +2092,13 @@ describe('Bridge Status Controller Transaction Utils', () => { gasIncluded7702: true, }); - const mockEstimateGasFeeFn = jest.fn().mockResolvedValue({}); - + const callSpy = jest.spyOn(mockMessagingSystem, 'call'); const result = await getAddTransactionBatchParams({ quoteResponse: mockQuoteResponse, messenger: mockMessagingSystem, isBridgeTx: true, trade: mockQuoteResponse.trade, isDelegatedAccount: true, - estimateGasFeeFn: mockEstimateGasFeeFn, }); // 7702 should be enabled @@ -2080,7 +2106,11 @@ describe('Bridge Status Controller Transaction Utils', () => { // Gas IS sponsored expect(result.isGasFeeIncluded).toBe(true); // Gas estimation should NOT have been called (skipped because gas is sponsored) - expect(mockEstimateGasFeeFn).not.toHaveBeenCalled(); + expect( + callSpy.mock.calls.filter( + ([action]) => action === 'TransactionController:estimateGasFee', + ), + ).toHaveLength(0); // Transaction params should NOT include gas fields expect(result.transactions).toHaveLength(1); expect(result.transactions[0].params).not.toHaveProperty('gas'); @@ -2132,7 +2162,7 @@ describe('Bridge Status Controller Transaction Utils', () => { } return undefined; }), - }; + } as unknown as BridgeStatusControllerMessenger; }; beforeEach(() => { @@ -2152,7 +2182,8 @@ describe('Bridge Status Controller Transaction Utils', () => { data: '0xapprovalData', }), ]; - const mockMessenger = createMockMessagingSystemWithTxs(txs); + const mockMessagingSystem = createMockMessagingSystemWithTxs(txs); + const callSpy = jest.spyOn(mockMessagingSystem, 'call'); const txDataByType = { [TransactionType.swap]: '0xswapData', @@ -2160,13 +2191,13 @@ describe('Bridge Status Controller Transaction Utils', () => { }; findAndUpdateTransactionsInBatch({ - messenger: mockMessenger as unknown as BridgeStatusControllerMessenger, + messenger: mockMessagingSystem, batchId, txDataByType, }); expect( - mockMessenger.call.mock.calls.filter( + callSpy.mock.calls.filter( ([action]) => action === 'TransactionController:updateTransaction', ), ).toMatchInlineSnapshot(` @@ -2213,7 +2244,7 @@ describe('Bridge Status Controller Transaction Utils', () => { ]; const mockMessenger = createMockMessagingSystemWithTxs(txs); - + const callSpy = jest.spyOn(mockMessenger, 'call'); const txDataByType = { [TransactionType.swap]: '0xswapData', }; @@ -2226,8 +2257,8 @@ describe('Bridge Status Controller Transaction Utils', () => { // Should identify and update 7702 transaction with delegationAddress expect( - mockMessenger.call.mock.calls.find( - (call) => call[0] === 'TransactionController:updateTransaction', + callSpy.mock.calls.find( + ([action]) => action === 'TransactionController:updateTransaction', ), ).toMatchInlineSnapshot(` [ @@ -2256,7 +2287,7 @@ describe('Bridge Status Controller Transaction Utils', () => { ]; const mockMessenger = createMockMessagingSystemWithTxs(txs); - + const callSpy = jest.spyOn(mockMessenger, 'call'); const txDataByType = { [TransactionType.swapApproval]: '0xapprovalData', }; @@ -2269,7 +2300,7 @@ describe('Bridge Status Controller Transaction Utils', () => { // Should match 7702 approval transaction by data expect( - mockMessenger.call.mock.calls.filter( + callSpy.mock.calls.filter( (call) => call[0] === 'TransactionController:updateTransaction', ), ).toMatchInlineSnapshot(` @@ -2306,7 +2337,7 @@ describe('Bridge Status Controller Transaction Utils', () => { ]; const mockMessenger = createMockMessagingSystemWithTxs(txs); - + const callSpy = jest.spyOn(mockMessenger, 'call'); const txDataByType = { [TransactionType.bridge]: '0xswapData', [TransactionType.bridgeApproval]: '0xapprovalData', @@ -2320,7 +2351,7 @@ describe('Bridge Status Controller Transaction Utils', () => { // Should update regular transactions by matching data expect( - mockMessenger.call.mock.calls.filter( + callSpy.mock.calls.filter( (call) => call[0] === 'TransactionController:updateTransaction', ), ).toMatchInlineSnapshot(` @@ -2362,21 +2393,21 @@ describe('Bridge Status Controller Transaction Utils', () => { }), ]; - const mockMessenger = createMockMessagingSystemWithTxs(txs); - + const mockMessagingSystem = createMockMessagingSystemWithTxs(txs); + const callSpy = jest.spyOn(mockMessagingSystem, 'call'); const txDataByType = { [TransactionType.swap]: '0xswapData', }; findAndUpdateTransactionsInBatch({ - messenger: mockMessenger as unknown as BridgeStatusControllerMessenger, + messenger: mockMessagingSystem, batchId, txDataByType, }); // Should not update transactions with different batchId expect( - mockMessenger.call.mock.calls.filter( + callSpy.mock.calls.filter( (call) => call[0] === 'TransactionController:updateTransaction', ), ).toHaveLength(0); @@ -2392,7 +2423,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }), ]; - const mockMessenger = createMockMessagingSystemWithTxs(txs); + const mockMessagingSystem = createMockMessagingSystemWithTxs(txs); const txDataByType = { [TransactionType.bridge]: '0xbridgeData', @@ -2400,19 +2431,13 @@ describe('Bridge Status Controller Transaction Utils', () => { // Test with bridge transaction — should match batch type for 7702 const result = findAndUpdateTransactionsInBatch({ - messenger: mockMessenger, + messenger: mockMessagingSystem, batchId, txDataByType, }); - // Should not match since it's looking for bridge but finds batch type - expect( - mockMessenger.call.mock.calls.filter( - (call) => call[0] === 'TransactionController:updateTransaction', - ), - ).toHaveLength(0); // Should match since 7702 bridge transactions use batch type - expect(mockMessenger.call).toHaveBeenCalledWith( + expect(mockMessagingSystem.call).toHaveBeenCalledWith( 'TransactionController:updateTransaction', { batchId, From 98fabbfe253bdb2223ee5269f49f19db7207db57 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Mar 2026 13:43:25 -0700 Subject: [PATCH 14/23] 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 7857fdea4ca..ba12a51430a 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] +### Fixed + +- Narrow the HexStringSchema and HexStringSchema's inferred type (string -> Hex) to fix type errors surfaced when using the QuoteReponse's `trade` and `approval` data ([#8188](https://github.com/MetaMask/core/pull/8188)) + ## [69.1.1] ### Changed diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index cf9dce7d9fe..10849c13d1f 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 + +- **BREAKING:** Replace transaction handlers provided to the `BridgeStatusController` constructor with calls to the TransactionController, through the controller messenger. Clients will need to add the `TransactionControllerUpdateTransactionAction`, `TransactionControllerAddTransactionAction`, `TransactionControllerAddTransactionBatchAction` and `TransactionControllerEstimateGasFeeAction` permissions to their controller init modules in addition to updating the constructor ([#8188](https://github.com/MetaMask/core/pull/8188)) + ## [69.0.0] ### Added From 9693c6715ecce91432e99bdbd3fddd58172e9ed7 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Mar 2026 13:44:09 -0700 Subject: [PATCH 15/23] fix: lint errors --- .../src/strategy/bridge/bridge-quotes.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts index ce0602309b8..9c68fa54f18 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts @@ -167,10 +167,10 @@ export async function refreshQuote( * @returns Batch transaction. */ function getBatchTransaction(transaction: TxData): BatchTransaction { - const data = transaction.data as Hex; + const { data } = transaction; const gas = transaction.gasLimit ? toHex(transaction.gasLimit) : undefined; - const to = transaction.to as Hex; - const value = transaction.value as Hex; + const { to } = transaction; + const { value } = transaction; return { data, From 31fef9e975087b5d85fb2e19faadcf52422ad072 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Mar 2026 13:50:53 -0700 Subject: [PATCH 16/23] fix: lint --- eslint-suppressions.json | 24 ------------------- .../src/strategy/bridge/bridge-quotes.ts | 1 - 2 files changed, 25 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 3960faf98b9..a8374e00bb1 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -573,14 +573,8 @@ } }, "packages/bridge-controller/src/utils/validators.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 1 - }, "@typescript-eslint/naming-convention": { "count": 1 - }, - "id-length": { - "count": 4 } }, "packages/bridge-controller/tests/mock-sse.ts": { @@ -591,16 +585,6 @@ "count": 2 } }, - "packages/bridge-status-controller/src/bridge-status-controller.test.ts": { - "no-new": { - "count": 1 - } - }, - "packages/bridge-status-controller/src/utils/gas.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 4 - } - }, "packages/bridge-status-controller/src/utils/snaps.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 1 @@ -611,15 +595,7 @@ "count": 3 } }, - "packages/bridge-status-controller/src/utils/transaction.test.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 3 - } - }, "packages/bridge-status-controller/src/utils/transaction.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 7 - }, "@typescript-eslint/prefer-nullish-coalescing": { "count": 1 } diff --git a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts index 9c68fa54f18..bbddb96f65d 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts @@ -6,7 +6,6 @@ import { toChecksumHexAddress, toHex } from '@metamask/controller-utils'; import { TransactionType } from '@metamask/transaction-controller'; import type { BatchTransaction } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; -import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import { orderBy } from 'lodash'; From 467819260d0b4df81a3dda7c2ec1e98e0fd1e02a Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Mar 2026 13:58:05 -0700 Subject: [PATCH 17/23] chore: changelog --- packages/bridge-controller/CHANGELOG.md | 2 +- packages/transaction-pay-controller/CHANGELOG.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index ba12a51430a..709091c8fdd 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Narrow the HexStringSchema and HexStringSchema's inferred type (string -> Hex) to fix type errors surfaced when using the QuoteReponse's `trade` and `approval` data ([#8188](https://github.com/MetaMask/core/pull/8188)) +- **BREAKING:** Narrow the HexStringSchema and HexStringSchema's inferred type (string -> Hex) to fix type errors surfaced when using the QuoteReponse's `trade` and `approval` data ([#8188](https://github.com/MetaMask/core/pull/8188)) ## [69.1.1] diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index cbf0d4ebbde..f9cfe8422ec 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Remove type assertions when reading hex values from bridge quote data ([#8188](https://github.com/MetaMask/core/pull/8188)) + ## [17.1.0] ### Added From 675dcfd329c7aef2e9abe6f3081036a0c82c6162 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 17 Mar 2026 14:16:30 -0700 Subject: [PATCH 18/23] fix: test --- ...e-status-controller.intent-manager.test.ts | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts index c22b837c8df..57e5d2291f1 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts @@ -147,9 +147,26 @@ describe('IntentManager', () => { ); manager.syncTransactionFromIntentStatus('order-3', historyItem); - expect(mockCall.mock.calls[0][0]).toMatchInlineSnapshot( - `"TransactionController:getState"`, - ); + expect(mockCall.mock.calls).toMatchInlineSnapshot(` + [ + [ + "TransactionController:getState", + ], + [ + "TransactionController:updateTransaction", + { + "hash": "0xhash", + "id": "tx-2", + "status": "confirmed", + "txReceipt": { + "status": "0x1", + "transactionHash": "0xhash", + }, + }, + "BridgeStatusController - Intent order status updated: completed", + ], + ] + `); }); it('getIntentTransactionStatus returns undefined when getOrderStatus rejects with non-Error', async () => { From e62fa239850a54e5794ce10588731d72982d70c6 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 18 Mar 2026 09:57:23 -0700 Subject: [PATCH 19/23] fix: undo hex --- eslint-suppressions.json | 6 ++++++ packages/bridge-controller/CHANGELOG.md | 4 ---- .../bridge-controller/src/utils/bridge.ts | 2 +- .../bridge-controller/src/utils/validators.ts | 18 ++++++++--------- ...e-status-controller.intent-manager.test.ts | 20 +++++++++++++++++-- .../src/bridge-status-controller.ts | 5 +++-- .../bridge-status-controller/src/utils/gas.ts | 6 +++--- .../src/utils/transaction.ts | 8 ++++---- .../transaction-pay-controller/CHANGELOG.md | 4 ---- .../src/strategy/bridge/bridge-quotes.ts | 7 ++++--- 10 files changed, 48 insertions(+), 32 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 7a4d94844b7..1f239e05e18 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -573,8 +573,14 @@ } }, "packages/bridge-controller/src/utils/validators.ts": { + "@typescript-eslint/explicit-function-return-type": { + "count": 1 + }, "@typescript-eslint/naming-convention": { "count": 1 + }, + "id-length": { + "count": 4 } }, "packages/bridge-controller/tests/mock-sse.ts": { diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 9f0197aa0f6..513575c59b2 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,10 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Fixed - -- **BREAKING:** Narrow the HexStringSchema and HexStringSchema's inferred type (string -> Hex) to fix type errors surfaced when using the QuoteReponse's `trade` and `approval` data ([#8188](https://github.com/MetaMask/core/pull/8188)) - ### Changed - Bump `@metamask/assets-controller` from `^2.4.0` to `^3.0.0` ([#8232](https://github.com/MetaMask/core/pull/8232)) diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index a77653fb513..c9b5be725f8 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -126,7 +126,7 @@ export const getEthUsdtResetData = ( '0', ]); - return data as Hex; + return data; }; export const isEthUsdt = ( diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index c0f0834b9f7..f2e85cb1bb8 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -40,23 +40,23 @@ export enum ActionTypes { REFUEL = 'refuel', } -const HexAddressSchema = define<`0x${string}`>('HexAddress', (data: unknown) => - isValidHexAddress(data as string, { allowNonPrefixed: false }), +const HexAddressSchema = define('HexAddress', (v: unknown) => + isValidHexAddress(v as string, { allowNonPrefixed: false }), ); -const HexStringSchema = define<`0x${string}`>('HexString', (data: unknown) => - isStrictHexString(data as string), +const HexStringSchema = define('HexString', (v: unknown) => + isStrictHexString(v as string), ); const VersionStringSchema = define( 'VersionString', - (data: unknown) => - typeof data === 'string' && - /^(\d+\.*){2}\d+$/u.test(data) && - data.split('.').length === 3, + (v: unknown) => + typeof v === 'string' && + /^(\d+\.*){2}\d+$/u.test(v) && + v.split('.').length === 3, ); -export const truthyString = (data: string): boolean => Boolean(data?.length); +export const truthyString = (s: string) => Boolean(s?.length); const TruthyDigitStringSchema = pattern(string(), /^\d+$/u); const ChainIdSchema = number(); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts index 1379e96c078..f310d3d1592 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts @@ -41,6 +41,10 @@ const createManagerOptions = (overrides?: { }); describe('IntentManager', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('returns early when no original tx id is present', () => { const options = createManagerOptions(); const manager = new IntentManager(options); @@ -148,6 +152,9 @@ describe('IntentManager', () => { expect(mockCall.mock.calls).toMatchInlineSnapshot(` [ + [ + "AuthenticationController:getBearerToken", + ], [ "TransactionController:getState", ], @@ -326,12 +333,15 @@ describe('IntentManager', () => { ); manager.syncTransactionFromIntentStatus('order-3', historyItem); - expect(mockCall).toHaveBeenCalledTimes(2); + expect(mockCall).toHaveBeenCalledTimes(3); manager.syncTransactionFromIntentStatus('order-3', historyItem); expect(mockCall.mock.calls).toMatchInlineSnapshot(` [ + [ + "AuthenticationController:getBearerToken", + ], [ "TransactionController:getState", ], @@ -396,12 +406,15 @@ describe('IntentManager', () => { ); manager.syncTransactionFromIntentStatus('order-3', historyItem); - expect(mockCall).toHaveBeenCalledTimes(2); + expect(mockCall).toHaveBeenCalledTimes(3); manager.syncTransactionFromIntentStatus('order-3', historyItem); expect(mockCall.mock.calls).toMatchInlineSnapshot(` [ + [ + "AuthenticationController:getBearerToken", + ], [ "TransactionController:getState", ], @@ -559,6 +572,9 @@ describe('IntentManager', () => { expect(mockCall.mock.calls).toMatchInlineSnapshot(` [ + [ + "AuthenticationController:getBearerToken", + ], [ "TransactionController:getState", ], diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 5fe46f8d8ad..13f86b4775e 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -735,7 +735,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { const params = { ...trade, - data: trade.data, - to: trade.to, - value: trade.value, + data: trade.data as `0x${string}`, + to: trade.to as `0x${string}`, + value: trade.value as `0x${string}`, }; if (skipGasFields) { return params; @@ -234,7 +234,7 @@ export const getAddTransactionBatchParams = async ({ networkClientId, requireApproval, origin: 'metamask', - from: trade.from, + from: trade.from as `0x${string}`, transactions, }; diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 3bed0e84142..1b95077c939 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,10 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Fixed - -- Remove type assertions when reading hex values from bridge quote data ([#8188](https://github.com/MetaMask/core/pull/8188)) - ### Changed - Bump `@metamask/assets-controller` from `^2.4.0` to `^3.0.0` ([#8232](https://github.com/MetaMask/core/pull/8232)) diff --git a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts index bbddb96f65d..ce0602309b8 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts @@ -6,6 +6,7 @@ import { toChecksumHexAddress, toHex } from '@metamask/controller-utils'; import { TransactionType } from '@metamask/transaction-controller'; import type { BatchTransaction } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; +import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import { orderBy } from 'lodash'; @@ -166,10 +167,10 @@ export async function refreshQuote( * @returns Batch transaction. */ function getBatchTransaction(transaction: TxData): BatchTransaction { - const { data } = transaction; + const data = transaction.data as Hex; const gas = transaction.gasLimit ? toHex(transaction.gasLimit) : undefined; - const { to } = transaction; - const { value } = transaction; + const to = transaction.to as Hex; + const value = transaction.value as Hex; return { data, From 6efd97506998fbdc50d5d6dc22030c09bf535384 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 18 Mar 2026 10:22:46 -0700 Subject: [PATCH 20/23] chore: undo addTransactionBatch --- .../bridge-status-controller/CHANGELOG.md | 2 +- .../bridge-status-controller.test.ts.snap | 301 +++++++++++++----- .../src/bridge-status-controller.test.ts | 119 +++++-- .../src/bridge-status-controller.ts | 10 +- .../bridge-status-controller/src/types.ts | 2 - .../src/utils/transaction.ts | 2 +- 6 files changed, 333 insertions(+), 103 deletions(-) diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 6f10c35d1b5..ffe2b673994 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** Replace transaction handlers provided to the `BridgeStatusController` constructor with calls to the TransactionController, through the controller messenger. Clients will need to add the `TransactionControllerUpdateTransactionAction`, `TransactionControllerAddTransactionAction`, `TransactionControllerAddTransactionBatchAction` and `TransactionControllerEstimateGasFeeAction` permissions to their controller init modules in addition to updating the constructor ([#8188](https://github.com/MetaMask/core/pull/8188)) +- **BREAKING:** Replace transaction handlers provided to the `BridgeStatusController` constructor with calls to the TransactionController, through the controller messenger. Clients will need to add the `TransactionControllerUpdateTransactionAction`, `TransactionControllerAddTransactionAction`, and `TransactionControllerEstimateGasFeeAction` permissions to their controller init modules in addition to updating the constructor ([#8188](https://github.com/MetaMask/core/pull/8188)) - Moved controller calls from bridge-status-controller.ts to their own utils for better readability ([#8226](https://github.com/MetaMask/core/pull/8226)) ## [69.0.0] 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 c352e6dc48b..b518d8ed036 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 @@ -1506,6 +1506,40 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac `; exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions and include quotesReceivedContext 3`] = ` +[ + [ + { + "disable7702": true, + "from": "0xaccount1", + "isGasFeeIncluded": false, + "isGasFeeSponsored": false, + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": false, + "transactions": [ + { + "assetsFiatValues": { + "receiving": "2.9999", + "sending": "2.00", + }, + "params": { + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "bridge", + }, + ], + }, + ], +] +`; + +exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions and include quotesReceivedContext 4`] = ` [ [ "BridgeController:stopPollingForQuotes", @@ -1588,36 +1622,6 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac }, }, ], - [ - "TransactionController:addTransactionBatch", - { - "disable7702": true, - "from": "0xaccount1", - "isGasFeeIncluded": false, - "isGasFeeSponsored": false, - "networkClientId": "arbitrum", - "origin": "metamask", - "requireApproval": false, - "transactions": [ - { - "assetsFiatValues": { - "receiving": "2.9999", - "sending": "2.00", - }, - "params": { - "data": "0xdata", - "from": "0xaccount1", - "gas": "0x5208", - "maxFeePerGas": "0x0", - "maxPriorityFeePerGas": "0x0", - "to": "0xbridgeContract", - "value": "0x0", - }, - "type": "bridge", - }, - ], - }, - ], [ "TransactionController:getState", ], @@ -3396,6 +3400,155 @@ exports[`BridgeStatusController submitTx: EVM swap should estimate gas when gasI } `; +exports[`BridgeStatusController submitTx: EVM swap should gracefully handle isAtomicBatchSupported failure 1`] = ` +[ + [ + "BridgeController:stopPollingForQuotes", + "Transaction submitted", + undefined, + ], + [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Submitted", + { + "action_type": "swapbridge-v1", + "chain_id_destination": "eip155:42161", + "chain_id_source": "eip155:42161", + "custom_slippage": false, + "gas_included": false, + "gas_included_7702": false, + "is_hardware_wallet": false, + "location": "Main View", + "price_impact": 0, + "provider": "lifi_across", + "quoted_time_minutes": 0, + "stx_enabled": false, + "swap_type": "single_chain", + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", + "usd_amount_source": 1.01, + "usd_quoted_gas": 2.5778, + "usd_quoted_return": 0, + }, + ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], + [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + [ + "NetworkController:findNetworkClientIdByChainId", + "0xa4b1", + ], + [ + "GasFeeController:getState", + ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum-client-id", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xtokenContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xtokenContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum-client-id", + "origin": "metamask", + "requireApproval": false, + "type": "swapApproval", + }, + ], + [ + "TransactionController:getState", + ], + [ + "AccountsController:getAccountByAddress", + "0xaccount1", + ], + [ + "NetworkController:findNetworkClientIdByChainId", + "0xa4b1", + ], + [ + "GasFeeController:getState", + ], + [ + "TransactionController:estimateGasFee", + { + "chainId": "0xa4b1", + "networkClientId": "arbitrum", + "transactionParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "21000", + "gasLimit": "21000", + "to": "0xbridgeContract", + "value": "0x0", + }, + }, + ], + [ + "TransactionController:addTransaction", + { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "gasLimit": "21000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "to": "0xbridgeContract", + "value": "0x0", + }, + { + "actionId": "1234567890.456", + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": false, + "type": "swap", + }, + ], + [ + "TransactionController:getState", + ], +] +`; + exports[`BridgeStatusController submitTx: EVM swap should handle a gasless swap transaction with approval 2`] = ` { "account": "0xaccount1", @@ -3657,6 +3810,52 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti `; exports[`BridgeStatusController submitTx: EVM swap should handle smart transactions 3`] = ` +[ + [ + { + "disable7702": true, + "from": "0xaccount1", + "isGasFeeIncluded": false, + "isGasFeeSponsored": false, + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": false, + "transactions": [ + { + "params": { + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xtokenContract", + "value": "0x0", + }, + "type": "swapApproval", + }, + { + "assetsFiatValues": { + "receiving": "2.9999", + "sending": "2.00", + }, + "params": { + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "swap", + }, + ], + }, + ], +] +`; + +exports[`BridgeStatusController submitTx: EVM swap should handle smart transactions 4`] = ` [ [ "BridgeController:stopPollingForQuotes", @@ -3742,48 +3941,6 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti }, }, ], - [ - "TransactionController:addTransactionBatch", - { - "disable7702": true, - "from": "0xaccount1", - "isGasFeeIncluded": false, - "isGasFeeSponsored": false, - "networkClientId": "arbitrum", - "origin": "metamask", - "requireApproval": false, - "transactions": [ - { - "params": { - "data": "0xapprovalData", - "from": "0xaccount1", - "gas": "0x5208", - "maxFeePerGas": "0x0", - "maxPriorityFeePerGas": "0x0", - "to": "0xtokenContract", - "value": "0x0", - }, - "type": "swapApproval", - }, - { - "assetsFiatValues": { - "receiving": "2.9999", - "sending": "2.00", - }, - "params": { - "data": "0xdata", - "from": "0xaccount1", - "gas": "0x5208", - "maxFeePerGas": "0x0", - "maxPriorityFeePerGas": "0x0", - "to": "0xbridgeContract", - "value": "0x0", - }, - "type": "swap", - }, - ], - }, - ], [ "TransactionController:getState", ], 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 ff3c9540bbb..0bb67e4537d 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -607,6 +607,7 @@ const executePollingWithPendingStatus = async () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), config: {}, }); const startPollingSpy = jest.spyOn(bridgeStatusController, 'startPolling'); @@ -642,6 +643,7 @@ const mockSelectedAccount = { }, }, }; +const addTransactionBatchFn = jest.fn(); const getController = ( call: jest.Mock, @@ -659,6 +661,7 @@ const getController = ( } as never, clientId, fetchFn: mockFetchFn, + addTransactionBatchFn, traceFn, }); @@ -682,6 +685,7 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); expect(bridgeStatusController.state).toStrictEqual(EMPTY_INIT_STATE); expect(mockMessengerSubscribe.mock.calls).toMatchSnapshot(); @@ -693,6 +697,7 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), state: { txHistory: MockTxHistory.getPending(), }, @@ -726,6 +731,7 @@ describe('BridgeStatusController', () => { .fn() .mockResolvedValueOnce(MockStatusResponse.getPending()) .mockResolvedValueOnce(MockStatusResponse.getComplete()), + addTransactionBatchFn: jest.fn(), }); jest.advanceTimersByTime(10000); await flushPromises(); @@ -766,6 +772,7 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: mockFetchFn, + addTransactionBatchFn: jest.fn(), }); // Execution @@ -823,6 +830,7 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: failedFetch, + addTransactionBatchFn: jest.fn(), }); // Execution @@ -896,6 +904,7 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); const argsWithoutId = getMockStartPollingForBridgeTxStatusArgs(); @@ -916,6 +925,7 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); const argsWithoutMeta = getMockStartPollingForBridgeTxStatusArgs(); @@ -939,6 +949,7 @@ describe('BridgeStatusController', () => { fetchFn: jest .fn() .mockResolvedValueOnce(MockStatusResponse.getPending()), + addTransactionBatchFn: jest.fn(), }); // Execution @@ -978,6 +989,7 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest.spyOn( bridgeStatusUtils, @@ -1061,6 +1073,7 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); // Start polling with args that have no srcTxHash @@ -1098,6 +1111,7 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest @@ -1143,6 +1157,7 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); // Execution @@ -1209,6 +1224,7 @@ describe('BridgeStatusController', () => { fetchFn: jest .fn() .mockResolvedValueOnce(MockStatusResponse.getPending()), + addTransactionBatchFn: jest.fn(), traceFn: jest.fn(), }); @@ -1285,6 +1301,7 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), state: EMPTY_INIT_STATE, }); @@ -1300,6 +1317,7 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), state: { txHistory: { bridgeTxMetaId1: { @@ -1392,6 +1410,7 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') @@ -1494,6 +1513,7 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') @@ -1609,6 +1629,7 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') @@ -2500,7 +2521,7 @@ describe('BridgeStatusController', () => { gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); mockCall.mockResolvedValueOnce(mockEstimateGasFeeResult); - mockCall.mockResolvedValueOnce({ + addTransactionBatchFn.mockResolvedValueOnce({ batchId: 'batchId1', }); mockCall.mockReturnValueOnce({ @@ -2572,7 +2593,7 @@ describe('BridgeStatusController', () => { it('should handle smart transactions and include quotesReceivedContext', async () => { setupEventTrackingMocks(mockMessengerCall); setupBridgeStxMocks(mockMessengerCall); - mockMessengerCall.mockResolvedValueOnce({ + addTransactionBatchFn.mockResolvedValueOnce({ batchId: 'batchId1', }); @@ -2590,6 +2611,7 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); expect(controller.state.txHistory[result.id]).toMatchSnapshot(); + expect(addTransactionBatchFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); }); @@ -2734,7 +2756,7 @@ describe('BridgeStatusController', () => { gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); - mockMessengerCall.mockResolvedValueOnce({ + addTransactionBatchFn.mockResolvedValueOnce({ batchId: 'batchId1', }); mockMessengerCall.mockReturnValueOnce({ @@ -2778,17 +2800,13 @@ describe('BridgeStatusController', () => { ([action]) => action === 'TransactionController:addTransaction', ), ).toHaveLength(0); - expect( - mockCalls.filter( - ([action]) => action === 'TransactionController:addTransactionBatch', - ), - ).toHaveLength(1); + expect(addTransactionBatchFn).toHaveBeenCalledTimes(1); expect( mockCalls.filter( ([action]) => action === 'TransactionController:updateTransaction', ), ).toHaveLength(1); - expect(mockMessengerCall).toHaveBeenCalledTimes(15); + expect(mockMessengerCall).toHaveBeenCalledTimes(14); }); it('should throw an error if approval tx fails', async () => { @@ -3393,7 +3411,7 @@ describe('BridgeStatusController', () => { setupEventTrackingMocks(mockMessengerCall); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); mockMessengerCall.mockReturnValueOnce('arbitrum'); - mockMessengerCall.mockResolvedValueOnce({ + addTransactionBatchFn.mockResolvedValueOnce({ batchId: 'batchId1', }); mockMessengerCall.mockReturnValueOnce({ @@ -3434,12 +3452,13 @@ describe('BridgeStatusController', () => { } `); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); + expect(addTransactionBatchFn).toHaveBeenCalledTimes(1); expect( mockMessengerCall.mock.calls.filter( ([action]) => action === 'TransactionController:addTransaction', ), ).toHaveLength(0); - expect(mockMessengerCall).toHaveBeenCalledTimes(9); + expect(mockMessengerCall).toHaveBeenCalledTimes(8); expect(controller.state.txHistory[result.id]).toMatchSnapshot(); }); @@ -3665,7 +3684,7 @@ describe('BridgeStatusController', () => { setupEventTrackingMocks(mockMessengerCall); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); mockMessengerCall.mockReturnValueOnce('arbitrum'); - mockMessengerCall.mockResolvedValueOnce({ + addTransactionBatchFn.mockResolvedValueOnce({ batchId: 'batchId1', }); mockMessengerCall.mockReturnValueOnce({ @@ -3698,12 +3717,8 @@ describe('BridgeStatusController', () => { controller.stopAllPolling(); // Should use batch path because gasIncluded7702 = true + expect(addTransactionBatchFn).toHaveBeenCalledTimes(1); const mockCalls = mockMessengerCall.mock.calls; - expect( - mockCalls.filter( - ([action]) => action === 'TransactionController:addTransactionBatch', - ), - ).toHaveLength(1); expect( mockCalls.filter( ([action]) => action === 'TransactionController:addTransaction', @@ -3717,7 +3732,7 @@ describe('BridgeStatusController', () => { setupEventTrackingMocks(mockMessengerCall); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); mockMessengerCall.mockReturnValueOnce('arbitrum'); - mockMessengerCall.mockResolvedValueOnce({ + addTransactionBatchFn.mockResolvedValueOnce({ batchId: 'batchId1', }); mockMessengerCall.mockReturnValueOnce({ @@ -3765,7 +3780,7 @@ describe('BridgeStatusController', () => { gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); - mockMessengerCall.mockResolvedValueOnce({ + addTransactionBatchFn.mockResolvedValueOnce({ batchId: 'batchId1', }); mockMessengerCall.mockReturnValueOnce({ @@ -3784,6 +3799,7 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); expect(controller.state.txHistory[result.id]).toMatchSnapshot(); + expect(addTransactionBatchFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); }); @@ -3836,7 +3852,7 @@ describe('BridgeStatusController', () => { gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); mockMessengerCall.mockResolvedValueOnce(mockEstimateGasFeeResult); - mockMessengerCall.mockResolvedValueOnce({ + addTransactionBatchFn.mockResolvedValueOnce({ batchId: 'batchId1', }); mockMessengerCall.mockReturnValueOnce({ @@ -3868,12 +3884,62 @@ describe('BridgeStatusController', () => { ([action]) => action === 'TransactionController:addTransaction', ), ).toHaveLength(0); + expect(addTransactionBatchFn).toHaveBeenCalledTimes(1); + expect(mockMessengerCall).toHaveBeenCalledTimes(11); + }); + + it('should gracefully handle isAtomicBatchSupported failure', async () => { + // Manually set up mocks without setupEventTrackingMocks + // to control the isAtomicBatchSupported mock + mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); // getAccountByAddress + mockMessengerCall.mockImplementationOnce(jest.fn()); // track event + mockMessengerCall.mockRejectedValueOnce( + new Error('isAtomicBatchSupported failed'), + ); // isAtomicBatchSupported throws + setupApprovalMocks(); + setupBridgeMocks(); + + const { controller } = getController(mockMessengerCall); + const result = await controller.submitTx( + (mockEvmQuoteResponse.trade as TxData).from, + mockEvmQuoteResponse, + false, // STX disabled - uses non-batch path + ); + controller.stopAllPolling(); + + // Should fall back to non-batch path when isAtomicBatchSupported throws + const mockCalls = mockMessengerCall.mock.calls; expect( mockCalls.filter( - ([action]) => action === 'TransactionController:addTransactionBatch', + ([action]) => action === 'TransactionController:estimateGasFee', ), - ).toHaveLength(1); - expect(mockMessengerCall).toHaveBeenCalledTimes(12); + ).toHaveLength(2); + expect( + mockCalls.filter( + ([action]) => action === 'TransactionController:addTransaction', + ), + ).toHaveLength(2); + expect(mockMessengerCall).toHaveBeenCalledTimes(16); + expect(addTransactionBatchFn).not.toHaveBeenCalled(); + expect(mockCalls).toMatchSnapshot(); + expect(result).toMatchInlineSnapshot(` + { + "chainId": "0xa4b1", + "hash": "0xevmTxHash", + "id": "test-tx-id", + "status": "unapproved", + "time": 1234567890, + "txParams": { + "chainId": "0xa4b1", + "data": "0xdata", + "from": "0xaccount1", + "gasLimit": "0x5208", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "swap", + } + `); }); }); @@ -3887,6 +3953,7 @@ describe('BridgeStatusController', () => { messenger: mockMessenger, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), state: { txHistory: { ...MockTxHistory.getPending({ @@ -3909,6 +3976,7 @@ describe('BridgeStatusController', () => { messenger: mockMessenger, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), state: { txHistory: { bridgeTxMetaId1: { @@ -3945,6 +4013,7 @@ describe('BridgeStatusController', () => { messenger: mockMessenger, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), state: { txHistory: { bridgeTxMetaId1: { @@ -3981,6 +4050,7 @@ describe('BridgeStatusController', () => { messenger: mockMessenger, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), state: { txHistory: { bridgeTxMetaId1: { @@ -4045,6 +4115,7 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), state: { txHistory: { bridgeTxMetaId1: { @@ -4143,6 +4214,7 @@ describe('BridgeStatusController', () => { messenger: mockMessenger, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), state: { txHistory: { noHashTx: { @@ -4246,6 +4318,7 @@ describe('BridgeStatusController', () => { messenger: mockBridgeStatusMessenger, clientId: BridgeClientId.EXTENSION, fetchFn: mockFetchFn, + addTransactionBatchFn: jest.fn(), state: { txHistory: { ...MockTxHistory.getPending(), diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 13f86b4775e..6a9d55e5441 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -136,6 +136,8 @@ export class BridgeStatusController extends StaticIntervalPollingController; clientId: BridgeClientId; fetchFn: FetchFunction; + addTransactionBatchFn: typeof TransactionController.prototype.addTransactionBatch; config?: { customBridgeApiBaseUrl?: string; }; @@ -168,6 +172,7 @@ export class BridgeStatusController extends StaticIntervalPollingController diff --git a/packages/bridge-status-controller/src/utils/transaction.ts b/packages/bridge-status-controller/src/utils/transaction.ts index 790c219f465..764f5a41fe9 100644 --- a/packages/bridge-status-controller/src/utils/transaction.ts +++ b/packages/bridge-status-controller/src/utils/transaction.ts @@ -15,12 +15,12 @@ import type { TransactionController, TransactionMeta, } from '@metamask/transaction-controller'; +import type { TransactionBatchSingleRequest } from '@metamask/transaction-controller'; import { createProjectLogger } from '@metamask/utils'; import { getAccountByAddress } from './accounts'; import { calculateGasFees } from './gas'; import { getNetworkClientIdByChainId } from './network'; -import type { TransactionBatchSingleRequest } from '../../../transaction-controller/src/types'; import { APPROVAL_DELAY_MS } from '../constants'; import type { BridgeStatusControllerMessenger } from '../types'; From 6c636cdc962ef66a8e0acd1060f82f5876a09413 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 18 Mar 2026 10:25:02 -0700 Subject: [PATCH 21/23] chore: update suppressions --- eslint-suppressions.json | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 1f239e05e18..b539365ba61 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -591,16 +591,6 @@ "count": 2 } }, - "packages/bridge-status-controller/src/bridge-status-controller.test.ts": { - "no-new": { - "count": 1 - } - }, - "packages/bridge-status-controller/src/utils/gas.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 4 - } - }, "packages/bridge-status-controller/src/utils/swap-received-amount.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 3 From a7fa55ee4d44235e561b3b8cf4a85e8dff5259e1 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 18 Mar 2026 11:09:00 -0700 Subject: [PATCH 22/23] fix: unit test --- .../src/bridge-status-controller.intent.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts index b27d2baa3d0..b95ba23110b 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts @@ -248,6 +248,7 @@ const setup = (options?: { }, clientId: options?.clientId ?? BridgeClientId.EXTENSION, fetchFn: (...args: any[]) => mockFetchFn(...args), + addTransactionBatchFn: jest.fn(), config: { customBridgeApiBaseUrl: 'http://localhost' }, traceFn: (_req: any, fn?: any): any => fn?.(), }); @@ -1052,6 +1053,7 @@ describe('BridgeStatusController (target uncovered branches)', () => { state, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), + addTransactionBatchFn: jest.fn(), config: { customBridgeApiBaseUrl: 'http://localhost' }, traceFn: (_r: any, fn?: any): any => fn?.(), }); From 31816a7d1b0dc387977bf3f5199ea5054918ca97 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 19 Mar 2026 09:09:52 -0700 Subject: [PATCH 23/23] fix: lint --- eslint-suppressions.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index d5316029c0c..be412fc9d8d 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -612,9 +612,6 @@ "packages/bridge-status-controller/src/utils/transaction.ts": { "@typescript-eslint/prefer-nullish-coalescing": { "count": 1 - }, - "import-x/no-relative-packages": { - "count": 1 } }, "packages/chain-agnostic-permission/src/caip25Permission.ts": {