diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 05bf83bafc5..ea3aa270a3c 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -13,8 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING:** `BridgeStatusControllerMessenger` must now allow `TransactionController:isAtomicBatchSupported` action ([#8125](https://github.com/MetaMask/core/pull/8125)) - Bump `@metamask/transaction-controller` from `^62.21.0` to `^62.22.0` ([#8217](https://github.com/MetaMask/core/pull/8217)) +### 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.1.0] ### Added 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..1b6525aa611 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", @@ -3247,6 +3346,15 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti "usd_quoted_return": 0, }, ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -3316,6 +3424,15 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an "AccountsController:getAccountByAddress", "0xaccount1", ], + [ + "TransactionController:isAtomicBatchSupported", + { + "address": "0xaccount1", + "chainIds": [ + "0xa4b1", + ], + }, + ], [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -3526,6 +3643,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 323f4dc5d19..8c65af6c1a4 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); expect(controller.state.txHistory[result.id]).toMatchSnapshot(); }); @@ -3835,7 +3840,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 () => { @@ -3874,7 +3879,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 1d1ce898a29..d4dbc2f39dd 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -34,6 +34,7 @@ import { TransactionType, } from '@metamask/transaction-controller'; import type { + IsAtomicBatchSupportedResultEntry, TransactionController, TransactionMeta, TransactionParams, @@ -1358,6 +1359,7 @@ export class BridgeStatusController extends StaticIntervalPollingController; let approvalTxId: string | undefined; + let isDelegatedAccount = false; const startTime = Date.now(); const isBridgeTx = isCrossChain( @@ -1479,7 +1481,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, @@ -1491,6 +1519,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..5d2227e9cb4 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', @@ -1924,6 +1924,25 @@ describe('Bridge Status Controller Transaction Utils', () => { expect(result.transactions[1].type).toBe(TransactionType.swap); }); + it('uses swap approval type for resetApproval when isBridgeTx is false', async () => { + const mockQuoteResponse = createMockQuoteResponse({ + includeResetApproval: true, + }); + + const result = await getAddTransactionBatchParams({ + quoteResponse: mockQuoteResponse, + messenger: mockMessagingSystem, + isBridgeTx: false, + trade: mockQuoteResponse.trade, + resetApproval: mockQuoteResponse.resetApproval, + estimateGasFeeFn: jest.fn().mockResolvedValue({}), + }); + + expect(result.transactions).toHaveLength(2); + expect(result.transactions[0].type).toBe(TransactionType.swapApproval); + expect(result.transactions[1].type).toBe(TransactionType.swap); + }); + it('should handle gasIncluded with gasIncluded7702', async () => { const mockQuoteResponse = createMockQuoteResponse({ gasIncluded: true, @@ -1999,6 +2018,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 +2337,62 @@ 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 }), + ); + }); + + it('should handle 7702 bridgeApproval transactions by matching data', () => { + const txs = [ + createMockTransaction({ + id: 'tx1', + data: '0xapprovalData', + authorizationList: ['0xAuth1'], + type: TransactionType.batch, + }), + ]; + + mockMessagingSystem = createMockMessagingSystemWithTxs( + txs, + ) as unknown as BridgeStatusControllerMessenger; + + const txDataByType = { + [TransactionType.bridgeApproval]: '0xapprovalData', + }; + + const result = findAndUpdateTransactionsInBatch({ + messenger: mockMessagingSystem, + batchId, + txDataByType, + updateTransactionFn: mockUpdateTransactionFn, + }); + + expect(mockUpdateTransactionFn).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'tx1', + type: TransactionType.bridgeApproval, + }), + 'Update tx type to bridgeApproval', + ); + expect(result.approvalMeta).toStrictEqual( + expect.objectContaining({ + id: 'tx1', + type: TransactionType.bridgeApproval, + }), + ); }); }); 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;