diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 83992a951c2..cd9e2a8fa7d 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** `BridgeStatusControllerMessenger` must now allow `TransactionController:isAtomicBatchSupported` action ([#8125](https://github.com/MetaMask/core/pull/8125)) + +### Fixed + +- Delegated accounts (EIP-7702) now use batched transactions to avoid in-flight transaction limit ([#8125](https://github.com/MetaMask/core/pull/8125)) +- Bridge transaction types now properly matched in 7702 batch path ([#8125](https://github.com/MetaMask/core/pull/8125)) +- Delegated account batch transactions now recorded in bridge status history ([#8125](https://github.com/MetaMask/core/pull/8125)) +- Gas fields now included for delegated account transactions that are not gas-sponsored ([#8125](https://github.com/MetaMask/core/pull/8125)) + ## [68.0.1] ### Changed diff --git a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap index 29bbc53bad7..d1050e81fe9 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 @@ -579,6 +579,15 @@ exports[`BridgeStatusController submitTx: EVM bridge should call handleMobileHar "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -818,6 +827,15 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0x2105", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -1057,6 +1075,15 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xe708", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -1363,6 +1390,15 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -1563,6 +1599,15 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -1802,6 +1847,15 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -2156,6 +2210,15 @@ exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -2429,6 +2492,15 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -2688,6 +2760,15 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -2765,6 +2846,15 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -2839,6 +2929,15 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -3132,6 +3231,15 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -3201,6 +3309,15 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an "AccountsController:getAccountByAddress", "0xaccount1", ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -3411,6 +3528,15 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", diff --git a/packages/bridge-status-controller/src/bridge-status-controller.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.test.ts index 5d4469f177f..2650bb2c502 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -2538,6 +2538,7 @@ describe('BridgeStatusController', () => { const setupEventTrackingMocks = (mockCall: jest.Mock) => { mockCall.mockReturnValueOnce(mockSelectedAccount); mockCall.mockImplementationOnce(jest.fn()); // track event + mockCall.mockReturnValueOnce([]); // isAtomicBatchSupported }; const setupApprovalMocks = (mockCall: jest.Mock) => { @@ -2854,7 +2855,7 @@ describe('BridgeStatusController', () => { expect(estimateGasFeeFn).toHaveBeenCalledTimes(3); expect(addTransactionFn).not.toHaveBeenCalled(); expect(addTransactionBatchFn).toHaveBeenCalledTimes(1); - expect(mockMessengerCall).toHaveBeenCalledTimes(9); + expect(mockMessengerCall).toHaveBeenCalledTimes(10); }); it('should throw an error if approval tx fails', async () => { @@ -3021,6 +3022,7 @@ describe('BridgeStatusController', () => { }, }); mockMessengerCall.mockImplementationOnce(jest.fn()); // track event + mockMessengerCall.mockReturnValueOnce([]); // isAtomicBatchSupported setupApprovalMocks(mockMessengerCall); setupBridgeMocks(mockMessengerCall); @@ -3103,6 +3105,7 @@ describe('BridgeStatusController', () => { }, }); mockMessengerCall.mockImplementationOnce(jest.fn()); // track event + mockMessengerCall.mockReturnValueOnce([]); // isAtomicBatchSupported setupApprovalMocks(mockMessengerCall); setupBridgeMocks(mockMessengerCall); @@ -3364,6 +3367,7 @@ describe('BridgeStatusController', () => { const setupEventTrackingMocks = (mockCall: jest.Mock) => { mockCall.mockReturnValueOnce(mockSelectedAccount); mockCall.mockImplementationOnce(jest.fn()); // track event + mockCall.mockReturnValueOnce([]); // isAtomicBatchSupported }; const setupApprovalMocks = () => { @@ -3422,11 +3426,12 @@ describe('BridgeStatusController', () => { const { approvalTxId } = controller.state.txHistory[result.id]; expect(approvalTxId).toBe('test-approval-tx-id'); expect(addTransactionFn).toHaveBeenCalledTimes(2); - expect(mockMessengerCall).toHaveBeenCalledTimes(11); + expect(mockMessengerCall).toHaveBeenCalledTimes(12); }); it('should successfully submit an EVM swap transaction with featureId', async () => { mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); + mockMessengerCall.mockReturnValueOnce([]); // isAtomicBatchSupported setupApprovalMocks(); setupBridgeMocks(); @@ -3450,7 +3455,7 @@ describe('BridgeStatusController', () => { FeatureId.PERPS, ); expect(addTransactionFn).toHaveBeenCalledTimes(2); - expect(mockMessengerCall).toHaveBeenCalledTimes(10); + expect(mockMessengerCall).toHaveBeenCalledTimes(11); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); }); @@ -3501,7 +3506,7 @@ describe('BridgeStatusController', () => { expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); expect(addTransactionFn).not.toHaveBeenCalled(); expect(addTransactionBatchFn).toHaveBeenCalledTimes(1); - expect(mockMessengerCall).toHaveBeenCalledTimes(6); + expect(mockMessengerCall).toHaveBeenCalledTimes(7); }); it('should successfully submit an EVM swap transaction with no approval', async () => { @@ -3729,7 +3734,7 @@ describe('BridgeStatusController', () => { expect(estimateGasFeeFn).not.toHaveBeenCalled(); expect(addTransactionFn).not.toHaveBeenCalled(); expect(addTransactionBatchFn).not.toHaveBeenCalled(); - expect(mockMessengerCall).toHaveBeenCalledTimes(4); + expect(mockMessengerCall).toHaveBeenCalledTimes(5); }); it('should throw error if batched tx is not found', async () => { @@ -3768,7 +3773,32 @@ describe('BridgeStatusController', () => { expect(estimateGasFeeFn).toHaveBeenCalledTimes(2); expect(addTransactionFn).not.toHaveBeenCalled(); expect(addTransactionBatchFn).toHaveBeenCalledTimes(1); - expect(mockMessengerCall).toHaveBeenCalledTimes(8); + expect(mockMessengerCall).toHaveBeenCalledTimes(9); + }); + + 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 + expect(addTransactionFn).toHaveBeenCalledTimes(2); + expect(addTransactionBatchFn).not.toHaveBeenCalled(); + expect(result).toBeDefined(); }); }); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 1b444f28f09..189ba21fdb6 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -33,6 +33,7 @@ import { TransactionType, } from '@metamask/transaction-controller'; import type { + IsAtomicBatchSupportedResultEntry, TransactionController, TransactionMeta, TransactionParams, @@ -1352,6 +1353,7 @@ export class BridgeStatusController extends StaticIntervalPollingController; let approvalTxId: string | undefined; + let isDelegatedAccount = false; const startTime = Date.now(); const isBridgeTx = isCrossChain( @@ -1471,7 +1473,33 @@ export class BridgeStatusController extends StaticIntervalPollingController => { + try { + const atomicBatchSupport = await this.messenger.call( + 'TransactionController:isAtomicBatchSupported', + { + address: (quoteResponse.trade as TxData).from as Hex, + chainIds: [hexChainId], + }, + ); + return atomicBatchSupport.some( + (entry: IsAtomicBatchSupportedResultEntry) => + entry.isSupported && entry.delegationAddress, + ); + } catch { + return false; + } + })(); + + if ( + isStxEnabledOnClient || + quoteResponse.quote.gasIncluded7702 || + isDelegatedAccount + ) { const { tradeMeta, approvalMeta } = await this.#handleEvmTransactionBatch({ isBridgeTx, @@ -1483,6 +1511,7 @@ export class BridgeStatusController extends StaticIntervalPollingController | BridgeControllerAction | GetGasFeeState diff --git a/packages/bridge-status-controller/src/utils/gas.test.ts b/packages/bridge-status-controller/src/utils/gas.test.ts index dccfa7fd050..b955d96e768 100644 --- a/packages/bridge-status-controller/src/utils/gas.test.ts +++ b/packages/bridge-status-controller/src/utils/gas.test.ts @@ -120,9 +120,9 @@ describe('gas calculation utils', () => { value: '0x1', }; - it('should return empty object if 7702 is enabled (disable7702 is false)', async () => { + it('should return empty object if gas fields should be skipped (skipGasFields is true)', async () => { const result = await calculateGasFees( - false, + true, null as never, jest.fn(), mockTrade, @@ -134,7 +134,7 @@ describe('gas calculation utils', () => { it('should txFee when provided', async () => { const result = await calculateGasFees( - true, + false, null as never, jest.fn(), mockTrade, @@ -178,7 +178,7 @@ describe('gas calculation utils', () => { }, }); const result = await calculateGasFees( - true, + false, { call: mockCall } as never, mockEstimateGasFeeFn, { ...mockTrade, gasLimit }, diff --git a/packages/bridge-status-controller/src/utils/gas.ts b/packages/bridge-status-controller/src/utils/gas.ts index 3f0e2aa2ab5..d4286205536 100644 --- a/packages/bridge-status-controller/src/utils/gas.ts +++ b/packages/bridge-status-controller/src/utils/gas.ts @@ -63,7 +63,7 @@ export const getTxGasEstimates = ({ }; export const calculateGasFees = async ( - disable7702: boolean, + skipGasFields: boolean, messenger: BridgeStatusControllerMessenger, estimateGasFeeFn: typeof TransactionController.prototype.estimateGasFee, { chainId: _, gasLimit, ...trade }: TxData, @@ -71,7 +71,7 @@ export const calculateGasFees = async ( chainId: Hex, txFee?: { maxFeePerGas: string; maxPriorityFeePerGas: string }, ) => { - if (!disable7702) { + if (skipGasFields) { return {}; } if (txFee) { diff --git a/packages/bridge-status-controller/src/utils/transaction.test.ts b/packages/bridge-status-controller/src/utils/transaction.test.ts index edea3e636db..29a04a49298 100644 --- a/packages/bridge-status-controller/src/utils/transaction.test.ts +++ b/packages/bridge-status-controller/src/utils/transaction.test.ts @@ -1721,7 +1721,7 @@ describe('Bridge Status Controller Transaction Utils', () => { }); describe('toBatchTxParams', () => { - it('should return params without gas if disable7702 is false', () => { + it('should return params without gas if skipGasFields is true', () => { const mockTrade = { chainId: 1, gasLimit: 1231, @@ -1730,7 +1730,7 @@ describe('Bridge Status Controller Transaction Utils', () => { from: '0x1', value: '0x1', }; - const result = toBatchTxParams(false, mockTrade, {}); + const result = toBatchTxParams(true, mockTrade, {}); expect(result).toStrictEqual({ data: '0x1', from: '0x1', @@ -1999,6 +1999,75 @@ describe('Bridge Status Controller Transaction Utils', () => { expect(result.isGasFeeIncluded).toBe(false); expect(result.disable7702).toBe(true); }); + + it('should enable 7702 but include gas fields when isDelegatedAccount is true and gasIncluded7702 is false', async () => { + const mockQuoteResponse = createMockQuoteResponse({ + gasIncluded7702: false, + }); + + const mockEstimateGasFeeFn = jest.fn().mockResolvedValue({ + estimates: { + medium: { + maxFeePerGas: '0xabc', + maxPriorityFeePerGas: '0xdef', + }, + }, + }); + + const result = await getAddTransactionBatchParams({ + quoteResponse: mockQuoteResponse, + messenger: mockMessagingSystem, + 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(); + // Transaction params should include gas fields + expect(result.transactions).toHaveLength(1); + expect(result.transactions[0].params).toHaveProperty('gas'); + expect(result.transactions[0].params).toHaveProperty('maxFeePerGas'); + expect(result.transactions[0].params).toHaveProperty( + 'maxPriorityFeePerGas', + ); + }); + + it('should enable 7702 and omit gas fields when isDelegatedAccount is true and gasIncluded7702 is true', async () => { + const mockQuoteResponse = createMockQuoteResponse({ + gasIncluded7702: true, + }); + + const mockEstimateGasFeeFn = jest.fn().mockResolvedValue({}); + + const result = await getAddTransactionBatchParams({ + quoteResponse: mockQuoteResponse, + messenger: mockMessagingSystem, + isBridgeTx: true, + trade: mockQuoteResponse.trade, + isDelegatedAccount: true, + estimateGasFeeFn: mockEstimateGasFeeFn, + }); + + // 7702 should be enabled + expect(result.disable7702).toBe(false); + // Gas IS sponsored + expect(result.isGasFeeIncluded).toBe(true); + // Gas estimation should NOT have been called (skipped because gas is sponsored) + expect(mockEstimateGasFeeFn).not.toHaveBeenCalled(); + // Transaction params should NOT include gas fields + expect(result.transactions).toHaveLength(1); + expect(result.transactions[0].params).not.toHaveProperty('gas'); + expect(result.transactions[0].params).not.toHaveProperty('maxFeePerGas'); + expect(result.transactions[0].params).not.toHaveProperty( + 'maxPriorityFeePerGas', + ); + }); }); describe('findAndUpdateTransactionsInBatch', () => { @@ -2249,16 +2318,22 @@ describe('Bridge Status Controller Transaction Utils', () => { [TransactionType.bridge]: '0xbridgeData', }; - // Test with bridge transaction (not swap) - findAndUpdateTransactionsInBatch({ + // Test with bridge transaction — should match batch type for 7702 + const result = findAndUpdateTransactionsInBatch({ messenger: mockMessagingSystem, batchId, txDataByType, updateTransactionFn: mockUpdateTransactionFn, }); - // Should not match since it's looking for bridge but finds batch type - expect(mockUpdateTransactionFn).not.toHaveBeenCalled(); + // Should match since 7702 bridge transactions use batch type + expect(mockUpdateTransactionFn).toHaveBeenCalledWith( + expect.objectContaining({ id: 'tx1', type: TransactionType.bridge }), + 'Update tx type to bridge', + ); + expect(result.tradeMeta).toStrictEqual( + expect.objectContaining({ id: 'tx1', type: TransactionType.bridge }), + ); }); }); diff --git a/packages/bridge-status-controller/src/utils/transaction.ts b/packages/bridge-status-controller/src/utils/transaction.ts index 5341222a5f6..930cf3940af 100644 --- a/packages/bridge-status-controller/src/utils/transaction.ts +++ b/packages/bridge-status-controller/src/utils/transaction.ts @@ -300,7 +300,7 @@ export const rekeyHistoryItemInState = ( }; export const toBatchTxParams = ( - disable7702: boolean, + skipGasFields: boolean, { chainId, gasLimit, ...trade }: TxData, { maxFeePerGas, @@ -314,7 +314,7 @@ export const toBatchTxParams = ( to: trade.to as `0x${string}`, value: trade.value as `0x${string}`, }; - if (!disable7702) { + if (skipGasFields) { return params; } @@ -343,6 +343,7 @@ export const getAddTransactionBatchParams = async ({ toTokenAmount, }, requireApproval = false, + isDelegatedAccount = false, estimateGasFeeFn, }: { messenger: BridgeStatusControllerMessenger; @@ -354,6 +355,7 @@ export const getAddTransactionBatchParams = async ({ approval?: TxData; resetApproval?: TxData; requireApproval?: boolean; + isDelegatedAccount?: boolean; }) => { const isGasless = gasIncluded || gasIncluded7702; const selectedAccount = messenger.call( @@ -371,13 +373,16 @@ export const getAddTransactionBatchParams = async ({ hexChainId, ); - // When an active quote has gasIncluded7702 set to true, - // enable 7702 gasless txs for smart accounts - const disable7702 = gasIncluded7702 !== true; + // Gas fields should be omitted only when gas is sponsored via 7702 + const skipGasFields = gasIncluded7702 === true; + // Enable 7702 batching when the quote includes gasless 7702 support, + // or when the account is already delegated (to avoid the in-flight + // transaction limit for delegated accounts) + const disable7702 = !skipGasFields && !isDelegatedAccount; const transactions: TransactionBatchSingleRequest[] = []; if (resetApproval) { const gasFees = await calculateGasFees( - disable7702, + skipGasFields, messenger, estimateGasFeeFn, resetApproval, @@ -389,12 +394,12 @@ export const getAddTransactionBatchParams = async ({ type: isBridgeTx ? TransactionType.bridgeApproval : TransactionType.swapApproval, - params: toBatchTxParams(disable7702, resetApproval, gasFees), + params: toBatchTxParams(skipGasFields, resetApproval, gasFees), }); } if (approval) { const gasFees = await calculateGasFees( - disable7702, + skipGasFields, messenger, estimateGasFeeFn, approval, @@ -406,11 +411,11 @@ export const getAddTransactionBatchParams = async ({ type: isBridgeTx ? TransactionType.bridgeApproval : TransactionType.swapApproval, - params: toBatchTxParams(disable7702, approval, gasFees), + params: toBatchTxParams(skipGasFields, approval, gasFees), }); } const gasFees = await calculateGasFees( - disable7702, + skipGasFields, messenger, estimateGasFeeFn, trade, @@ -420,7 +425,7 @@ export const getAddTransactionBatchParams = async ({ ); transactions.push({ type: isBridgeTx ? TransactionType.bridge : TransactionType.swap, - params: toBatchTxParams(disable7702, trade, gasFees), + params: toBatchTxParams(skipGasFields, trade, gasFees), assetsFiatValues: { sending: sentAmount?.valueInCurrency?.toString(), receiving: toTokenAmount?.valueInCurrency?.toString(), @@ -465,6 +470,11 @@ export const findAndUpdateTransactionsInBatch = ({ // This is a workaround to update the tx type after the tx is signed // TODO: remove this once the tx type for batch txs is preserved in the tx controller Object.entries(txDataByType).forEach(([txType, txData]) => { + // Skip types not present in the batch (e.g. swap entry is undefined for bridge txs) + if (txData === undefined) { + return; + } + // Find transaction by batchId and either matching data or delegation characteristics const txMeta = txs.find((tx) => { if (tx.batchId !== batchId) { @@ -482,14 +492,16 @@ export const findAndUpdateTransactionsInBatch = ({ // For 7702 transactions, we need to match based on transaction type // since the data field might be different (batch execute call) if ( - txType === TransactionType.swap && + (txType === TransactionType.swap || + txType === TransactionType.bridge) && tx.type === TransactionType.batch ) { return true; } // Also check if it's an approval transaction for 7702 if ( - txType === TransactionType.swapApproval && + (txType === TransactionType.swapApproval || + txType === TransactionType.bridgeApproval) && tx.txParams.data === txData ) { return true; diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index c8449895024..a870f989f8a 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Expose `isAtomicBatchSupported` as a messenger action ([#8125](https://github.com/MetaMask/core/pull/8125)) +- Export `TransactionControllerIsAtomicBatchSupportedAction` type ([#8125](https://github.com/MetaMask/core/pull/8125)) - 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)) ### Fixed diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index cc6e47cb5e2..d60bc7ac1c9 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -407,6 +407,14 @@ export type TransactionControllerGetGasFeeTokensAction = { handler: (request: GetGasFeeTokensRequest) => Promise; }; +/** + * Represents the `TransactionController:isAtomicBatchSupported` action. + */ +export type TransactionControllerIsAtomicBatchSupportedAction = { + type: `${typeof controllerName}:isAtomicBatchSupported`; + handler: TransactionController['isAtomicBatchSupported']; +}; + /** * The internal actions available to the TransactionController. */ @@ -417,6 +425,7 @@ export type TransactionControllerActions = | TransactionControllerEstimateGasAction | TransactionControllerEstimateGasBatchAction | TransactionControllerGetGasFeeTokensAction + | TransactionControllerIsAtomicBatchSupportedAction | TransactionControllerGetNonceLockAction | TransactionControllerGetStateAction | TransactionControllerGetTransactionsAction @@ -4657,6 +4666,11 @@ export class TransactionController extends BaseController< `${controllerName}:updateTransaction`, this.updateTransaction.bind(this), ); + + this.messenger.registerActionHandler( + `${controllerName}:isAtomicBatchSupported`, + this.isAtomicBatchSupported.bind(this), + ); } #deleteTransaction(transactionId: string): void { diff --git a/packages/transaction-controller/src/index.ts b/packages/transaction-controller/src/index.ts index fe1dc7ad587..3c5a655d40f 100644 --- a/packages/transaction-controller/src/index.ts +++ b/packages/transaction-controller/src/index.ts @@ -13,6 +13,7 @@ export type { TransactionControllerGetGasFeeTokensAction, TransactionControllerGetNonceLockAction, TransactionControllerGetStateAction, + TransactionControllerIsAtomicBatchSupportedAction, TransactionControllerGetTransactionsAction, TransactionControllerIncomingTransactionsReceivedEvent, TransactionControllerPostTransactionBalanceUpdatedEvent,