From 2ca6a53563f6a8b68a9cbd77219f619db077e4d5 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 7 Apr 2026 14:33:32 +0200 Subject: [PATCH 01/48] Fix hardware wallet MMPay on EIP-7702 chains by gating 7702 paths on account keyring capability --- .../src/KeyringController.test.ts | 46 ++++++++++++++++ .../src/KeyringController.ts | 17 ++++-- .../src/strategy/across/across-submit.test.ts | 36 +++++++++++++ .../src/strategy/across/across-submit.ts | 9 +++- .../src/strategy/relay/relay-quotes.test.ts | 23 ++++++++ .../src/strategy/relay/relay-quotes.ts | 6 +++ .../src/strategy/relay/relay-submit.test.ts | 52 +++++++++++++++++++ .../src/strategy/relay/relay-submit.ts | 16 ++++-- .../src/tests/messenger-mock.ts | 11 ++++ .../transaction-pay-controller/src/types.ts | 6 ++- 10 files changed, 212 insertions(+), 10 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index 0aa455b3775..4d6bc4b785b 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -1037,6 +1037,52 @@ describe('KeyringController', () => { }); }); + describe('accountSupports7702', () => { + it('should return true for HD keyring accounts', async () => { + await withController(async ({ controller, initialState }) => { + const account = initialState.keyrings[0].accounts[0]; + const result = await controller.accountSupports7702(account); + expect(result).toBe(true); + }); + }); + + it('should return true for simple keyring accounts', async () => { + await withController(async ({ controller }) => { + const importedAccountAddress = + await controller.importAccountWithStrategy( + AccountImportStrategy.privateKey, + [privateKey], + ); + + const result = + await controller.accountSupports7702(importedAccountAddress); + expect(result).toBe(true); + }); + }); + + it('should return false for non-HD and non-simple keyring accounts', async () => { + const address = '0x5AC6D462f054690a373FABF8CC28e161003aEB19'; + stubKeyringClassWithAccount(MockKeyring, address); + await withController( + { keyringBuilders: [keyringBuilderFactory(MockKeyring)] }, + async ({ controller }) => { + await controller.addNewKeyring(MockKeyring.type); + + const result = await controller.accountSupports7702(address); + expect(result).toBe(false); + }, + ); + }); + + it('should throw error if no keyring is found for the given account', async () => { + await withController(async ({ controller }) => { + await expect(controller.accountSupports7702('0x')).rejects.toThrow( + KeyringControllerErrorMessage.KeyringNotFound, + ); + }); + }); + }); + describe('getEncryptionPublicKey', () => { describe('when the keyring for the given address supports getEncryptionPublicKey', () => { it('should return the correct encryption public key', async () => { diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index f989e277586..bff1fb9be37 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -2213,15 +2213,24 @@ export class KeyringController< return keyring.type; } - /** - * Constructor helper for registering this controller's messeger - * actions. - */ + async accountSupports7702(account: string): Promise { + const keyringType = await this.getAccountKeyringType(account); + return ( + keyringType === (KeyringTypes.hd as string) || + keyringType === (KeyringTypes.simple as string) + ); + } + #registerMessageHandlers(): void { this.messenger.registerMethodActionHandlers( this, MESSENGER_EXPOSED_METHODS, ); + + this.messenger.registerActionHandler( + `${name}:accountSupports7702`, + this.accountSupports7702.bind(this), + ); } /** diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts index 12b6fabcd98..f992c437b35 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts @@ -101,6 +101,7 @@ describe('Across Submit', () => { const successfulFetchMock = jest.mocked(successfulFetch); const { + accountSupports7702Mock, addTransactionBatchMock, addTransactionMock, estimateGasMock, @@ -126,6 +127,7 @@ describe('Across Submit', () => { }, }); + accountSupports7702Mock.mockResolvedValue(true); estimateGasMock.mockResolvedValue({ gas: '0x5208', simulationFails: undefined, @@ -259,6 +261,40 @@ describe('Across Submit', () => { ); }); + it('disables 7702 batch when account does not support 7702', async () => { + accountSupports7702Mock.mockResolvedValue(false); + + const batchGasQuote = { + ...QUOTE_MOCK, + original: { + ...QUOTE_MOCK.original, + metamask: { + gasLimits: [ + { estimate: 21000, max: 21000 }, + { estimate: 22000, max: 22000 }, + ], + is7702: true, + }, + }, + } as unknown as TransactionPayQuote; + + await submitAcrossQuotes({ + messenger, + quotes: [batchGasQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); + + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + disable7702: true, + disableHook: false, + disableSequential: false, + gasLimit7702: undefined, + }), + ); + }); + it('submits a single transaction when no approvals', async () => { const noApprovalQuote = { ...QUOTE_MOCK, diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts index 78d3cd17a5c..1f43202388d 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts @@ -48,6 +48,7 @@ export async function submitAcrossQuotes( log('Executing quotes', request); const { quotes, messenger, transaction } = request; + let transactionHash: Hex | undefined; for (const quote of quotes) { @@ -117,8 +118,14 @@ async function submitTransactions( messenger: TransactionPayControllerMessenger, ): Promise { const { swapTx } = quote.original.quote; - const { gasLimits: quoteGasLimits, is7702 } = quote.original.metamask; + const { gasLimits: quoteGasLimits, is7702: apiIs7702 } = + quote.original.metamask; const { from } = quote.request; + const accountSupports7702 = await messenger.call( + 'KeyringController:accountSupports7702', + from, + ); + const is7702 = apiIs7702 && accountSupports7702; const chainId = toHex(swapTx.chainId); const orderedTransactions = getAcrossOrderedTransactions({ quote: quote.original.quote, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 4432974b027..8d9c724f63f 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -180,6 +180,7 @@ describe('Relay Quotes Utils', () => { const getSlippageMock = jest.mocked(getSlippage); const { + accountSupports7702Mock, messenger, estimateGasMock, estimateGasBatchMock, @@ -215,6 +216,7 @@ describe('Relay Quotes Utils', () => { ...getDefaultRemoteFeatureFlagControllerState(), }); + accountSupports7702Mock.mockResolvedValue(true); isEIP7702ChainMock.mockReturnValue(true); isRelayExecuteEnabledMock.mockReturnValue(false); getGasBufferMock.mockReturnValue(1.0); @@ -320,6 +322,27 @@ describe('Relay Quotes Utils', () => { expect(body.originGasOverhead).toBeUndefined(); }); + it('omits originGasOverhead when account does not support 7702 even on EIP-7702 chain with relay execute enabled', async () => { + isRelayExecuteEnabledMock.mockReturnValue(true); + accountSupports7702Mock.mockResolvedValue(false); + + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body.originGasOverhead).toBeUndefined(); + }); + it('sends request with EXACT_INPUT trade type when isMaxAmount is true', async () => { successfulFetchMock.mockResolvedValue({ json: async () => QUOTE_MOCK, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index e8f04bb3d9a..01c603fdb15 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -221,7 +221,13 @@ async function getSingleQuote( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const useExactInput = isMaxAmount || request.isPostQuote; + const accountSupports7702 = await messenger.call( + 'KeyringController:accountSupports7702', + from, + ); + const useExecute = + accountSupports7702 && isRelayExecuteEnabled(messenger) && isEIP7702Chain(messenger, sourceChainId); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index 3aeaa9de628..eafccf7e279 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -141,6 +141,7 @@ describe('Relay Submit Utils', () => { const getRelayPollingTimeoutMock = jest.mocked(getRelayPollingTimeout); const { + accountSupports7702Mock, addTransactionMock, addTransactionBatchMock, getDelegationTransactionMock, @@ -157,6 +158,7 @@ describe('Relay Submit Utils', () => { beforeEach(() => { jest.resetAllMocks(); + accountSupports7702Mock.mockResolvedValue(true); getRelayPollingIntervalMock.mockReturnValue(1); getRelayPollingTimeoutMock.mockReturnValue(undefined); @@ -448,6 +450,33 @@ describe('Relay Submit Utils', () => { ); }); + it('does not add authorization list when account does not support 7702', async () => { + accountSupports7702Mock.mockResolvedValue(false); + request.quotes[0].original.details.currencyOut.currency.chainId = 1; + request.quotes[0].original.request = { + authorizationList: [ + { + address: '0xabc' as Hex, + chainId: 1, + nonce: 2, + r: '0xr' as Hex, + s: '0xs' as Hex, + yParity: 1, + }, + ], + } as never; + + await submitRelayQuotes(request); + + expect(addTransactionMock).toHaveBeenCalledTimes(1); + expect(addTransactionMock).toHaveBeenCalledWith( + expect.not.objectContaining({ + authorizationList: expect.anything(), + }), + expect.anything(), + ); + }); + it('adds transaction batch if multiple params', async () => { request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], @@ -1054,6 +1083,29 @@ describe('Relay Submit Utils', () => { ); }); + it('disables 7702 batch when account does not support 7702', async () => { + accountSupports7702Mock.mockResolvedValue(false); + + request.quotes[0].original.steps[0].items.push({ + ...request.quotes[0].original.steps[0].items[0], + }); + + request.quotes[0].original.metamask.gasLimits = [42000]; + request.quotes[0].original.metamask.is7702 = true; + + await submitRelayQuotes(request); + + expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + disable7702: true, + disableHook: false, + disableSequential: false, + gasLimit7702: undefined, + }), + ); + }); + it('adds transaction batch without gasLimit7702 when multiple gas limits', async () => { request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 9694ce83a99..e16b101899d 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -485,6 +485,11 @@ async function submitViaTransactionController( const { from, sourceChainId, sourceTokenAddress } = quote.request; const { isPostQuote } = quote.request; + const accountSupports7702 = await messenger.call( + 'KeyringController:accountSupports7702', + from, + ); + const networkClientId = messenger.call( 'NetworkController:findNetworkClientIdByChainId', sourceChainId, @@ -535,7 +540,9 @@ async function submitViaTransactionController( quote.original.details.currencyOut.currency.chainId; const authorizationList: AuthorizationList | undefined = - isSameChain && quote.original.request.authorizationList?.length + accountSupports7702 && + isSameChain && + quote.original.request.authorizationList?.length ? quote.original.request.authorizationList.map((a) => ({ address: a.address, chainId: toHex(a.chainId), @@ -564,9 +571,10 @@ async function submitViaTransactionController( }, ); } else { - const gasLimit7702 = metamask.is7702 - ? toHex(metamask.gasLimits[0]) - : undefined; + const gasLimit7702 = + accountSupports7702 && metamask.is7702 + ? toHex(metamask.gasLimits[0]) + : undefined; const transactions = allParams.map((singleParams, index) => { const gasLimit = gasLimits[index]; diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index 7ebda275cde..622d5722a68 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -14,6 +14,7 @@ import type { import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { NetworkControllerGetNetworkClientByIdAction } from '@metamask/network-controller'; import type { NetworkControllerFindNetworkClientIdByChainIdAction } from '@metamask/network-controller'; +import type { KeyringControllerAccountSupports7702Action } from '@metamask/keyring-controller'; import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; import type { TransactionControllerAddTransactionAction, @@ -129,6 +130,10 @@ export function getMessengerMock({ TransactionControllerEstimateGasBatchAction['handler'] > = jest.fn(); + const accountSupports7702Mock: jest.MockedFn< + KeyringControllerAccountSupports7702Action['handler'] + > = jest.fn(); + const getAssetsControllerStateMock = jest.fn(); const messenger: RootMessenger = new Messenger({ @@ -250,11 +255,17 @@ export function getMessengerMock({ 'AssetsController:getStateForTransactionPay', getAssetsControllerStateMock, ); + + messenger.registerActionHandler( + 'KeyringController:accountSupports7702', + accountSupports7702Mock, + ); } const publish = messenger.publish.bind(messenger); return { + accountSupports7702Mock, addTransactionMock, getAssetsControllerStateMock, addTransactionBatchMock, diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 175668f3827..f2052a4fae2 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -12,7 +12,10 @@ import type { BridgeControllerActions } from '@metamask/bridge-controller'; import type { BridgeStatusControllerStateChangeEvent } from '@metamask/bridge-status-controller'; import type { BridgeStatusControllerActions } from '@metamask/bridge-status-controller'; import type { GasFeeControllerActions } from '@metamask/gas-fee-controller'; -import type { KeyringControllerSignTypedMessageAction } from '@metamask/keyring-controller'; +import type { + KeyringControllerAccountSupports7702Action, + KeyringControllerSignTypedMessageAction, +} from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; import type { NetworkControllerFindNetworkClientIdByChainIdAction } from '@metamask/network-controller'; import type { NetworkControllerGetNetworkClientByIdAction } from '@metamask/network-controller'; @@ -47,6 +50,7 @@ export type AllowedActions = | BridgeStatusControllerActions | CurrencyRateControllerActions | GasFeeControllerActions + | KeyringControllerAccountSupports7702Action | KeyringControllerSignTypedMessageAction | NetworkControllerFindNetworkClientIdByChainIdAction | NetworkControllerGetNetworkClientByIdAction From b34b0bcade0143e27c1526609b958fce167f553a Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 7 Apr 2026 14:37:51 +0200 Subject: [PATCH 02/48] Add changelog --- packages/transaction-pay-controller/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index cab7f66d35f..73994aed028 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -93,6 +93,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix perps withdraw to Arbitrum USDC showing inflated transaction fee by bypassing same-token filter when `isHyperliquidSource` is set ([#8387](https://github.com/MetaMask/core/pull/8387)) +- Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on `KeyringController:accountSupports7702` ([#8388](https://github.com/MetaMask/core/pull/8388)) ## [19.0.3] From 4da8f2581de12ce2792a22a7716de38ffff981bf Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 7 Apr 2026 19:41:45 +0200 Subject: [PATCH 03/48] Update --- .../src/KeyringController.test.ts | 5 ++- .../src/strategy/across/across-quotes.test.ts | 23 ++++++----- .../src/strategy/across/across-quotes.ts | 40 ++++++++++++++++++- .../src/strategy/across/across-submit.test.ts | 6 +-- .../src/tests/messenger-mock.ts | 2 +- 5 files changed, 58 insertions(+), 18 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index 4d6bc4b785b..87e575c842a 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -1054,8 +1054,9 @@ describe('KeyringController', () => { [privateKey], ); - const result = - await controller.accountSupports7702(importedAccountAddress); + const result = await controller.accountSupports7702( + importedAccountAddress, + ); expect(result).toBe(true); }); }); diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts index 0af7ac0fdfe..8b91864deab 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts @@ -164,6 +164,7 @@ describe('Across Quotes', () => { const calculateGasCostMock = jest.mocked(calculateGasCost); const { + accountSupports7702Mock, messenger, estimateGasMock, estimateGasBatchMock, @@ -203,6 +204,7 @@ describe('Across Quotes', () => { getGasBufferMock.mockReturnValue(1.0); getSlippageMock.mockReturnValue(0.005); + accountSupports7702Mock.mockResolvedValue(true); findNetworkClientIdByChainIdMock.mockReturnValue('mainnet'); estimateGasMock.mockResolvedValue({ gas: '0x5208', @@ -1255,12 +1257,19 @@ describe('Across Quotes', () => { ); }); - it('omits the authorization-list flag when a combined batch does not require one', async () => { + it('re-estimates individually when batch returns 7702 but account does not support it', async () => { + accountSupports7702Mock.mockResolvedValue(false); + estimateGasBatchMock.mockResolvedValue({ totalGasLimit: 51000, gasLimits: [51000], }); + estimateGasMock.mockResolvedValue({ + gas: '0x5208', + simulationFails: undefined, + }); + successfulFetchMock.mockResolvedValue({ json: async () => ({ ...QUOTE_MOCK, @@ -1281,15 +1290,9 @@ describe('Across Quotes', () => { transaction: TRANSACTION_META_MOCK, }); - expect(result[0].original.metamask).toStrictEqual({ - gasLimits: [ - { - estimate: 51000, - max: 51000, - }, - ], - is7702: true, - }); + expect(result[0].original.metamask.is7702).toBe(false); + expect(result[0].original.metamask.gasLimits).toHaveLength(2); + expect(estimateGasMock).toHaveBeenCalledTimes(2); }); it('throws when the shared gas estimator marks a quote as 7702 without a combined gas limit', async () => { diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts index 4f4f7778658..11edb0da171 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -396,7 +396,7 @@ async function calculateSourceNetworkCost( const orderedTransactions = getAcrossOrderedTransactions({ quote }); const { swapTx } = quote; const swapChainId = toHex(swapTx.chainId); - const gasEstimates = await estimateQuoteGasLimits({ + let gasEstimates = await estimateQuoteGasLimits({ fallbackGas: acrossFallbackGas, messenger, transactions: orderedTransactions.map((transaction) => ({ @@ -408,7 +408,43 @@ async function calculateSourceNetworkCost( value: transaction.value ?? '0x0', })), }); - const { batchGasLimit, is7702, requiresAuthorizationList } = gasEstimates; + const { batchGasLimit } = gasEstimates; + + const accountSupports7702 = await messenger.call( + 'KeyringController:accountSupports7702', + from, + ); + + // If the chain returned a combined 7702 gas limit but the account cannot sign + // EIP-7702 authorizations (e.g. hardware wallet), re-estimate each transaction + // individually so the submit path receives per-transaction gas limits. + if (gasEstimates.is7702 && !accountSupports7702) { + const individualResults = await Promise.all( + orderedTransactions.map((transaction) => + estimateQuoteGasLimits({ + fallbackGas: acrossFallbackGas, + messenger, + transactions: [ + { + chainId: toHex(transaction.chainId), + data: transaction.data, + from, + gas: transaction.gas, + to: transaction.to, + value: transaction.value ?? '0x0', + }, + ], + }), + ), + ); + gasEstimates = { + ...gasEstimates, + is7702: false, + gasLimits: individualResults.map((result) => result.gasLimits[0]), + }; + } + + const { is7702 } = gasEstimates; if (is7702) { if (!batchGasLimit) { diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts index f992c437b35..baf6c36572e 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts @@ -264,7 +264,7 @@ describe('Across Submit', () => { it('disables 7702 batch when account does not support 7702', async () => { accountSupports7702Mock.mockResolvedValue(false); - const batchGasQuote = { + const nonIs7702Quote = { ...QUOTE_MOCK, original: { ...QUOTE_MOCK.original, @@ -273,14 +273,14 @@ describe('Across Submit', () => { { estimate: 21000, max: 21000 }, { estimate: 22000, max: 22000 }, ], - is7702: true, + is7702: false, }, }, } as unknown as TransactionPayQuote; await submitAcrossQuotes({ messenger, - quotes: [batchGasQuote], + quotes: [nonIs7702Quote], transaction: TRANSACTION_META_MOCK, isSmartTransaction: jest.fn(), }); diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index 622d5722a68..a7c3768fc10 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -6,6 +6,7 @@ import type { BridgeStatusControllerGetStateAction, BridgeStatusControllerSubmitTxAction, } from '@metamask/bridge-status-controller'; +import type { KeyringControllerAccountSupports7702Action } from '@metamask/keyring-controller'; import type { MessengerActions, MessengerEvents, @@ -14,7 +15,6 @@ import type { import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { NetworkControllerGetNetworkClientByIdAction } from '@metamask/network-controller'; import type { NetworkControllerFindNetworkClientIdByChainIdAction } from '@metamask/network-controller'; -import type { KeyringControllerAccountSupports7702Action } from '@metamask/keyring-controller'; import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; import type { TransactionControllerAddTransactionAction, From fe481e65a39e59b0ffc96b16f0cacf2316bd7bef Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 10:25:35 +0200 Subject: [PATCH 04/48] Update --- .../src/strategy/across/across-submit.test.ts | 12 +++------- .../src/strategy/across/across-submit.ts | 24 ++++++++++--------- .../src/strategy/relay/relay-submit.test.ts | 15 ++++-------- .../src/strategy/relay/relay-submit.ts | 20 ++++++++-------- 4 files changed, 30 insertions(+), 41 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts index baf6c36572e..6d8d7d7ab1f 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts @@ -261,7 +261,7 @@ describe('Across Submit', () => { ); }); - it('disables 7702 batch when account does not support 7702', async () => { + it('submits individually when account does not support 7702', async () => { accountSupports7702Mock.mockResolvedValue(false); const nonIs7702Quote = { @@ -285,14 +285,8 @@ describe('Across Submit', () => { isSmartTransaction: jest.fn(), }); - expect(addTransactionBatchMock).toHaveBeenCalledWith( - expect.objectContaining({ - disable7702: true, - disableHook: false, - disableSequential: false, - gasLimit7702: undefined, - }), - ); + expect(addTransactionBatchMock).not.toHaveBeenCalled(); + expect(addTransactionMock).toHaveBeenCalledTimes(2); }); it('submits a single transaction when no approvals', async () => { diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts index 1f43202388d..7bbfe95f071 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts @@ -203,17 +203,19 @@ async function submitTransactions( let result: { result: Promise } | undefined; try { - if (transactions.length === 1) { - result = await messenger.call( - 'TransactionController:addTransaction', - transactions[0].params, - { - networkClientId, - origin: ORIGIN_METAMASK, - requireApproval: false, - type: transactions[0].type, - }, - ); + if (transactions.length === 1 || !accountSupports7702) { + for (const { params, type } of transactions) { + result = await messenger.call( + 'TransactionController:addTransaction', + params, + { + networkClientId, + origin: ORIGIN_METAMASK, + requireApproval: false, + type, + }, + ); + } } else { const batchTransactions = transactions.map(({ params, type }) => ({ params: toBatchTransactionParams(params), diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index eafccf7e279..b5cc27d0618 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -1083,27 +1083,20 @@ describe('Relay Submit Utils', () => { ); }); - it('disables 7702 batch when account does not support 7702', async () => { + it('submits individually when account does not support 7702', async () => { accountSupports7702Mock.mockResolvedValue(false); request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], }); - request.quotes[0].original.metamask.gasLimits = [42000]; + request.quotes[0].original.metamask.gasLimits = [21000, 22000]; request.quotes[0].original.metamask.is7702 = true; await submitRelayQuotes(request); - expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); - expect(addTransactionBatchMock).toHaveBeenCalledWith( - expect.objectContaining({ - disable7702: true, - disableHook: false, - disableSequential: false, - gasLimit7702: undefined, - }), - ); + expect(addTransactionBatchMock).not.toHaveBeenCalled(); + expect(addTransactionMock).toHaveBeenCalledTimes(2); }); it('adds transaction batch without gasLimit7702 when multiple gas limits', async () => { diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index e16b101899d..01fde7cb3c0 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -552,12 +552,13 @@ async function submitViaTransactionController( const { metamask } = quote.original; const { gasLimits } = metamask; - if (allParams.length === 1) { - const transactionParams = { - ...allParams[0], - authorizationList, - gas: toHex(gasLimits[0]), - }; + if (allParams.length === 1 || !accountSupports7702) { + for (let i = 0; i < allParams.length; i++) { + const transactionParams = { + ...allParams[i], + ...(i === 0 ? { authorizationList } : {}), + gas: toHex(gasLimits[i] ?? gasLimits[0]), + }; result = await messenger.call( 'TransactionController:addTransaction', @@ -571,10 +572,9 @@ async function submitViaTransactionController( }, ); } else { - const gasLimit7702 = - accountSupports7702 && metamask.is7702 - ? toHex(metamask.gasLimits[0]) - : undefined; + const gasLimit7702 = metamask.is7702 + ? toHex(metamask.gasLimits[0]) + : undefined; const transactions = allParams.map((singleParams, index) => { const gasLimit = gasLimits[index]; From 6b4bd9778f7099d879e21d360b8c1437e46454dd Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 12:50:08 +0200 Subject: [PATCH 05/48] Update --- packages/transaction-pay-controller/src/utils/quotes.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index ec0b7bf927a..6e355ac69d0 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -128,8 +128,13 @@ export async function updateQuotes( log('Calculated totals', { transactionId, totals }); + const accountSupports7702 = await messenger.call( + 'KeyringController:accountSupports7702', + from, + ); + syncTransaction({ - batchTransactions, + batchTransactions: accountSupports7702 ? batchTransactions : [], isPostQuote, messenger: messenger as never, paymentToken, From 8c4c354a24ea57533f4890160765e80a6c646dbb Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 13:10:01 +0200 Subject: [PATCH 06/48] Update --- .../src/strategy/relay/relay-submit.test.ts | 22 +++++++++++++++++++ .../src/utils/quotes.test.ts | 20 ++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index b5cc27d0618..73be889959b 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -1099,6 +1099,28 @@ describe('Relay Submit Utils', () => { expect(addTransactionMock).toHaveBeenCalledTimes(2); }); + it('falls back to first gas limit when entry is missing for individual submission', async () => { + accountSupports7702Mock.mockResolvedValue(false); + + request.quotes[0].original.steps[0].items.push({ + ...request.quotes[0].original.steps[0].items[0], + }); + + request.quotes[0].original.metamask.gasLimits = [21000]; + + await submitRelayQuotes(request); + + expect(addTransactionBatchMock).not.toHaveBeenCalled(); + expect(addTransactionMock).toHaveBeenCalledTimes(2); + expect(addTransactionMock).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + gas: '0x5208', + }), + expect.anything(), + ); + }); + it('adds transaction batch without gasLimit7702 when multiple gas limits', async () => { request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 2d2030acff4..2683b7115bd 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -101,7 +101,8 @@ const BATCH_TRANSACTION_MOCK = { } as BatchTransaction; describe('Quotes Utils', () => { - const { messenger, getControllerStateMock } = getMessengerMock(); + const { messenger, getControllerStateMock, accountSupports7702Mock } = + getMessengerMock(); const updateTransactionDataMock = jest.fn(); const getStrategyByNameMock = jest.mocked(getStrategyByName); const getStrategiesByNameMock = jest.mocked(getStrategiesByName); @@ -177,6 +178,7 @@ describe('Quotes Utils', () => { getQuotesMock.mockResolvedValue([QUOTE_MOCK]); getBatchTransactionsMock.mockResolvedValue([BATCH_TRANSACTION_MOCK]); calculateTotalsMock.mockReturnValue(TOTALS_MOCK); + accountSupports7702Mock.mockResolvedValue(true); getLiveTokenBalanceMock.mockResolvedValue('5000000'); getTokenFiatRateMock.mockReturnValue({ @@ -660,6 +662,22 @@ describe('Quotes Utils', () => { ); }); + it('clears batch transactions when account does not support 7702', async () => { + accountSupports7702Mock.mockResolvedValue(false); + + await run(); + + const transactionMetaMock = {} as TransactionMeta; + updateTransactionMock.mock.calls[0][1](transactionMetaMock); + + expect(transactionMetaMock).toMatchObject( + expect.objectContaining({ + batchTransactions: [], + batchTransactionsOptions: {}, + }), + ); + }); + it('updates metrics in metadata', async () => { await run(); From 5d68c620ea8f913dd37bedb6875f9a8709a74b9b Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 13:25:14 +0200 Subject: [PATCH 07/48] Update --- packages/keyring-controller/src/KeyringController.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index bff1fb9be37..934c40addf4 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -77,6 +77,7 @@ const MESSENGER_EXPOSED_METHODS = [ 'createNewVaultAndRestore', 'removeAccount', 'isUnlocked', + 'accountSupports7702', ] as const; /** @@ -2226,11 +2227,6 @@ export class KeyringController< this, MESSENGER_EXPOSED_METHODS, ); - - this.messenger.registerActionHandler( - `${name}:accountSupports7702`, - this.accountSupports7702.bind(this), - ); } /** From ffb4f0af5b7e91d7ba207c0a742f8affd18a5e5c Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 13:32:40 +0200 Subject: [PATCH 08/48] Update --- .../KeyringController-method-action-types.ts | 53 +------------------ 1 file changed, 2 insertions(+), 51 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController-method-action-types.ts b/packages/keyring-controller/src/KeyringController-method-action-types.ts index e5799d22cb0..9ba9b0378b5 100644 --- a/packages/keyring-controller/src/KeyringController-method-action-types.ts +++ b/packages/keyring-controller/src/KeyringController-method-action-types.ts @@ -321,25 +321,7 @@ export type KeyringControllerWithKeyringUnsafeAction = { }; /** - * Select a keyring using its `KeyringV2` adapter, and execute - * the given operation with the wrapped keyring as a mutually - * exclusive atomic operation. - * - * The cached `KeyringV2` adapter is retrieved from the keyring - * entry. - * - * A `KeyringV2Builder` for the selected keyring's type must exist - * (either as a default or registered via the `keyringV2Builders` - * constructor option); otherwise an error is thrown. - * - * The method automatically persists changes at the end of the - * function execution, or rolls back the changes if an error - * is thrown. - * - * @param selector - Keyring selector object. - * @param operation - Function to execute with the wrapped V2 keyring. - * @returns Promise resolving to the result of the function execution. - * @template CallbackResult - The type of the value resolved by the callback function. + * {@inheritDoc KeyringController.withKeyringV2} */ export type KeyringControllerWithKeyringV2Action = { type: `KeyringController:withKeyringV2`; @@ -347,38 +329,7 @@ export type KeyringControllerWithKeyringV2Action = { }; /** - * Select a keyring, wrap it in a `KeyringV2` adapter, and execute - * the given read-only operation **without** acquiring the controller's - * mutual exclusion lock. - * - * ## When to use this method - * - * This method is an escape hatch for read-only access to keyring data that - * is immutable once the keyring is initialized. A typical safe use case is - * reading immutable fields from a `KeyringV2` adapter: data that is set - * during initialization and never mutated afterwards. - * - * ## Why it is "unsafe" - * - * The "unsafe" designation mirrors the semantics of `unsafe { }` blocks in - * Rust: the method itself does not enforce thread-safety guarantees. By - * calling this method the **caller** explicitly takes responsibility for - * ensuring that: - * - * - The operation is **read-only** — no state is mutated. - * - The data being read is **immutable** after the keyring is initialized, - * so concurrent locked operations cannot alter it while this callback - * runs. - * - * Do **not** use this method to: - * - Mutate keyring state (add accounts, sign, etc.) — use `withKeyringV2`. - * - Read mutable fields that could change during concurrent operations. - * - * @param selector - Keyring selector object. - * @param operation - Read-only function to execute with the wrapped V2 keyring. - * @returns Promise resolving to the result of the function execution. - * @template SelectedKeyring - The type of the selected V2 keyring. - * @template CallbackResult - The type of the value resolved by the callback function. + * {@inheritDoc KeyringController.withKeyringV2Unsafe} */ export type KeyringControllerWithKeyringV2UnsafeAction = { type: `KeyringController:withKeyringV2Unsafe`; From 0d55ee4634c5c2b8a1b90123f1e9de2b7571abac Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 13:36:52 +0200 Subject: [PATCH 09/48] Update --- packages/keyring-controller/src/index.ts | 1 + .../transaction-controller/src/TransactionController.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/keyring-controller/src/index.ts b/packages/keyring-controller/src/index.ts index 7da62e7fbdd..7e723ca3485 100644 --- a/packages/keyring-controller/src/index.ts +++ b/packages/keyring-controller/src/index.ts @@ -13,6 +13,7 @@ export type { KeyringControllerPersistAllKeyringsAction, KeyringControllerRemoveAccountAction, KeyringControllerSignMessageAction, + KeyringControllerAccountSupports7702Action, KeyringControllerSignEip7702AuthorizationAction, KeyringControllerSignPersonalMessageAction, KeyringControllerSignTransactionAction, diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index ac6e9561d99..f10e3ab938c 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -31,7 +31,10 @@ import type { FetchGasFeeEstimateOptions, GasFeeState, } from '@metamask/gas-fee-controller'; -import type { KeyringControllerSignEip7702AuthorizationAction } from '@metamask/keyring-controller'; +import type { + KeyringControllerAccountSupports7702Action, + KeyringControllerSignEip7702AuthorizationAction, +} from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; import type { BlockTracker, @@ -492,6 +495,7 @@ export type AllowedActions = | AccountsControllerGetSelectedAccountAction | AccountsControllerGetStateAction | ApprovalControllerAddRequestAction + | KeyringControllerAccountSupports7702Action | KeyringControllerSignEip7702AuthorizationAction | NetworkControllerFindNetworkClientIdByChainIdAction | NetworkControllerGetNetworkClientByIdAction From 5ea24d8e0055db3093dfad5923a24e21a4f59560 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 14:05:19 +0200 Subject: [PATCH 10/48] Update changelog --- packages/transaction-pay-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 73994aed028..45303607c6a 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -80,6 +80,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Set `submittedTime` at the start of `TransactionPayPublishHook` before strategy execution for accurate `mm_pay_time_to_complete_s` metrics in intent-based flows ([#8439](https://github.com/MetaMask/core/pull/8439)) +### Fixed + +- Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on `KeyringController:accountSupports7702` ([#8388](https://github.com/MetaMask/core/pull/8388)) + ## [19.1.0] ### Added From 19ef78296b76f505375b9b9f084f06121c41d399 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Thu, 9 Apr 2026 13:20:56 +0200 Subject: [PATCH 11/48] Update --- .../src/strategy/relay/relay-submit.test.ts | 64 +++++++++++++++++++ .../src/strategy/relay/relay-submit.ts | 10 +++ 2 files changed, 74 insertions(+) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index 73be889959b..495999f5c04 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -1121,6 +1121,70 @@ describe('Relay Submit Utils', () => { ); }); + it('skips on-chain confirmation wait for non-7702 accounts with multiple transactions', async () => { + accountSupports7702Mock.mockResolvedValue(false); + + request.quotes[0].original.steps[0].items.push({ + ...request.quotes[0].original.steps[0].items[0], + }); + + request.quotes[0].original.metamask.gasLimits = [21000, 22000]; + + await submitRelayQuotes(request); + + expect(addTransactionMock).toHaveBeenCalledTimes(2); + expect(waitForTransactionConfirmedMock).not.toHaveBeenCalled(); + }); + + it('awaits all transaction results before returning for non-7702 accounts', async () => { + accountSupports7702Mock.mockResolvedValue(false); + + request.quotes[0].original.steps[0].items.push({ + ...request.quotes[0].original.steps[0].items[0], + }); + + request.quotes[0].original.metamask.gasLimits = [21000, 22000]; + + const resultPromise1 = Promise.resolve('0xhash1'); + const resultPromise2 = Promise.resolve('0xhash2'); + + addTransactionMock + .mockResolvedValueOnce({ + result: resultPromise1, + transactionMeta: TRANSACTION_META_MOCK, + }) + .mockResolvedValueOnce({ + result: resultPromise2, + transactionMeta: TRANSACTION_META_MOCK, + }); + + await submitRelayQuotes(request); + + expect(await resultPromise1).toBe('0xhash1'); + expect(await resultPromise2).toBe('0xhash2'); + }); + + it('still waits for on-chain confirmation for 7702 accounts with multiple transactions', async () => { + accountSupports7702Mock.mockResolvedValue(true); + + request.quotes[0].original.steps[0].items.push({ + ...request.quotes[0].original.steps[0].items[0], + }); + + await submitRelayQuotes(request); + + expect(waitForTransactionConfirmedMock).toHaveBeenCalled(); + }); + + it('still waits for on-chain confirmation for non-7702 accounts with single transaction', async () => { + accountSupports7702Mock.mockResolvedValue(false); + + await submitRelayQuotes(request); + + expect(addTransactionMock).toHaveBeenCalledTimes(1); + expect(waitForTransactionConfirmedMock).toHaveBeenCalled(); + }); + it('adds transaction batch without gasLimit7702 when multiple gas limits', async () => { request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 01fde7cb3c0..bf4426e891f 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -552,6 +552,8 @@ async function submitViaTransactionController( const { metamask } = quote.original; const { gasLimits } = metamask; + const results: { result: Promise }[] = []; + if (allParams.length === 1 || !accountSupports7702) { for (let i = 0; i < allParams.length; i++) { const transactionParams = { @@ -618,6 +620,14 @@ async function submitViaTransactionController( log('Added transactions', transactionIds); + if (!accountSupports7702 && allParams.length > 1) { + log( + 'Hardware wallet transactions signed and broadcast, skipping on-chain confirmation wait', + ); + await Promise.all(results.map((res) => res.result)); + return undefined as unknown as Hex; + } + if (result) { const txHash = await result.result; log('Submitted transaction', txHash); From a338a59270f3bf58321575b4738c789c9d74342c Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 12:31:53 +0200 Subject: [PATCH 12/48] Address comments --- .../src/KeyringController.test.ts | 47 - .../src/KeyringController.ts | 12 +- packages/keyring-controller/src/index.ts | 1 - .../src/TransactionController.ts | 6 +- .../transaction-pay-controller/CHANGELOG.md | 3 +- .../src/TransactionPayController.test.ts | 6 + .../src/TransactionPayController.ts | 7 + .../src/helpers/QuoteRefresher.test.ts | 7 + .../src/helpers/QuoteRefresher.ts | 7 + .../helpers/TransactionPayPublishHook.test.ts | 3 + .../src/helpers/TransactionPayPublishHook.ts | 9 + .../transaction-pay-controller/src/index.ts | 1 + .../src/strategy/across/across-quotes.test.ts | 749 ++++----- .../src/strategy/across/across-quotes.ts | 46 +- .../src/strategy/across/across-submit.test.ts | 326 ++-- .../src/strategy/across/across-submit.ts | 42 +- .../strategy/bridge/BridgeStrategy.test.ts | 2 + .../src/strategy/bridge/bridge-quotes.test.ts | 1 + .../src/strategy/fiat/fiat-quotes.ts | 4 +- .../src/strategy/relay/relay-quotes.test.ts | 1464 +++++++---------- .../src/strategy/relay/relay-quotes.ts | 7 +- .../src/strategy/relay/relay-submit.test.ts | 104 +- .../src/strategy/relay/relay-submit.ts | 44 +- .../src/strategy/test/TestStrategy.test.ts | 2 + .../src/tests/messenger-mock.ts | 74 +- .../transaction-pay-controller/src/types.ts | 20 +- .../src/utils/quote-gas.ts | 39 +- .../src/utils/quotes.test.ts | 15 +- .../src/utils/quotes.ts | 25 +- 29 files changed, 1388 insertions(+), 1685 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index 87e575c842a..0aa455b3775 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -1037,53 +1037,6 @@ describe('KeyringController', () => { }); }); - describe('accountSupports7702', () => { - it('should return true for HD keyring accounts', async () => { - await withController(async ({ controller, initialState }) => { - const account = initialState.keyrings[0].accounts[0]; - const result = await controller.accountSupports7702(account); - expect(result).toBe(true); - }); - }); - - it('should return true for simple keyring accounts', async () => { - await withController(async ({ controller }) => { - const importedAccountAddress = - await controller.importAccountWithStrategy( - AccountImportStrategy.privateKey, - [privateKey], - ); - - const result = await controller.accountSupports7702( - importedAccountAddress, - ); - expect(result).toBe(true); - }); - }); - - it('should return false for non-HD and non-simple keyring accounts', async () => { - const address = '0x5AC6D462f054690a373FABF8CC28e161003aEB19'; - stubKeyringClassWithAccount(MockKeyring, address); - await withController( - { keyringBuilders: [keyringBuilderFactory(MockKeyring)] }, - async ({ controller }) => { - await controller.addNewKeyring(MockKeyring.type); - - const result = await controller.accountSupports7702(address); - expect(result).toBe(false); - }, - ); - }); - - it('should throw error if no keyring is found for the given account', async () => { - await withController(async ({ controller }) => { - await expect(controller.accountSupports7702('0x')).rejects.toThrow( - KeyringControllerErrorMessage.KeyringNotFound, - ); - }); - }); - }); - describe('getEncryptionPublicKey', () => { describe('when the keyring for the given address supports getEncryptionPublicKey', () => { it('should return the correct encryption public key', async () => { diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 934c40addf4..65dfbd1aa14 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -2214,14 +2214,10 @@ export class KeyringController< return keyring.type; } - async accountSupports7702(account: string): Promise { - const keyringType = await this.getAccountKeyringType(account); - return ( - keyringType === (KeyringTypes.hd as string) || - keyringType === (KeyringTypes.simple as string) - ); - } - + /** + * Constructor helper for registering this controller's messenger + * actions. + */ #registerMessageHandlers(): void { this.messenger.registerMethodActionHandlers( this, diff --git a/packages/keyring-controller/src/index.ts b/packages/keyring-controller/src/index.ts index 7e723ca3485..7da62e7fbdd 100644 --- a/packages/keyring-controller/src/index.ts +++ b/packages/keyring-controller/src/index.ts @@ -13,7 +13,6 @@ export type { KeyringControllerPersistAllKeyringsAction, KeyringControllerRemoveAccountAction, KeyringControllerSignMessageAction, - KeyringControllerAccountSupports7702Action, KeyringControllerSignEip7702AuthorizationAction, KeyringControllerSignPersonalMessageAction, KeyringControllerSignTransactionAction, diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index f10e3ab938c..ac6e9561d99 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -31,10 +31,7 @@ import type { FetchGasFeeEstimateOptions, GasFeeState, } from '@metamask/gas-fee-controller'; -import type { - KeyringControllerAccountSupports7702Action, - KeyringControllerSignEip7702AuthorizationAction, -} from '@metamask/keyring-controller'; +import type { KeyringControllerSignEip7702AuthorizationAction } from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; import type { BlockTracker, @@ -495,7 +492,6 @@ export type AllowedActions = | AccountsControllerGetSelectedAccountAction | AccountsControllerGetStateAction | ApprovalControllerAddRequestAction - | KeyringControllerAccountSupports7702Action | KeyringControllerSignEip7702AuthorizationAction | NetworkControllerFindNetworkClientIdByChainIdAction | NetworkControllerGetNetworkClientByIdAction diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 45303607c6a..077cbd41f13 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -82,7 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on `KeyringController:accountSupports7702` ([#8388](https://github.com/MetaMask/core/pull/8388)) +- Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on a new `accountSupports7702` callback option ([#8388](https://github.com/MetaMask/core/pull/8388)) ## [19.1.0] @@ -97,7 +97,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix perps withdraw to Arbitrum USDC showing inflated transaction fee by bypassing same-token filter when `isHyperliquidSource` is set ([#8387](https://github.com/MetaMask/core/pull/8387)) -- Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on `KeyringController:accountSupports7702` ([#8388](https://github.com/MetaMask/core/pull/8388)) ## [19.0.3] diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 1fd9d21d1bd..9151367bdb5 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -45,6 +45,7 @@ describe('TransactionPayController', () => { */ function createController(): TransactionPayController { return new TransactionPayController({ + accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: jest.fn(), messenger, }); @@ -354,6 +355,7 @@ describe('TransactionPayController', () => { .mockResolvedValue(resultMock); new TransactionPayController({ + accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: getDelegationTransactionMock, messenger, }); @@ -384,6 +386,7 @@ describe('TransactionPayController', () => { it('returns callback value if provided', async () => { new TransactionPayController({ + accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: jest.fn(), getStrategy: (): TransactionPayStrategy => TransactionPayStrategy.Test, messenger, @@ -399,6 +402,7 @@ describe('TransactionPayController', () => { it('does not query feature flag strategy order when getStrategies callback returns values', async () => { new TransactionPayController({ + accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: jest.fn(), getStrategies: (): TransactionPayStrategy[] => [ TransactionPayStrategy.Test, @@ -422,6 +426,7 @@ describe('TransactionPayController', () => { getStrategyOrderMock.mockReturnValue([TransactionPayStrategy.Test]); new TransactionPayController({ + accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: jest.fn(), getStrategies: (): TransactionPayStrategy[] => [], messenger, @@ -439,6 +444,7 @@ describe('TransactionPayController', () => { getStrategyOrderMock.mockReturnValue([TransactionPayStrategy.Bridge]); new TransactionPayController({ + accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: jest.fn(), getStrategies: (): TransactionPayStrategy[] => [undefined] as unknown as TransactionPayStrategy[], diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index d52a50466c8..594dade958b 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -13,6 +13,7 @@ import { } from './constants'; import { QuoteRefresher } from './helpers/QuoteRefresher'; import type { + AccountSupports7702Callback, GetDelegationTransactionCallback, TransactionConfigCallback, TransactionData, @@ -53,6 +54,8 @@ export class TransactionPayController extends BaseController< TransactionPayControllerState, TransactionPayControllerMessenger > { + readonly #accountSupports7702: AccountSupports7702Callback; + readonly #getDelegationTransaction: GetDelegationTransactionCallback; readonly #getStrategy?: ( @@ -64,6 +67,7 @@ export class TransactionPayController extends BaseController< ) => TransactionPayStrategy[]; constructor({ + accountSupports7702, getDelegationTransaction, getStrategy, getStrategies, @@ -77,6 +81,7 @@ export class TransactionPayController extends BaseController< state: { ...getDefaultState(), ...state }, }); + this.#accountSupports7702 = accountSupports7702; this.#getDelegationTransaction = getDelegationTransaction; this.#getStrategy = getStrategy; this.#getStrategies = getStrategies; @@ -94,6 +99,7 @@ export class TransactionPayController extends BaseController< // eslint-disable-next-line no-new new QuoteRefresher({ + accountSupports7702: this.#accountSupports7702, getStrategies: this.#getStrategiesWithFallback.bind(this), messenger, updateTransactionData: this.#updateTransactionData.bind(this), @@ -263,6 +269,7 @@ export class TransactionPayController extends BaseController< if (shouldUpdateQuotes) { updateQuotes({ + accountSupports7702: this.#accountSupports7702, getStrategies: this.#getStrategiesWithFallback.bind(this), messenger: this.messenger, transactionData: this.state.transactionData[transactionId], diff --git a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.test.ts b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.test.ts index 1882d52d1d7..6b88a257ad3 100644 --- a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.test.ts +++ b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.test.ts @@ -52,6 +52,7 @@ describe('QuoteRefresher', () => { it('polls if quotes detected in state', async () => { new QuoteRefresher({ + accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData: jest.fn(), @@ -67,6 +68,7 @@ describe('QuoteRefresher', () => { it('does not poll if no quotes in state', async () => { new QuoteRefresher({ + accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData: jest.fn(), @@ -82,6 +84,7 @@ describe('QuoteRefresher', () => { it('polls again after interval', async () => { new QuoteRefresher({ + accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData: jest.fn(), @@ -100,6 +103,7 @@ describe('QuoteRefresher', () => { it('stops polling if quotes removed', async () => { new QuoteRefresher({ + accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData: jest.fn(), @@ -118,6 +122,7 @@ describe('QuoteRefresher', () => { const updateTransactionData = jest.fn(); new QuoteRefresher({ + accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData, @@ -140,6 +145,7 @@ describe('QuoteRefresher', () => { const updateTransactionData = jest.fn(); new QuoteRefresher({ + accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData, @@ -166,6 +172,7 @@ describe('QuoteRefresher', () => { const updateTransactionData = jest.fn(); new QuoteRefresher({ + accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData, diff --git a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts index ec93f5c22db..36408c3793b 100644 --- a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts +++ b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts @@ -3,6 +3,7 @@ import { createModuleLogger } from '@metamask/utils'; import { noop } from 'lodash'; import type { + AccountSupports7702Callback, TransactionPayControllerMessenger, TransactionPayControllerState, } from '..'; @@ -30,19 +31,24 @@ export class QuoteRefresher { readonly #updateTransactionData: UpdateTransactionDataCallback; + readonly #accountSupports7702: AccountSupports7702Callback; + constructor({ getStrategies, messenger, updateTransactionData, + accountSupports7702, }: { getStrategies: (transaction: TransactionMeta) => TransactionPayStrategy[]; messenger: TransactionPayControllerMessenger; updateTransactionData: UpdateTransactionDataCallback; + accountSupports7702: AccountSupports7702Callback; }) { this.#getStrategies = getStrategies; this.#messenger = messenger; this.#isRunning = false; this.#isUpdating = false; + this.#accountSupports7702 = accountSupports7702; this.#updateTransactionData = updateTransactionData; messenger.subscribe( @@ -81,6 +87,7 @@ export class QuoteRefresher { this.#messenger, this.#updateTransactionData, this.#getStrategies, + this.#accountSupports7702, ); } catch (error) { log('Error refreshing quotes', error); diff --git a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts index a8b739d451b..f880dcb4af3 100644 --- a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts +++ b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts @@ -26,6 +26,7 @@ const QUOTE_MOCK = { } as TransactionPayQuote; describe('TransactionPayPublishHook', () => { + const accountSupports7702Mock = jest.fn().mockResolvedValue(true); const isSmartTransactionMock = jest.fn(); const executeMock = jest.fn(); const getStrategyByNameMock = jest.mocked(getStrategyByName); @@ -52,6 +53,7 @@ describe('TransactionPayPublishHook', () => { jest.resetAllMocks(); hook = new TransactionPayPublishHook({ + accountSupports7702: accountSupports7702Mock, isSmartTransaction: isSmartTransactionMock, messenger, }); @@ -81,6 +83,7 @@ describe('TransactionPayPublishHook', () => { expect(executeMock).toHaveBeenCalledWith( expect.objectContaining({ + accountSupports7702: true, quotes: [QUOTE_MOCK, QUOTE_MOCK], }), ); diff --git a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts index 14244237fdb..3b45fdc000c 100644 --- a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts +++ b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts @@ -6,6 +6,7 @@ import { createModuleLogger } from '@metamask/utils'; import { projectLogger } from '../logger'; import type { + AccountSupports7702Callback, TransactionPayControllerMessenger, TransactionPayQuote, } from '../types'; @@ -23,13 +24,18 @@ export class TransactionPayPublishHook { readonly #messenger: TransactionPayControllerMessenger; + readonly #accountSupports7702: AccountSupports7702Callback; + constructor({ + accountSupports7702, isSmartTransaction, messenger, }: { + accountSupports7702: AccountSupports7702Callback; isSmartTransaction: (chainId: Hex) => boolean; messenger: TransactionPayControllerMessenger; }) { + this.#accountSupports7702 = accountSupports7702; this.#isSmartTransaction = isSmartTransaction; this.#messenger = messenger; } @@ -81,8 +87,11 @@ export class TransactionPayPublishHook { ); const strategy = getStrategyByName(quotes[0].strategy); + const from = transactionMeta.txParams.from as Hex; + const accountSupports7702 = await this.#accountSupports7702(from); return await strategy.execute({ + accountSupports7702, isSmartTransaction: this.#isSmartTransaction, quotes, messenger: this.#messenger, diff --git a/packages/transaction-pay-controller/src/index.ts b/packages/transaction-pay-controller/src/index.ts index 53cc04fa203..930cb9c3b9f 100644 --- a/packages/transaction-pay-controller/src/index.ts +++ b/packages/transaction-pay-controller/src/index.ts @@ -1,4 +1,5 @@ export type { + AccountSupports7702Callback, TransactionConfig, TransactionConfigCallback, TransactionFiatPayment, diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts index 8b91864deab..3b83924acc8 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts @@ -164,7 +164,6 @@ describe('Across Quotes', () => { const calculateGasCostMock = jest.mocked(calculateGasCost); const { - accountSupports7702Mock, messenger, estimateGasMock, estimateGasBatchMock, @@ -204,7 +203,6 @@ describe('Across Quotes', () => { getGasBufferMock.mockReturnValue(1.0); getSlippageMock.mockReturnValue(0.005); - accountSupports7702Mock.mockResolvedValue(true); findNetworkClientIdByChainIdMock.mockReturnValue('mainnet'); estimateGasMock.mockResolvedValue({ gas: '0x5208', @@ -218,11 +216,9 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result).toHaveLength(1); expect(result[0].strategy).toBe(TransactionPayStrategy.Across); @@ -231,32 +227,28 @@ describe('Across Quotes', () => { }); it('filters out requests with zero target amount', async () => { - const result = await getAcrossQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - }, - ], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + }, + ], + transaction: TRANSACTION_META_MOCK, }); expect(result).toStrictEqual([]); expect(successfulFetchMock).not.toHaveBeenCalled(); }); it('filters out non-max requests with missing target amount', async () => { - const result = await getAcrossQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: undefined, - } as unknown as QuoteRequest, - ], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: undefined, + } as unknown as QuoteRequest, + ], + transaction: TRANSACTION_META_MOCK, }); expect(result).toStrictEqual([]); expect(successfulFetchMock).not.toHaveBeenCalled(); @@ -266,11 +258,9 @@ describe('Across Quotes', () => { successfulFetchMock.mockRejectedValue(new Error('Network error')); await expect( - getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }), + getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }), ).rejects.toThrow(/Failed to fetch Across quotes/u); }); @@ -279,11 +269,9 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], - transaction: TRANSACTION_META_MOCK, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], + transaction: TRANSACTION_META_MOCK, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -297,11 +285,9 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -317,11 +303,9 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -353,11 +337,9 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -372,11 +354,9 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); const [, options] = successfulFetchMock.mock.calls[0]; const body = getRequestBody(); @@ -396,26 +376,24 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '1000000', - targetChainId: CHAIN_ID_ARBITRUM, - targetTokenAddress: ARBITRUM_USDC_ADDRESS, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - type: TransactionType.perpsDeposit, - txParams: { - from: FROM_MOCK, - to: ARBITRUM_USDC_ADDRESS, - data: buildTransferData(TRANSFER_RECIPIENT, 1), - }, - } as TransactionMeta, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '1000000', + targetChainId: CHAIN_ID_ARBITRUM, + targetTokenAddress: ARBITRUM_USDC_ADDRESS, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.perpsDeposit, + txParams: { + from: FROM_MOCK, + to: ARBITRUM_USDC_ADDRESS, + data: buildTransferData(TRANSFER_RECIPIENT, 1), + }, + } as TransactionMeta, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -438,17 +416,15 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - from: FROM_MOCK, - data: transferData, - }, + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + from: FROM_MOCK, + data: transferData, }, - }); + }, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -464,14 +440,12 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [{ data: transferData }], - } as TransactionMeta, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [{ data: transferData }], + } as TransactionMeta, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -487,18 +461,16 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - type: TransactionType.predictDeposit, - txParams: { - from: FROM_MOCK, - data: transferData, - }, + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.predictDeposit, + txParams: { + from: FROM_MOCK, + data: transferData, }, - }); + }, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -514,15 +486,13 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - type: TransactionType.predictDeposit, - nestedTransactions: [{ data: transferData }], - } as TransactionMeta, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.predictDeposit, + nestedTransactions: [{ data: transferData }], + } as TransactionMeta, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -540,18 +510,16 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { to: FACTORY_ADDRESS, data: createProxyData }, - { to: SAFE_ADDRESS, data: execTransactionData }, - { to: QUOTE_REQUEST_MOCK.targetTokenAddress, data: transferData }, - ], - } as TransactionMeta, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { to: FACTORY_ADDRESS, data: createProxyData }, + { to: SAFE_ADDRESS, data: execTransactionData }, + { to: QUOTE_REQUEST_MOCK.targetTokenAddress, data: transferData }, + ], + } as TransactionMeta, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -643,17 +611,15 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { to: FACTORY_ADDRESS, data: createProxyData }, - { data: transferData }, - ], - } as TransactionMeta, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { to: FACTORY_ADDRESS, data: createProxyData }, + { data: transferData }, + ], + } as TransactionMeta, }); const body = getRequestBody(); const [url] = successfulFetchMock.mock.calls[0]; @@ -680,23 +646,21 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { - to: QUOTE_REQUEST_MOCK.targetTokenAddress, - data: firstTransferData, - }, - { - to: QUOTE_REQUEST_MOCK.targetTokenAddress, - data: secondTransferData, - }, - ], - } as TransactionMeta, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + to: QUOTE_REQUEST_MOCK.targetTokenAddress, + data: firstTransferData, + }, + { + to: QUOTE_REQUEST_MOCK.targetTokenAddress, + data: secondTransferData, + }, + ], + } as TransactionMeta, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -730,11 +694,9 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -750,18 +712,16 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - from: FROM_MOCK, - to: FACTORY_ADDRESS, - data: createProxyData, - }, + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + from: FROM_MOCK, + to: FACTORY_ADDRESS, + data: createProxyData, }, - }); + }, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -802,19 +762,17 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { - to: FACTORY_ADDRESS, - data: '0xdeadbeef' as Hex, - }, - ], - } as TransactionMeta, - }), + getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + to: FACTORY_ADDRESS, + data: '0xdeadbeef' as Hex, + }, + ], + } as TransactionMeta, }), ).rejects.toThrow(/Destination selector: 0xdeadbeef/u); }); @@ -826,14 +784,12 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [{ data: createProxyData }], - } as TransactionMeta, - }), + getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [{ data: createProxyData }], + } as TransactionMeta, }), ).rejects.toThrow(/Across only supports direct token transfers/u); }); @@ -845,47 +801,41 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [{ data: execTransactionData }], - } as TransactionMeta, - }), + getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [{ data: execTransactionData }], + } as TransactionMeta, }), ).rejects.toThrow(/Across only supports direct token transfers/u); }); it('throws when destination flow is not transfer-style', async () => { await expect( - getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - from: FROM_MOCK, - data: '0xabc' as Hex, - }, + getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + from: FROM_MOCK, + data: '0xabc' as Hex, }, - }), + }, }), ).rejects.toThrow(/Across only supports direct token transfers/u); }); it('throws when txParams include authorization list', async () => { await expect( - getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - from: FROM_MOCK, - data: '0xabc' as Hex, - authorizationList: [{ address: '0xabc' as Hex }], - }, - } as TransactionMeta, - }), + getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + from: FROM_MOCK, + data: '0xabc' as Hex, + authorizationList: [{ address: '0xabc' as Hex }], + }, + } as TransactionMeta, }), ).rejects.toThrow(/Across does not support type-4\/EIP-7702/u); expect(successfulFetchMock).not.toHaveBeenCalled(); @@ -900,11 +850,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(parseFloat(result[0].dust.usd)).toBeGreaterThan(0); }); @@ -920,11 +868,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.provider.usd).toBe('0.5'); expect(result[0].fees.provider.fiat).toBe('1'); @@ -941,11 +887,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.provider.usd).toBe('1.9996'); expect(result[0].fees.provider.fiat).toBe('3.9992'); @@ -964,11 +908,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.provider.usd).toBe('0'); expect(result[0].fees.provider.fiat).toBe('0'); @@ -987,11 +929,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.provider.usd).toBe('0'); expect(result[0].fees.provider.fiat).toBe('0'); @@ -1011,11 +951,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [request], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [request], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].dust.usd).toBe('0.0004'); }); @@ -1028,11 +966,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].estimatedDuration).toBe(0); }); @@ -1047,11 +983,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.targetNetwork.usd).toBe('0'); }); @@ -1064,11 +998,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].sourceAmount.raw).toBe('0'); }); @@ -1098,11 +1030,9 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result).toHaveLength(1); expect(calculateGasCostMock).toHaveBeenNthCalledWith( @@ -1145,11 +1075,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(calculateGasCostMock).toHaveBeenCalledTimes(6); expect(calculateGasCostMock).toHaveBeenCalledWith( @@ -1207,11 +1135,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(estimateGasBatchMock).toHaveBeenCalledWith({ chainId: '0x1', @@ -1258,8 +1184,6 @@ describe('Across Quotes', () => { }); it('re-estimates individually when batch returns 7702 but account does not support it', async () => { - accountSupports7702Mock.mockResolvedValue(false); - estimateGasBatchMock.mockResolvedValue({ totalGasLimit: 51000, gasLimits: [51000], @@ -1285,6 +1209,7 @@ describe('Across Quotes', () => { } as Response); const result = await getAcrossQuotes({ + accountSupports7702: false, messenger, requests: [QUOTE_REQUEST_MOCK], transaction: TRANSACTION_META_MOCK, @@ -1323,11 +1248,9 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }), + getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }), ).rejects.toThrow( 'Failed to fetch Across quotes: Error: Across combined batch gas estimate missing', ); @@ -1355,11 +1278,9 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }), + getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }), ).rejects.toThrow( 'Failed to fetch Across quotes: Error: Batch estimation failed', ); @@ -1379,11 +1300,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(estimateGasMock).not.toHaveBeenCalled(); expect(result[0].original.metamask.gasLimits).toStrictEqual([ @@ -1411,11 +1330,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(estimateGasMock).toHaveBeenCalledTimes(1); expect(result[0].original.metamask.gasLimits).toStrictEqual([ @@ -1478,11 +1395,9 @@ describe('Across Quotes', () => { }), } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(calculateGasCostMock).toHaveBeenNthCalledWith( 1, @@ -1512,11 +1427,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].original.metamask.gasLimits).toStrictEqual([ { @@ -1553,11 +1466,9 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result).toHaveLength(1); }); @@ -1575,13 +1486,11 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); - expect(result[0].targetAmount.raw).toBe('150'); + expect(result[0].targetAmount.usd).toBe('0.0003'); expect(result[0].dust.usd).toBe('0'); expect(result[0].fees.provider.usd).toBe('0'); expect(result[0].fees.provider.fiat).toBe('0'); @@ -1600,11 +1509,9 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.provider.usd).toBe('0.5'); expect(result[0].fees.provider.fiat).toBe('1'); @@ -1619,15 +1526,11 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); - expect(result[0].targetAmount.raw).toBe( - QUOTE_REQUEST_MOCK.targetAmountMinimum, - ); + expect(result[0].targetAmount.usd).toBe('0.000246'); }); it('handles missing target amount minimum for max amount requests', async () => { @@ -1645,13 +1548,11 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [request], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [request], + transaction: TRANSACTION_META_MOCK, }); - expect(result[0].targetAmount.raw).toBe('0'); + expect(result[0].targetAmount.usd).toBe('0'); }); it('uses from address as recipient when no transfer data', async () => { @@ -1659,11 +1560,9 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -1679,21 +1578,19 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { data: transferData }, - { data: '0xbeef' as Hex }, - ], - txParams: { - from: FROM_MOCK, - data: '0xabc' as Hex, - }, - } as TransactionMeta, - }), + getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { data: transferData }, + { data: '0xbeef' as Hex }, + ], + txParams: { + from: FROM_MOCK, + data: '0xabc' as Hex, + }, + } as TransactionMeta, }), ).rejects.toThrow(/Across only supports direct token transfers/u); }); @@ -1703,18 +1600,16 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [{ to: '0xabc' as Hex }], - txParams: { - from: FROM_MOCK, - data: '0xdeadbeef' as Hex, - }, - } as TransactionMeta, - }), + getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [{ to: '0xabc' as Hex }], + txParams: { + from: FROM_MOCK, + data: '0xdeadbeef' as Hex, + }, + } as TransactionMeta, }), ).rejects.toThrow(/Destination selector: 0xdeadbeef/u); }); @@ -1725,11 +1620,9 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -1745,11 +1638,9 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }), + getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }), ).rejects.toThrow(/Failed to fetch Across quotes/u); }); @@ -1765,11 +1656,9 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - const result = await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result).toHaveLength(1); }); @@ -1782,21 +1671,19 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { data: '0xother' as Hex }, - { data: transferData }, - ], - txParams: { - from: FROM_MOCK, - data: '0xnonTransferData' as Hex, - }, - } as TransactionMeta, - }), + getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { data: '0xother' as Hex }, + { data: transferData }, + ], + txParams: { + from: FROM_MOCK, + data: '0xnonTransferData' as Hex, + }, + } as TransactionMeta, }), ).rejects.toThrow(/Across only supports direct token transfers/u); }); @@ -1807,18 +1694,16 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [{ to: '0xabc' as Hex }, { data: transferData }], - txParams: { - from: FROM_MOCK, - data: '0xnonTransferData' as Hex, - }, - } as TransactionMeta, - }); + await getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [{ to: '0xabc' as Hex }, { data: transferData }], + txParams: { + from: FROM_MOCK, + data: '0xnonTransferData' as Hex, + }, + } as TransactionMeta, }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -1832,21 +1717,19 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { data: '0xdeadbeef' as Hex }, - { data: '0xcafebabe' as Hex }, - ], - txParams: { - from: FROM_MOCK, - data: undefined, - }, - } as TransactionMeta, - }), + getAcrossQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { data: '0xdeadbeef' as Hex }, + { data: '0xcafebabe' as Hex }, + ], + txParams: { + from: FROM_MOCK, + data: undefined, + }, + } as TransactionMeta, }), ).rejects.toThrow(/Destination selector: 0xdeadbeef/u); }); }); diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts index 11edb0da171..88294e1d2c3 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -189,7 +189,7 @@ async function normalizeQuote( request: QuoteRequest, fullRequest: PayStrategyGetQuotesRequest, ): Promise> { - const { messenger } = fullRequest; + const { accountSupports7702, messenger } = fullRequest; const { quote } = original; const { usdToFiatRate, sourceFiatRate, targetFiatRate } = getFiatRates( @@ -200,8 +200,12 @@ async function normalizeQuote( const dustUsd = calculateDustUsd(quote, request, targetFiatRate); const dust = getFiatValueFromUsd(dustUsd, usdToFiatRate); - const { gasLimits, is7702, requiresAuthorizationList, sourceNetwork } = - await calculateSourceNetworkCost(quote, messenger, request); + const { gasLimits, is7702, sourceNetwork } = await calculateSourceNetworkCost( + quote, + messenger, + request, + accountSupports7702, + ); const targetNetwork = getFiatValueFromUsd(new BigNumber(0), usdToFiatRate); @@ -384,6 +388,7 @@ async function calculateSourceNetworkCost( quote: AcrossSwapApprovalResponse, messenger: TransactionPayControllerMessenger, request: QuoteRequest, + accountSupports7702: boolean, ): Promise<{ sourceNetwork: TransactionPayQuote['fees']['sourceNetwork']; gasLimits: AcrossGasLimits; @@ -407,43 +412,10 @@ async function calculateSourceNetworkCost( to: transaction.to, value: transaction.value ?? '0x0', })), + accountSupports7702, }); const { batchGasLimit } = gasEstimates; - const accountSupports7702 = await messenger.call( - 'KeyringController:accountSupports7702', - from, - ); - - // If the chain returned a combined 7702 gas limit but the account cannot sign - // EIP-7702 authorizations (e.g. hardware wallet), re-estimate each transaction - // individually so the submit path receives per-transaction gas limits. - if (gasEstimates.is7702 && !accountSupports7702) { - const individualResults = await Promise.all( - orderedTransactions.map((transaction) => - estimateQuoteGasLimits({ - fallbackGas: acrossFallbackGas, - messenger, - transactions: [ - { - chainId: toHex(transaction.chainId), - data: transaction.data, - from, - gas: transaction.gas, - to: transaction.to, - value: transaction.value ?? '0x0', - }, - ], - }), - ), - ); - gasEstimates = { - ...gasEstimates, - is7702: false, - gasLimits: individualResults.map((result) => result.gasLimits[0]), - }; - } - const { is7702 } = gasEstimates; if (is7702) { diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts index 6d8d7d7ab1f..0465c27042a 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts @@ -36,6 +36,7 @@ const QUOTE_MOCK: TransactionPayQuote = { dust: { usd: '0', fiat: '0' }, estimatedDuration: 0, fees: { + metaMask: { usd: '0', fiat: '0' }, provider: { usd: '0', fiat: '0' }, sourceNetwork: { estimate: { usd: '0', fiat: '0', human: '0', raw: '0' }, @@ -93,7 +94,7 @@ const QUOTE_MOCK: TransactionPayQuote = { targetTokenAddress: '0xdef' as Hex, }, sourceAmount: { usd: '0', fiat: '0', human: '0', raw: '0' }, - targetAmount: { usd: '0', fiat: '0', human: '0', raw: '0' }, + targetAmount: { usd: '0', fiat: '0' }, strategy: TransactionPayStrategy.Across, }; @@ -101,7 +102,6 @@ describe('Across Submit', () => { const successfulFetchMock = jest.mocked(successfulFetch); const { - accountSupports7702Mock, addTransactionBatchMock, addTransactionMock, estimateGasMock, @@ -127,7 +127,6 @@ describe('Across Submit', () => { }, }); - accountSupports7702Mock.mockResolvedValue(true); estimateGasMock.mockResolvedValue({ gas: '0x5208', simulationFails: undefined, @@ -191,12 +190,10 @@ describe('Across Submit', () => { }) as TransactionPayQuote; it('submits a batch when approvals exist', async () => { - await submitAcrossQuotes({ - messenger, - quotes: [QUOTE_MOCK], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [QUOTE_MOCK], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); expect(addTransactionBatchMock).toHaveBeenCalledWith( @@ -230,12 +227,10 @@ describe('Across Submit', () => { }, } as unknown as TransactionPayQuote; - await submitAcrossQuotes({ - messenger, - quotes: [batchGasQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [batchGasQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -261,8 +256,7 @@ describe('Across Submit', () => { ); }); - it('submits individually when account does not support 7702', async () => { - accountSupports7702Mock.mockResolvedValue(false); + it('submits batch sequentially when account does not support 7702', async () => { const nonIs7702Quote = { ...QUOTE_MOCK, @@ -278,15 +272,19 @@ describe('Across Submit', () => { }, } as unknown as TransactionPayQuote; - await submitAcrossQuotes({ - messenger, - quotes: [nonIs7702Quote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: false, messenger, + quotes: [nonIs7702Quote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); - expect(addTransactionBatchMock).not.toHaveBeenCalled(); - expect(addTransactionMock).toHaveBeenCalledTimes(2); + expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + disable7702: true, + disableSequential: false, + }), + ); + expect(addTransactionMock).not.toHaveBeenCalled(); }); it('submits a single transaction when no approvals', async () => { @@ -301,12 +299,10 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ - messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(addTransactionMock).toHaveBeenCalledTimes(1); expect(addTransactionMock).toHaveBeenCalledWith( @@ -330,12 +326,10 @@ describe('Across Submit', () => { } as TransactionPayQuote; await expect( - submitAcrossQuotes({ - messenger, - quotes: [missingBatchGasQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }), + submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [missingBatchGasQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }), ).rejects.toThrow('Missing quote gas limit for Across 7702 batch'); expect(addTransactionBatchMock).not.toHaveBeenCalled(); @@ -353,15 +347,13 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ - messenger, - quotes: [noApprovalQuote], - transaction: { - ...TRANSACTION_META_MOCK, - type: TransactionType.predictDeposit, - }, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [noApprovalQuote], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.predictDeposit, + }, + isSmartTransaction: jest.fn(), }); expect(addTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -383,15 +375,13 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ - messenger, - quotes: [noApprovalQuote], - transaction: { - ...TRANSACTION_META_MOCK, - type: TransactionType.swap, - }, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [noApprovalQuote], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.swap, + }, + isSmartTransaction: jest.fn(), }); expect(addTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -413,15 +403,13 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ - messenger, - quotes: [noApprovalQuote], - transaction: { - ...TRANSACTION_META_MOCK, - type: undefined, - }, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [noApprovalQuote], + transaction: { + ...TRANSACTION_META_MOCK, + type: undefined, + }, + isSmartTransaction: jest.fn(), }); expect(addTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -455,12 +443,10 @@ describe('Across Submit', () => { }, ]); - await submitAcrossQuotes({ - messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(addTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -484,12 +470,10 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ - messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(updateTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -538,12 +522,10 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - const result = await submitAcrossQuotes({ - messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + const result = await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(updateTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -593,12 +575,10 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ - messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(updateTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -655,12 +635,10 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - const result = await submitAcrossQuotes({ - messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + const result = await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(successfulFetchMock).toHaveBeenCalledWith( expect.stringContaining('/deposit/status?depositTxnRef=0xconfirmed'), @@ -764,12 +742,10 @@ describe('Across Submit', () => { } as TransactionPayQuote; await expect( - submitAcrossQuotes({ - messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }), + submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }), ).rejects.toThrow('Across request failed with status: failed'); }); @@ -787,12 +763,10 @@ describe('Across Submit', () => { }), } as Response); - const resultPromise = submitAcrossQuotes({ - messenger, - quotes: [buildDepositQuote()], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + const resultPromise = submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [buildDepositQuote()], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); await jest.runAllTimersAsync(); const result = await resultPromise; @@ -813,12 +787,10 @@ describe('Across Submit', () => { }), } as Response); - const result = await submitAcrossQuotes({ - messenger, - quotes: [buildDepositQuote()], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + const result = await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [buildDepositQuote()], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(result.transactionHash).toBe('0xfill'); }); @@ -851,12 +823,10 @@ describe('Across Submit', () => { }), } as Response); - const result = await submitAcrossQuotes({ - messenger, - quotes: [buildDepositQuote()], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + const result = await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [buildDepositQuote()], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(result.transactionHash).toBe('0xbridge'); }); @@ -869,12 +839,10 @@ describe('Across Submit', () => { }), } as Response); - const result = await submitAcrossQuotes({ - messenger, - quotes: [buildDepositQuote()], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + const result = await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [buildDepositQuote()], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(result.transactionHash).toBe('0xconfirmed'); }); @@ -899,12 +867,10 @@ describe('Across Submit', () => { }), } as Response); - const resultPromise = submitAcrossQuotes({ - messenger, - quotes: [buildDepositQuote()], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + const resultPromise = submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [buildDepositQuote()], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); await jest.runAllTimersAsync(); const result = await resultPromise; @@ -933,12 +899,10 @@ describe('Across Submit', () => { }), } as Response); - const resultPromise = submitAcrossQuotes({ - messenger, - quotes: [buildDepositQuote()], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + const resultPromise = submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [buildDepositQuote()], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); await jest.runAllTimersAsync(); const result = await resultPromise; @@ -965,12 +929,10 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ - messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); const params = addTransactionMock.mock.calls[0][0] as { gas: Hex }; @@ -994,12 +956,10 @@ describe('Across Submit', () => { } as unknown as TransactionPayQuote; await expect( - submitAcrossQuotes({ - messenger, - quotes: [missingSwapGasQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }), + submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [missingSwapGasQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }), ).rejects.toThrow('Missing quote gas limit for Across swap transaction'); expect(addTransactionMock).not.toHaveBeenCalled(); @@ -1017,12 +977,10 @@ describe('Across Submit', () => { } as TransactionPayQuote; await expect( - submitAcrossQuotes({ - messenger, - quotes: [missingApprovalGasQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }), + submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [missingApprovalGasQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }), ).rejects.toThrow( 'Missing quote gas limit for Across approval transaction at index 0', ); @@ -1043,12 +1001,10 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ - messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); const params = addTransactionMock.mock.calls[0][0] as { maxFeePerGas: Hex; @@ -1076,12 +1032,10 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ - messenger, - quotes: [decimalGasQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [decimalGasQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); const params = addTransactionMock.mock.calls[0][0] as { maxFeePerGas: Hex; @@ -1110,12 +1064,10 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ - messenger, - quotes: [quoteWithApproval], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [quoteWithApproval], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(addTransactionBatchMock).toHaveBeenCalled(); }); @@ -1141,12 +1093,10 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ - messenger, - quotes: [quoteWithoutValue], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [quoteWithoutValue], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(addTransactionMock).toHaveBeenCalled(); const params = addTransactionMock.mock.calls[0][0] as { value: Hex }; @@ -1209,12 +1159,10 @@ describe('Across Submit', () => { }; }); - await submitAcrossQuotes({ - messenger, - quotes: [quote1, quote2], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); + await submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [quote1, quote2], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }); expect(addTransactionMock).toHaveBeenCalledTimes(2); }); @@ -1235,12 +1183,10 @@ describe('Across Submit', () => { addTransactionMock.mockRejectedValue(new Error('submission failed')); await expect( - submitAcrossQuotes({ - messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }), + submitAcrossQuotes({ accountSupports7702: true, messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), }), ).rejects.toThrow('submission failed'); expect(unsubscribeSpy).toHaveBeenCalledWith( diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts index 7bbfe95f071..f08fe2e017c 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts @@ -48,6 +48,7 @@ export async function submitAcrossQuotes( log('Executing quotes', request); const { quotes, messenger, transaction } = request; + const { accountSupports7702 } = request; let transactionHash: Hex | undefined; @@ -56,6 +57,7 @@ export async function submitAcrossQuotes( quote, messenger, transaction, + accountSupports7702, )); } @@ -66,6 +68,7 @@ async function executeSingleQuote( quote: TransactionPayQuote, messenger: TransactionPayControllerMessenger, transaction: TransactionMeta, + accountSupports7702: boolean, ): Promise<{ transactionHash?: Hex }> { log('Executing single quote', quote); @@ -86,6 +89,7 @@ async function executeSingleQuote( transaction.id, acrossDepositType, messenger, + accountSupports7702, ); updateTransaction( @@ -116,15 +120,12 @@ async function submitTransactions( parentTransactionId: string, acrossDepositType: TransactionType, messenger: TransactionPayControllerMessenger, + accountSupports7702: boolean, ): Promise { const { swapTx } = quote.original.quote; const { gasLimits: quoteGasLimits, is7702: apiIs7702 } = quote.original.metamask; const { from } = quote.request; - const accountSupports7702 = await messenger.call( - 'KeyringController:accountSupports7702', - from, - ); const is7702 = apiIs7702 && accountSupports7702; const chainId = toHex(swapTx.chainId); const orderedTransactions = getAcrossOrderedTransactions({ @@ -200,22 +201,18 @@ async function submitTransactions( }, ); - let result: { result: Promise } | undefined; - try { - if (transactions.length === 1 || !accountSupports7702) { - for (const { params, type } of transactions) { - result = await messenger.call( - 'TransactionController:addTransaction', - params, - { - networkClientId, - origin: ORIGIN_METAMASK, - requireApproval: false, - type, - }, - ); - } + if (transactions.length === 1) { + await messenger.call( + 'TransactionController:addTransaction', + transactions[0].params, + { + networkClientId, + origin: ORIGIN_METAMASK, + requireApproval: false, + type: transactions[0].type, + }, + ); } else { const batchTransactions = transactions.map(({ params, type }) => ({ params: toBatchTransactionParams(params), @@ -224,7 +221,7 @@ async function submitTransactions( await messenger.call('TransactionController:addTransactionBatch', { disable7702: !gasLimit7702, - disableHook: Boolean(gasLimit7702), + disableHook: !gasLimit7702, disableSequential: Boolean(gasLimit7702), from, gasLimit7702, @@ -238,11 +235,6 @@ async function submitTransactions( end(); } - if (result) { - const txHash = await result.result; - log('Submitted transaction', txHash); - } - await Promise.all( transactionIds.map((txId) => waitForTransactionConfirmed(txId, messenger)), ); diff --git a/packages/transaction-pay-controller/src/strategy/bridge/BridgeStrategy.test.ts b/packages/transaction-pay-controller/src/strategy/bridge/BridgeStrategy.test.ts index ed1e824313c..37c807c4003 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/BridgeStrategy.test.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/BridgeStrategy.test.ts @@ -53,6 +53,7 @@ describe('BridgeStrategy', () => { describe('getQuotes', () => { it('returns result from util', async () => { const result = new BridgeStrategy().getQuotes({ + accountSupports7702: true, messenger: {} as TransactionPayControllerMessenger, requests: [], transaction: {} as TransactionMeta, @@ -86,6 +87,7 @@ describe('BridgeStrategy', () => { describe('execute', () => { it('calls util', async () => { await new BridgeStrategy().execute({ + accountSupports7702: true, isSmartTransaction: () => false, quotes: [QUOTE_MOCK], messenger: {} as TransactionPayControllerMessenger, diff --git a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts index ec7a12a130b..e69a73072ac 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts @@ -143,6 +143,7 @@ describe('Bridge Quotes Utils', () => { }); request = { + accountSupports7702: true, requests: [QUOTE_REQUEST_1_MOCK, QUOTE_REQUEST_2_MOCK], messenger, transaction: TRANSACTION_META_MOCK, diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts index ec26912193c..959631c6019 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts @@ -35,7 +35,8 @@ const log = createModuleLogger(projectLogger, 'fiat-strategy'); export async function getFiatQuotes( request: PayStrategyGetQuotesRequest, ): Promise[]> { - const { fiatPaymentMethod, messenger, transaction } = request; + const { accountSupports7702, fiatPaymentMethod, messenger, transaction } = + request; const transactionId = transaction.id; const state = messenger.call('TransactionPayController:getState'); @@ -76,6 +77,7 @@ export async function getFiatQuotes( } const relayQuotes = await getRelayQuotes({ + accountSupports7702, messenger, requests: [relayRequest], transaction, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 8d9c724f63f..ee1aa07751c 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -180,7 +180,6 @@ describe('Relay Quotes Utils', () => { const getSlippageMock = jest.mocked(getSlippage); const { - accountSupports7702Mock, messenger, estimateGasMock, estimateGasBatchMock, @@ -216,7 +215,6 @@ describe('Relay Quotes Utils', () => { ...getDefaultRemoteFeatureFlagControllerState(), }); - accountSupports7702Mock.mockResolvedValue(true); isEIP7702ChainMock.mockReturnValue(true); isRelayExecuteEnabledMock.mockReturnValue(false); getGasBufferMock.mockReturnValue(1.0); @@ -232,11 +230,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result).toStrictEqual([ expect.objectContaining({ @@ -250,11 +246,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(successfulFetchMock).toHaveBeenCalledWith( DEFAULT_RELAY_QUOTE_URL, @@ -288,11 +282,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -309,11 +301,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -324,17 +314,14 @@ describe('Relay Quotes Utils', () => { it('omits originGasOverhead when account does not support 7702 even on EIP-7702 chain with relay execute enabled', async () => { isRelayExecuteEnabledMock.mockReturnValue(true); - accountSupports7702Mock.mockResolvedValue(false); successfulFetchMock.mockResolvedValue({ json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: false, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -348,11 +335,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], + transaction: TRANSACTION_META_MOCK, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -368,16 +353,14 @@ describe('Relay Quotes Utils', () => { it('throws if isMaxAmount is true and transaction includes data', async () => { await expect( - getRelayQuotes({ - messenger, - requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - data: '0xabc' as Hex, - }, - } as TransactionMeta, - }), + getRelayQuotes({ accountSupports7702: true, messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: '0xabc' as Hex, + }, + } as TransactionMeta, }), ).rejects.toThrow( 'Max amount quotes do not support included transactions', ); @@ -388,16 +371,14 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - data: '0xabc' as Hex, - }, - } as TransactionMeta, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: '0xabc' as Hex, + }, + } as TransactionMeta, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -434,11 +415,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].original.request).toStrictEqual({ amount: QUOTE_REQUEST_MOCK.targetAmountMinimum, @@ -458,16 +437,14 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - data: TOKEN_TRANSFER_DATA_MOCK, - }, - } as TransactionMeta, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: TOKEN_TRANSFER_DATA_MOCK, + }, + } as TransactionMeta, }); expect(getDelegationTransactionMock).not.toHaveBeenCalled(); }); @@ -477,16 +454,14 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - data: TOKEN_TRANSFER_DATA_MOCK, - }, - } as TransactionMeta, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: TOKEN_TRANSFER_DATA_MOCK, + }, + } as TransactionMeta, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -500,21 +475,19 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { - data: NESTED_TRANSACTION_DATA_MOCK, - }, - ], - txParams: { - data: '0xabc' as Hex, + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + data: NESTED_TRANSACTION_DATA_MOCK, }, - } as TransactionMeta, - }); + ], + txParams: { + data: '0xabc' as Hex, + }, + } as TransactionMeta, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -552,21 +525,19 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { - data: TOKEN_TRANSFER_DATA_MOCK, - }, - ], - txParams: { - data: '0xabc' as Hex, + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + data: TOKEN_TRANSFER_DATA_MOCK, }, - } as TransactionMeta, - }); + ], + txParams: { + data: '0xabc' as Hex, + }, + } as TransactionMeta, }); expect(getDelegationTransactionMock).not.toHaveBeenCalled(); }); @@ -576,21 +547,19 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { - data: TOKEN_TRANSFER_DATA_MOCK, - }, - ], - txParams: { - data: '0xabc' as Hex, + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + data: TOKEN_TRANSFER_DATA_MOCK, }, - } as TransactionMeta, - }); + ], + txParams: { + data: '0xabc' as Hex, + }, + } as TransactionMeta, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -604,24 +573,22 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { - data: NESTED_TRANSACTION_DATA_MOCK, - }, - { - data: TOKEN_TRANSFER_DATA_MOCK, - }, - ], - txParams: { - data: '0xabc' as Hex, + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + data: NESTED_TRANSACTION_DATA_MOCK, }, - } as TransactionMeta, - }); + { + data: TOKEN_TRANSFER_DATA_MOCK, + }, + ], + txParams: { + data: '0xabc' as Hex, + }, + } as TransactionMeta, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -636,21 +603,19 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetChainId: CHAIN_ID_HYPERCORE, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - data: '0xabc' as Hex, - }, - } as TransactionMeta, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetChainId: CHAIN_ID_HYPERCORE, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: '0xabc' as Hex, + }, + } as TransactionMeta, }); expect(getDelegationTransactionMock).not.toHaveBeenCalled(); }); @@ -660,21 +625,19 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetChainId: CHAIN_ID_HYPERCORE, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - data: TOKEN_TRANSFER_DATA_MOCK, - }, - } as TransactionMeta, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetChainId: CHAIN_ID_HYPERCORE, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: TOKEN_TRANSFER_DATA_MOCK, + }, + } as TransactionMeta, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -701,11 +664,9 @@ describe('Relay Quotes Utils', () => { }, }); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(successfulFetchMock).toHaveBeenCalledWith( relayQuoteUrl, @@ -718,17 +679,15 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - sourceTokenAmount: '0', - }, - ], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + sourceTokenAmount: '0', + }, + ], + transaction: TRANSACTION_META_MOCK, }); expect(successfulFetchMock).not.toHaveBeenCalled(); }); @@ -738,17 +697,15 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: TRANSACTION_META_MOCK, }); expect(successfulFetchMock).toHaveBeenCalled(); @@ -767,18 +724,16 @@ describe('Relay Quotes Utils', () => { const refundTo = '0xsafe000000000000000000000000000000000001' as Hex; - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo, + }, + ], + transaction: TRANSACTION_META_MOCK, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -792,17 +747,15 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: TRANSACTION_META_MOCK, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -828,17 +781,15 @@ describe('Relay Quotes Utils', () => { }, } as TransactionMeta; - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: postQuoteTransaction, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: postQuoteTransaction, }); // Original transaction should NOT be included in gas estimation. // Only relay step params are estimated. @@ -852,27 +803,25 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - gas: '0x13498', - value: '0', - }, - } as TransactionMeta, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + gas: '0x13498', + value: '0', + }, + } as TransactionMeta, }); expect(result[0].original.metamask.gasLimits).toStrictEqual([ 79000, 21000, @@ -887,28 +836,26 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - gas: '0x13498', - value: '0', - }, - nestedTransactions: [{ gas: '0xC350' }], - } as TransactionMeta, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + gas: '0x13498', + value: '0', + }, + nestedTransactions: [{ gas: '0xC350' }], + } as TransactionMeta, }); expect(result[0].original.metamask.gasLimits).toStrictEqual([ 50000, 21000, @@ -948,27 +895,25 @@ describe('Relay Quotes Utils', () => { gasLimits: [51000], }); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - gas: '0x13498', - value: '0', - }, - } as TransactionMeta, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + gas: '0x13498', + value: '0', + }, + } as TransactionMeta, }); // EIP-7702: original tx gas (79000) added to combined relay gas (51000) expect(result[0].original.metamask.gasLimits).toStrictEqual([130000]); @@ -1007,27 +952,25 @@ describe('Relay Quotes Utils', () => { gasLimits: [21000, 30000], }); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - gas: '0x13498', - value: '0', - }, - } as TransactionMeta, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + gas: '0x13498', + value: '0', + }, + } as TransactionMeta, }); // Original tx gas (79000) prepended to relay gas limits [21000, 30000] expect(result[0].original.metamask.gasLimits).toStrictEqual([ @@ -1041,26 +984,24 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - value: '0', - }, - } as TransactionMeta, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + value: '0', + }, + } as TransactionMeta, }); // No gas on txParams or nestedTransactions — only relay gas limits expect(result[0].original.metamask.gasLimits).toStrictEqual([21000]); @@ -1080,27 +1021,25 @@ describe('Relay Quotes Utils', () => { estimateGasMock.mockRejectedValue(new Error('Estimation failed')); - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - gas: '0x13498', // 79 000 - value: '0', - }, - } as TransactionMeta, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + gas: '0x13498', // 79 000 + value: '0', + }, + } as TransactionMeta, }); // Fallback: estimate=900000, max=1500000. // With originalTxGas=79000 added independently: @@ -1122,17 +1061,15 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: TRANSACTION_META_MOCK, }); // With no txParams.to the original tx should be skipped, so only // the relay step params are sent to gas estimation (single path). @@ -1154,18 +1091,16 @@ describe('Relay Quotes Utils', () => { const proxyAddress = '0xproxyAddress1234567890123456789012345678' as Hex; - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: proxyAddress, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: proxyAddress, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); expect(estimateGasMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1187,18 +1122,16 @@ describe('Relay Quotes Utils', () => { const proxyAddress = '0xproxyAddress1234567890123456789012345678' as Hex; - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: proxyAddress, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: proxyAddress, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); expect(estimateGasMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1232,18 +1165,16 @@ describe('Relay Quotes Utils', () => { const proxyAddress = '0xproxyAddress1234567890123456789012345678' as Hex; - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: proxyAddress, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: proxyAddress, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); expect(estimateGasBatchMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1277,18 +1208,16 @@ describe('Relay Quotes Utils', () => { const proxyAddress = '0xproxyAddress1234567890123456789012345678' as Hex; - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: proxyAddress, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: proxyAddress, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); expect(estimateGasBatchMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1312,18 +1241,16 @@ describe('Relay Quotes Utils', () => { const proxyAddress = '0xproxyAddress1234567890123456789012345678' as Hex; - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: proxyAddress, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: proxyAddress, + }, + ], + transaction: TRANSACTION_META_MOCK, }); expect(estimateGasMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1346,11 +1273,9 @@ describe('Relay Quotes Utils', () => { simulationFails: undefined, }); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(estimateGasMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1380,11 +1305,9 @@ describe('Relay Quotes Utils', () => { gasLimits: [50000, 50000], }); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(estimateGasBatchMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1401,18 +1324,16 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('0'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); expect(result[0].fees.isSourceGasFeeToken).toBe(true); }); @@ -1425,18 +1346,16 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('0'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); expect(getGasFeeTokensMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1458,18 +1377,16 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('0'); getGasFeeTokensMock.mockResolvedValue([]); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); }); @@ -1481,18 +1398,16 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('0'); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); }); @@ -1524,18 +1439,16 @@ describe('Relay Quotes Utils', () => { usd: '4.45', }); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); expect(successfulFetchMock).toHaveBeenCalledTimes(2); @@ -1569,19 +1482,17 @@ describe('Relay Quotes Utils', () => { usd: '999', }); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - sourceTokenAmount: '1', - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + sourceTokenAmount: '1', + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); expect(successfulFetchMock).toHaveBeenCalledTimes(1); expect(result).toHaveLength(1); @@ -1595,18 +1506,16 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('0'); getGasFeeTokensMock.mockRejectedValue(new Error('Simulation failed')); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); }); @@ -1626,18 +1535,16 @@ describe('Relay Quotes Utils', () => { usd: '4.45', }); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: TRANSACTION_META_MOCK, }); expect(successfulFetchMock).toHaveBeenCalledTimes(2); expect(result).toHaveLength(1); @@ -1665,18 +1572,16 @@ describe('Relay Quotes Utils', () => { usd: '4.45', }); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); expect(successfulFetchMock).toHaveBeenCalledTimes(2); expect(result[0].fees.isSourceGasFeeToken).toBe(true); @@ -1687,11 +1592,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].estimatedDuration).toBe(300); }); @@ -1701,11 +1604,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.metaMask).toStrictEqual({ usd: '0', @@ -1722,11 +1623,9 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.metaMask).toStrictEqual({ usd: '0.75', @@ -1743,11 +1642,9 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.provider).toStrictEqual({ usd: '1.11', @@ -1760,11 +1657,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.provider).toStrictEqual({ usd: '1.11', @@ -1790,11 +1685,9 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.provider).toStrictEqual({ usd: '0', @@ -1837,11 +1730,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].dust).toStrictEqual({ usd: '0.0246', @@ -1855,11 +1746,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.sourceNetwork).toStrictEqual({ estimate: { @@ -1885,11 +1774,9 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 900000 }), @@ -1941,11 +1828,9 @@ describe('Relay Quotes Utils', () => { gasLimits: [21000, 480000, 1000, 2000], }); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 504000 }), @@ -1960,11 +1845,9 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('1724999999999999'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.isSourceGasFeeToken).toBe(true); expect(result[0].fees.sourceNetwork).toStrictEqual({ @@ -2007,11 +1890,9 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('1724999999999999'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(calculateGasFeeTokenCostMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -2031,28 +1912,26 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('1724999999999999'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: { - ...PREDICT_WITHDRAW_TRANSACTION_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - gas: '0x5208', - value: '0', - }, - } as TransactionMeta, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: { + ...PREDICT_WITHDRAW_TRANSACTION_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + gas: '0x5208', + value: '0', + }, + } as TransactionMeta, }); expect(getGasFeeTokensMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -2074,11 +1953,9 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('1725000000000000'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); expect(result[0].fees.sourceNetwork).toStrictEqual({ @@ -2107,11 +1984,9 @@ describe('Relay Quotes Utils', () => { { ...GAS_FEE_TOKEN_MOCK, tokenAddress: '0xdef' as Hex }, ]); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); expect(result[0].fees.sourceNetwork).toStrictEqual({ @@ -2139,11 +2014,9 @@ describe('Relay Quotes Utils', () => { getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); calculateGasFeeTokenCostMock.mockReturnValue(undefined); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); expect(result[0].fees.sourceNetwork).toStrictEqual({ @@ -2173,11 +2046,9 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('1724999999999999'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(getGasFeeTokensMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -2203,11 +2074,9 @@ describe('Relay Quotes Utils', () => { }, }); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); expect(result[0].fees.sourceNetwork).toStrictEqual({ @@ -2239,11 +2108,9 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('1724999999999999'); isEIP7702ChainMock.mockReturnValue(true); - const result = await getRelayQuotes({ - messenger, - requests: [lineaQuoteRequest], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [lineaQuoteRequest], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); expect(result[0].fees.sourceNetwork).toStrictEqual({ @@ -2274,11 +2141,9 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.sourceNetwork).toStrictEqual({ estimate: ZERO_AMOUNT, @@ -2294,11 +2159,9 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].original.metamask.isExecute).toBe(true); }); @@ -2308,11 +2171,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.sourceNetwork).not.toStrictEqual({ estimate: ZERO_AMOUNT, @@ -2328,11 +2189,9 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].original.metamask.gasLimits).toStrictEqual([]); }); @@ -2353,11 +2212,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [HL_REQUEST], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [HL_REQUEST], + transaction: TRANSACTION_META_MOCK, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -2372,11 +2229,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [HL_REQUEST], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [HL_REQUEST], + transaction: TRANSACTION_META_MOCK, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -2390,11 +2245,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [HL_REQUEST], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [HL_REQUEST], + transaction: TRANSACTION_META_MOCK, }); const zeroAmount = { fiat: '0', human: '0', raw: '0', usd: '0' }; @@ -2407,11 +2260,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [HL_REQUEST], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [HL_REQUEST], + transaction: TRANSACTION_META_MOCK, }); expect(getTokenFiatRateMock).toHaveBeenCalledWith( expect.anything(), @@ -2426,11 +2277,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].fees.targetNetwork).toStrictEqual({ usd: '0', @@ -2443,11 +2292,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].targetAmount).toStrictEqual({ usd: '1.23', @@ -2473,13 +2320,12 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].targetAmount).toStrictEqual({ + usd: '1.23', fiat: '2.46', }); @@ -2490,17 +2336,15 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetChainId: CHAIN_ID_ARBITRUM, - targetTokenAddress: ARBITRUM_USDC_ADDRESS, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetChainId: CHAIN_ID_ARBITRUM, + targetTokenAddress: ARBITRUM_USDC_ADDRESS, + }, + ], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].targetAmount).toStrictEqual({ usd: '1', @@ -2513,11 +2357,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].targetAmount).toStrictEqual({ usd: '1.23', @@ -2529,11 +2371,9 @@ describe('Relay Quotes Utils', () => { successfulFetchMock.mockRejectedValue(new Error('Fetch error')); await expect( - getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }), + getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }), ).rejects.toThrow('Fetch error'); }); @@ -2545,11 +2385,9 @@ describe('Relay Quotes Utils', () => { } as never); await expect( - getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }), + getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }), ).rejects.toThrow(`Source token fiat rate not found`); }); @@ -2564,11 +2402,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [arbitrumToHyperliquidRequest], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [arbitrumToHyperliquidRequest], + transaction: TRANSACTION_META_MOCK, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -2596,11 +2432,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [postQuoteRequest], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [postQuoteRequest], + transaction: TRANSACTION_META_MOCK, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -2627,11 +2461,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [polygonToHyperliquidRequest], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [polygonToHyperliquidRequest], + transaction: TRANSACTION_META_MOCK, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -2655,11 +2487,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ - messenger, - requests: [polygonTargetRequest], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [polygonTargetRequest], + transaction: TRANSACTION_META_MOCK, }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -2685,11 +2515,9 @@ describe('Relay Quotes Utils', () => { simulationFails: undefined, }); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(estimateGasMock).toHaveBeenCalledWith( { @@ -2716,11 +2544,9 @@ describe('Relay Quotes Utils', () => { estimateGasMock.mockRejectedValue(new Error('Estimation failed')); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 900000 }), @@ -2742,11 +2568,9 @@ describe('Relay Quotes Utils', () => { }, }); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 900000 }), @@ -2773,11 +2597,9 @@ describe('Relay Quotes Utils', () => { gasLimits: [50000, 50000], }); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(estimateGasBatchMock).toHaveBeenCalledWith({ chainId: '0x1', @@ -2821,11 +2643,9 @@ describe('Relay Quotes Utils', () => { gasLimits: [30000, 50000], }); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(estimateGasBatchMock).toHaveBeenCalledTimes(1); expect(estimateGasMock).not.toHaveBeenCalled(); @@ -2854,11 +2674,9 @@ describe('Relay Quotes Utils', () => { ); await expect( - getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }), + getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }), ).rejects.toThrow( 'Failed to fetch Relay quotes: Error: Batch estimation failed', ); @@ -2869,11 +2687,9 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + const result = await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(result[0].original.metamask).toStrictEqual({ gasLimits: [21000], @@ -2890,11 +2706,9 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(estimateGasMock).toHaveBeenCalledWith( expect.objectContaining({ value: '0x0' }), @@ -2913,11 +2727,9 @@ describe('Relay Quotes Utils', () => { } as never); await expect( - getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }), + getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }), ).rejects.toThrow('Failed to fetch Relay quotes'); }); @@ -2935,11 +2747,9 @@ describe('Relay Quotes Utils', () => { } as never); await expect( - getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }), + getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }), ).rejects.toThrow('Failed to fetch Relay quotes'); }); @@ -2959,11 +2769,9 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1.5); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 75000 }), @@ -2993,11 +2801,9 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1.5); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 120000 }), @@ -3028,11 +2834,9 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1.5); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 70000 }), @@ -3063,11 +2867,9 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1.5); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 90000 }), @@ -3097,11 +2899,9 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1.5); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, - }); + await getRelayQuotes({ accountSupports7702: true, messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 105000 }), diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 01c603fdb15..65bee520764 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -221,13 +221,8 @@ async function getSingleQuote( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const useExactInput = isMaxAmount || request.isPostQuote; - const accountSupports7702 = await messenger.call( - 'KeyringController:accountSupports7702', - from, - ); - const useExecute = - accountSupports7702 && + fullRequest.accountSupports7702 && isRelayExecuteEnabled(messenger) && isEIP7702Chain(messenger, sourceChainId); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index 495999f5c04..d8ceda214f9 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -4,6 +4,7 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; + import { getMessengerMock } from '../../tests/messenger-mock'; import type { PayStrategyExecuteRequest, @@ -25,7 +26,7 @@ import { } from '../../utils/transaction'; import { RELAY_STATUS_URL } from './constants'; import { submitRelayQuotes } from './relay-submit'; -import type { RelayQuote } from './types'; +import type { RelayQuote, RelayTransactionStep } from './types'; jest.mock('../../utils/token'); jest.mock('../../utils/transaction'); @@ -51,16 +52,18 @@ const TRANSACTION_META_MOCK = { hash: TRANSACTION_HASH_MOCK, } as TransactionMeta; -const ORIGINAL_QUOTE_MOCK = { +const ORIGINAL_QUOTE_MOCK: RelayQuote & { steps: RelayTransactionStep[] } = { details: { currencyIn: { currency: { chainId: 1, + decimals: 6, }, }, currencyOut: { currency: { chainId: 2, + decimals: 6, }, }, }, @@ -68,7 +71,17 @@ const ORIGINAL_QUOTE_MOCK = { gasLimits: [21000, 21000], is7702: false, }, - request: {}, + request: { + amount: '1', + destinationChainId: 2, + destinationCurrency: '0xdef' as Hex, + originChainId: 1, + originCurrency: '0xabc' as Hex, + recipient: FROM_MOCK, + slippageTolerance: '100', + tradeType: 'EXPECTED_OUTPUT', + user: FROM_MOCK, + }, steps: [ { id: 'swap', @@ -76,6 +89,10 @@ const ORIGINAL_QUOTE_MOCK = { requestId: REQUEST_ID_MOCK, items: [ { + check: { + endpoint: '/test', + method: 'GET', + }, data: { chainId: 1, data: '0x1234' as Hex, @@ -91,7 +108,7 @@ const ORIGINAL_QUOTE_MOCK = { ], }, ], -} as RelayQuote; +}; const STATUS_RESPONSE_MOCK = { status: 'success', @@ -102,6 +119,7 @@ const STATUS_RESPONSE_MOCK = { const SOURCE_AMOUNT_RAW_MOCK = '1000000'; const REQUEST_MOCK: PayStrategyExecuteRequest = { + accountSupports7702: true, quotes: [ { fees: { @@ -141,7 +159,6 @@ describe('Relay Submit Utils', () => { const getRelayPollingTimeoutMock = jest.mocked(getRelayPollingTimeout); const { - accountSupports7702Mock, addTransactionMock, addTransactionBatchMock, getDelegationTransactionMock, @@ -158,7 +175,6 @@ describe('Relay Submit Utils', () => { beforeEach(() => { jest.resetAllMocks(); - accountSupports7702Mock.mockResolvedValue(true); getRelayPollingIntervalMock.mockReturnValue(1); getRelayPollingTimeoutMock.mockReturnValue(undefined); @@ -451,7 +467,7 @@ describe('Relay Submit Utils', () => { }); it('does not add authorization list when account does not support 7702', async () => { - accountSupports7702Mock.mockResolvedValue(false); + request.accountSupports7702 = false; request.quotes[0].original.details.currencyOut.currency.chainId = 1; request.quotes[0].original.request = { authorizationList: [ @@ -492,7 +508,7 @@ describe('Relay Submit Utils', () => { from: FROM_MOCK, networkClientId: NETWORK_CLIENT_ID_MOCK, origin: ORIGIN_METAMASK, - overwriteUpgrade: true, + overwriteUpgrade: false, requireApproval: false, transactions: [ { @@ -888,7 +904,7 @@ describe('Relay Submit Utils', () => { gasFeeToken: undefined, networkClientId: NETWORK_CLIENT_ID_MOCK, origin: ORIGIN_METAMASK, - overwriteUpgrade: true, + overwriteUpgrade: false, requireApproval: false, transactions: [ { @@ -1083,8 +1099,8 @@ describe('Relay Submit Utils', () => { ); }); - it('submits individually when account does not support 7702', async () => { - accountSupports7702Mock.mockResolvedValue(false); + it('submits batch sequentially when account does not support 7702', async () => { + request.accountSupports7702 = false; request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], @@ -1095,12 +1111,18 @@ describe('Relay Submit Utils', () => { await submitRelayQuotes(request); - expect(addTransactionBatchMock).not.toHaveBeenCalled(); - expect(addTransactionMock).toHaveBeenCalledTimes(2); + expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + disable7702: true, + disableSequential: false, + }), + ); + expect(addTransactionMock).not.toHaveBeenCalled(); }); - it('falls back to first gas limit when entry is missing for individual submission', async () => { - accountSupports7702Mock.mockResolvedValue(false); + it('omits per-transaction gas when entry is missing in batch submission', async () => { + request.accountSupports7702 = false; request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], @@ -1110,19 +1132,26 @@ describe('Relay Submit Utils', () => { await submitRelayQuotes(request); - expect(addTransactionBatchMock).not.toHaveBeenCalled(); - expect(addTransactionMock).toHaveBeenCalledTimes(2); - expect(addTransactionMock).toHaveBeenNthCalledWith( - 2, + expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ - gas: '0x5208', + transactions: [ + expect.objectContaining({ + params: expect.objectContaining({ + gas: '0x5208', + }), + }), + expect.objectContaining({ + params: expect.objectContaining({ + gas: undefined, + }), + }), + ], }), - expect.anything(), ); }); - it('skips on-chain confirmation wait for non-7702 accounts with multiple transactions', async () => { - accountSupports7702Mock.mockResolvedValue(false); + it('waits for on-chain confirmation for non-7702 accounts with multiple transactions', async () => { + request.accountSupports7702 = false; request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], @@ -1132,12 +1161,12 @@ describe('Relay Submit Utils', () => { await submitRelayQuotes(request); - expect(addTransactionMock).toHaveBeenCalledTimes(2); - expect(waitForTransactionConfirmedMock).not.toHaveBeenCalled(); + expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); + expect(waitForTransactionConfirmedMock).toHaveBeenCalled(); }); - it('awaits all transaction results before returning for non-7702 accounts', async () => { - accountSupports7702Mock.mockResolvedValue(false); + it('uses addTransactionBatch for non-7702 accounts with multiple transactions', async () => { + request.accountSupports7702 = false; request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], @@ -1145,27 +1174,14 @@ describe('Relay Submit Utils', () => { request.quotes[0].original.metamask.gasLimits = [21000, 22000]; - const resultPromise1 = Promise.resolve('0xhash1'); - const resultPromise2 = Promise.resolve('0xhash2'); - - addTransactionMock - .mockResolvedValueOnce({ - result: resultPromise1, - transactionMeta: TRANSACTION_META_MOCK, - }) - .mockResolvedValueOnce({ - result: resultPromise2, - transactionMeta: TRANSACTION_META_MOCK, - }); - await submitRelayQuotes(request); - expect(await resultPromise1).toBe('0xhash1'); - expect(await resultPromise2).toBe('0xhash2'); + expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); + expect(addTransactionMock).not.toHaveBeenCalled(); }); it('still waits for on-chain confirmation for 7702 accounts with multiple transactions', async () => { - accountSupports7702Mock.mockResolvedValue(true); + request.accountSupports7702 = true; request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], @@ -1177,7 +1193,7 @@ describe('Relay Submit Utils', () => { }); it('still waits for on-chain confirmation for non-7702 accounts with single transaction', async () => { - accountSupports7702Mock.mockResolvedValue(false); + request.accountSupports7702 = false; await submitRelayQuotes(request); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index bf4426e891f..bf14ab54570 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -61,6 +61,7 @@ export async function submitRelayQuotes( log('Executing quotes', request); const { quotes, messenger, transaction } = request; + const { accountSupports7702 } = request; let transactionHash: Hex | undefined; @@ -69,6 +70,7 @@ export async function submitRelayQuotes( quote, messenger, transaction, + accountSupports7702, )); } @@ -87,6 +89,7 @@ async function executeSingleQuote( quote: TransactionPayQuote, messenger: TransactionPayControllerMessenger, transaction: TransactionMeta, + accountSupports7702: boolean, ): Promise<{ transactionHash?: Hex }> { log('Executing single quote', quote); @@ -104,7 +107,7 @@ async function executeSingleQuote( if (quote.request.isHyperliquidSource) { await submitHyperliquidWithdraw(quote, quote.request.from, messenger); } else { - await submitTransactions(quote, transaction, messenger); + await submitTransactions(quote, transaction, messenger, accountSupports7702); } const targetHash = await waitForRelayCompletion( @@ -314,6 +317,7 @@ async function submitTransactions( quote: TransactionPayQuote, transaction: TransactionMeta, messenger: TransactionPayControllerMessenger, + accountSupports7702: boolean, ): Promise { const { steps } = quote.original; const txSteps = steps.filter( @@ -371,6 +375,7 @@ async function submitTransactions( quote, transaction, messenger, + accountSupports7702, normalizedParams, allParams, ); @@ -478,6 +483,7 @@ async function submitViaTransactionController( quote: TransactionPayQuote, transaction: TransactionMeta, messenger: TransactionPayControllerMessenger, + accountSupports7702: boolean, normalizedParams: TransactionParams[], allParams: TransactionParams[], ): Promise { @@ -485,11 +491,6 @@ async function submitViaTransactionController( const { from, sourceChainId, sourceTokenAddress } = quote.request; const { isPostQuote } = quote.request; - const accountSupports7702 = await messenger.call( - 'KeyringController:accountSupports7702', - from, - ); - const networkClientId = messenger.call( 'NetworkController:findNetworkClientIdByChainId', sourceChainId, @@ -552,15 +553,14 @@ async function submitViaTransactionController( const { metamask } = quote.original; const { gasLimits } = metamask; - const results: { result: Promise }[] = []; + const is7702 = metamask.is7702 && accountSupports7702; - if (allParams.length === 1 || !accountSupports7702) { - for (let i = 0; i < allParams.length; i++) { - const transactionParams = { - ...allParams[i], - ...(i === 0 ? { authorizationList } : {}), - gas: toHex(gasLimits[i] ?? gasLimits[0]), - }; + if (allParams.length === 1) { + const transactionParams = { + ...allParams[0], + authorizationList, + gas: toHex(gasLimits[0]), + }; result = await messenger.call( 'TransactionController:addTransaction', @@ -574,9 +574,7 @@ async function submitViaTransactionController( }, ); } else { - const gasLimit7702 = metamask.is7702 - ? toHex(metamask.gasLimits[0]) - : undefined; + const gasLimit7702 = is7702 ? toHex(metamask.gasLimits[0]) : undefined; const transactions = allParams.map((singleParams, index) => { const gasLimit = gasLimits[index]; @@ -604,13 +602,13 @@ async function submitViaTransactionController( await messenger.call('TransactionController:addTransactionBatch', { from, disable7702: !gasLimit7702, - disableHook: Boolean(gasLimit7702), + disableHook: !gasLimit7702, disableSequential: Boolean(gasLimit7702), gasFeeToken, gasLimit7702, networkClientId, origin: ORIGIN_METAMASK, - overwriteUpgrade: true, + overwriteUpgrade: is7702, requireApproval: false, transactions, }); @@ -620,14 +618,6 @@ async function submitViaTransactionController( log('Added transactions', transactionIds); - if (!accountSupports7702 && allParams.length > 1) { - log( - 'Hardware wallet transactions signed and broadcast, skipping on-chain confirmation wait', - ); - await Promise.all(results.map((res) => res.result)); - return undefined as unknown as Hex; - } - if (result) { const txHash = await result.result; log('Submitted transaction', txHash); diff --git a/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts b/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts index 0cd4996615f..33f9f5ac247 100644 --- a/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts +++ b/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts @@ -18,6 +18,7 @@ describe('TestStrategy', () => { describe('getQuotes', () => { it('returns quote', async () => { const quotesPromise = new TestStrategy().getQuotes({ + accountSupports7702: true, messenger: {} as TransactionPayControllerMessenger, requests: [REQUEST_MOCK], transaction: TRANSACTION_META_MOCK, @@ -83,6 +84,7 @@ describe('TestStrategy', () => { describe('execute', () => { it('resolves', async () => { const executePromise = new TestStrategy().execute({ + accountSupports7702: true, isSmartTransaction: () => false, messenger: {} as TransactionPayControllerMessenger, quotes: [QUOTE_MOCK], diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index a7c3768fc10..55ca446cc86 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -6,7 +6,6 @@ import type { BridgeStatusControllerGetStateAction, BridgeStatusControllerSubmitTxAction, } from '@metamask/bridge-status-controller'; -import type { KeyringControllerAccountSupports7702Action } from '@metamask/keyring-controller'; import type { MessengerActions, MessengerEvents, @@ -37,6 +36,68 @@ type AllActions = MessengerActions; type AllEvents = MessengerEvents; type RootMessenger = Messenger; +type MessengerMockResult = { + addTransactionBatchMock: jest.MockedFn< + TransactionControllerAddTransactionBatchAction['handler'] + >; + addTransactionMock: jest.MockedFn< + TransactionControllerAddTransactionAction['handler'] + >; + estimateGasBatchMock: jest.MockedFn< + TransactionControllerEstimateGasBatchAction['handler'] + >; + estimateGasMock: jest.MockedFn; + fetchQuotesMock: jest.Mock; + findNetworkClientIdByChainIdMock: jest.MockedFn< + NetworkControllerFindNetworkClientIdByChainIdAction['handler'] + >; + getAccountTrackerControllerStateMock: jest.MockedFn< + AccountTrackerControllerGetStateAction['handler'] + >; + getAssetsControllerStateMock: jest.Mock; + getBridgeStatusControllerStateMock: jest.MockedFn< + BridgeStatusControllerGetStateAction['handler'] + >; + getControllerStateMock: jest.MockedFn< + TransactionPayControllerGetStateAction['handler'] + >; + getCurrencyRateControllerStateMock: jest.Mock; + getDelegationTransactionMock: jest.MockedFn< + TransactionPayControllerGetDelegationTransactionAction['handler'] + >; + getGasFeeControllerStateMock: jest.Mock; + getGasFeeTokensMock: jest.MockedFn< + TransactionControllerGetGasFeeTokensAction['handler'] + >; + getNetworkClientByIdMock: jest.MockedFn< + NetworkControllerGetNetworkClientByIdAction['handler'] + >; + getRemoteFeatureFlagControllerStateMock: jest.MockedFn< + RemoteFeatureFlagControllerGetStateAction['handler'] + >; + getStrategyMock: jest.MockedFn; + getTokenBalanceControllerStateMock: jest.MockedFn< + TokenBalancesControllerGetStateAction['handler'] + >; + getTokenRatesControllerStateMock: jest.MockedFn< + TokenRatesControllerGetStateAction['handler'] + >; + getTokensControllerStateMock: jest.MockedFn< + TokensControllerGetStateAction['handler'] + >; + getTransactionControllerStateMock: jest.MockedFn< + TransactionControllerGetStateAction['handler'] + >; + messenger: TransactionPayControllerMessenger; + publish: RootMessenger['publish']; + submitTransactionMock: jest.MockedFunction< + BridgeStatusControllerSubmitTxAction['handler'] + >; + updateTransactionMock: jest.MockedFn< + TransactionControllerUpdateTransactionAction['handler'] + >; +}; + /** * Creates a mock controller messenger for testing. * @@ -47,7 +108,7 @@ type RootMessenger = Messenger; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function getMessengerMock({ skipRegister, -}: { skipRegister?: boolean } = {}) { +}: { skipRegister?: boolean } = {}): MessengerMockResult { const getControllerStateMock: jest.MockedFn< TransactionPayControllerGetStateAction['handler'] > = jest.fn(); @@ -130,10 +191,6 @@ export function getMessengerMock({ TransactionControllerEstimateGasBatchAction['handler'] > = jest.fn(); - const accountSupports7702Mock: jest.MockedFn< - KeyringControllerAccountSupports7702Action['handler'] - > = jest.fn(); - const getAssetsControllerStateMock = jest.fn(); const messenger: RootMessenger = new Messenger({ @@ -256,16 +313,11 @@ export function getMessengerMock({ getAssetsControllerStateMock, ); - messenger.registerActionHandler( - 'KeyringController:accountSupports7702', - accountSupports7702Mock, - ); } const publish = messenger.publish.bind(messenger); return { - accountSupports7702Mock, addTransactionMock, getAssetsControllerStateMock, addTransactionBatchMock, diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index f2052a4fae2..b9201e40116 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -12,10 +12,7 @@ import type { BridgeControllerActions } from '@metamask/bridge-controller'; import type { BridgeStatusControllerStateChangeEvent } from '@metamask/bridge-status-controller'; import type { BridgeStatusControllerActions } from '@metamask/bridge-status-controller'; import type { GasFeeControllerActions } from '@metamask/gas-fee-controller'; -import type { - KeyringControllerAccountSupports7702Action, - KeyringControllerSignTypedMessageAction, -} from '@metamask/keyring-controller'; +import type { KeyringControllerSignTypedMessageAction } from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; import type { NetworkControllerFindNetworkClientIdByChainIdAction } from '@metamask/network-controller'; import type { NetworkControllerGetNetworkClientByIdAction } from '@metamask/network-controller'; @@ -50,7 +47,6 @@ export type AllowedActions = | BridgeStatusControllerActions | CurrencyRateControllerActions | GasFeeControllerActions - | KeyringControllerAccountSupports7702Action | KeyringControllerSignTypedMessageAction | NetworkControllerFindNetworkClientIdByChainIdAction | NetworkControllerGetNetworkClientByIdAction @@ -140,8 +136,16 @@ export type TransactionPayControllerMessenger = Messenger< TransactionPayControllerEvents | AllowedEvents >; +/** Callback to check whether an account supports EIP-7702 authorization signing. */ +export type AccountSupports7702Callback = ( + account: string, +) => Promise; + /** Options for the TransactionPayController. */ export type TransactionPayControllerOptions = { + /** Callback to check whether an account supports EIP-7702. */ + accountSupports7702: AccountSupports7702Callback; + /** Callback to convert a transaction into a redeem delegation. */ getDelegationTransaction: GetDelegationTransactionCallback; @@ -442,6 +446,9 @@ export type TransactionPayQuote = { /** Request to get quotes for a transaction. */ export type PayStrategyGetQuotesRequest = { + /** Whether the account supports EIP-7702 authorization signing. */ + accountSupports7702: boolean; + /** Selected fiat payment method ID, if applicable. */ fiatPaymentMethod?: string; @@ -457,6 +464,9 @@ export type PayStrategyGetQuotesRequest = { /** Request to submit quotes for a transaction. */ export type PayStrategyExecuteRequest = { + /** Whether the account supports EIP-7702 authorization signing. */ + accountSupports7702: boolean; + /** Callback to determine if the transaction is a smart transaction. */ isSmartTransaction: (chainId: Hex) => boolean; diff --git a/packages/transaction-pay-controller/src/utils/quote-gas.ts b/packages/transaction-pay-controller/src/utils/quote-gas.ts index 25c2823c1d3..3b1f69d1722 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.ts @@ -26,11 +26,13 @@ export type QuoteGasLimit = { }; export async function estimateQuoteGasLimits({ + accountSupports7702 = true, fallbackGas, fallbackOnSimulationFailure = false, messenger, transactions, }: { + accountSupports7702?: boolean; fallbackGas?: { estimate: number; max: number; @@ -54,8 +56,43 @@ export async function estimateQuoteGasLimits({ const useBatch = transactions.length > 1; if (useBatch) { + const result = await estimateQuoteGasLimitsBatch( + transactions, + messenger, + ); + + // If the batch returned a combined 7702 gas limit but the account cannot + // sign EIP-7702 authorizations (e.g. hardware wallet), re-estimate each + // transaction individually so callers receive per-transaction gas limits. + if (result.is7702 && !accountSupports7702) { + const individualResults = await Promise.all( + transactions.map((transaction) => + estimateQuoteGasLimitSingle({ + fallbackGas, + fallbackOnSimulationFailure, + messenger, + transaction, + }), + ), + ); + + return { + gasLimits: individualResults.map((r) => r.gasLimits[0]), + is7702: false, + totalGasEstimate: individualResults.reduce( + (acc, r) => acc + r.totalGasEstimate, + 0, + ), + totalGasLimit: individualResults.reduce( + (acc, r) => acc + r.totalGasLimit, + 0, + ), + usedBatch: true, + }; + } + return { - ...(await estimateQuoteGasLimitsBatch(transactions, messenger)), + ...result, usedBatch: true, }; } diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 2683b7115bd..ad7a1433d5c 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -7,6 +7,7 @@ import { cloneDeep } from 'lodash'; import { TransactionPayStrategy } from '../constants'; import { getMessengerMock } from '../tests/messenger-mock'; import type { + AccountSupports7702Callback, TransactionPaySourceAmount, TransactionData, TransactionPayQuote, @@ -101,8 +102,10 @@ const BATCH_TRANSACTION_MOCK = { } as BatchTransaction; describe('Quotes Utils', () => { - const { messenger, getControllerStateMock, accountSupports7702Mock } = - getMessengerMock(); + const { messenger, getControllerStateMock } = getMessengerMock(); + const accountSupports7702Mock: jest.MockedFunction< + AccountSupports7702Callback + > = jest.fn(); const updateTransactionDataMock = jest.fn(); const getStrategyByNameMock = jest.mocked(getStrategyByName); const getStrategiesByNameMock = jest.mocked(getStrategiesByName); @@ -125,6 +128,7 @@ describe('Quotes Utils', () => { */ async function run(params?: Partial): Promise { return await updateQuotes({ + accountSupports7702: accountSupports7702Mock, getStrategies: getStrategiesMock, messenger, transactionData: cloneDeep(TRANSACTION_DATA_MOCK), @@ -582,6 +586,7 @@ describe('Quotes Utils', () => { await run(); expect(getQuotesMock).toHaveBeenCalledWith({ + accountSupports7702: true, messenger, requests: [ { @@ -624,6 +629,7 @@ describe('Quotes Utils', () => { }); expect(getQuotesMock).toHaveBeenCalledWith({ + accountSupports7702: true, messenger, requests: [ expect.objectContaining({ @@ -859,6 +865,7 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, + accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(4); @@ -888,6 +895,7 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, + accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(4); @@ -918,6 +926,7 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, + accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(0); @@ -939,6 +948,7 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, + accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(0); @@ -991,6 +1001,7 @@ describe('Quotes Utils', () => { }); expect(getQuotesMock).toHaveBeenCalledWith({ + accountSupports7702: true, messenger, requests: [ { diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index 6e355ac69d0..7c219ba496e 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -7,6 +7,7 @@ import { createModuleLogger } from '@metamask/utils'; import { TransactionPayStrategy } from '../constants'; import { projectLogger } from '../logger'; import type { + AccountSupports7702Callback, QuoteRequest, TransactionData, TransactionPayControllerMessenger, @@ -36,6 +37,7 @@ const DEFAULT_REFRESH_INTERVAL = 30 * 1000; // 30 Seconds const log = createModuleLogger(projectLogger, 'quotes'); export type UpdateQuotesRequest = { + accountSupports7702: AccountSupports7702Callback; getStrategies: (transaction: TransactionMeta) => TransactionPayStrategy[]; messenger: TransactionPayControllerMessenger; transactionData: TransactionData | undefined; @@ -53,6 +55,7 @@ export async function updateQuotes( request: UpdateQuotesRequest, ): Promise { const { + accountSupports7702, getStrategies, messenger, transactionData, @@ -110,9 +113,12 @@ export async function updateQuotes( transactionId, }); + const supports7702 = await accountSupports7702(from); + const { batchTransactions, quotes } = await getQuotes( transaction, requests, + supports7702, getStrategies, messenger, transactionData.fiatPayment?.selectedPaymentMethodId, @@ -128,13 +134,8 @@ export async function updateQuotes( log('Calculated totals', { transactionId, totals }); - const accountSupports7702 = await messenger.call( - 'KeyringController:accountSupports7702', - from, - ); - syncTransaction({ - batchTransactions: accountSupports7702 ? batchTransactions : [], + batchTransactions: supports7702 ? batchTransactions : undefined, isPostQuote, messenger: messenger as never, paymentToken, @@ -175,7 +176,7 @@ function syncTransaction({ totals, transactionId, }: { - batchTransactions: BatchTransaction[]; + batchTransactions: BatchTransaction[] | undefined; isPostQuote?: boolean; messenger: TransactionPayControllerMessenger; paymentToken: TransactionPaymentToken | undefined; @@ -194,7 +195,9 @@ function syncTransaction({ }, (tx: TransactionMeta) => { tx.batchTransactions = batchTransactions; - tx.batchTransactionsOptions = {}; + tx.batchTransactionsOptions = batchTransactions?.length + ? {} + : undefined; tx.metamaskPay = { bridgeFeeFiat: totals.fees.provider.usd, @@ -215,11 +218,13 @@ function syncTransaction({ * @param messenger - Messenger instance. * @param updateTransactionData - Callback to update transaction data. * @param getStrategies - Callback to get ordered strategy names for a transaction. + * @param accountSupports7702 - Callback to check account EIP-7702 support. */ export async function refreshQuotes( messenger: TransactionPayControllerMessenger, updateTransactionData: UpdateTransactionDataCallback, getStrategies: (transaction: TransactionMeta) => TransactionPayStrategy[], + accountSupports7702: AccountSupports7702Callback, ): Promise { const state = messenger.call('TransactionPayController:getState'); const transactionIds = Object.keys(state.transactionData); @@ -248,6 +253,7 @@ export async function refreshQuotes( } const isUpdated = await updateQuotes({ + accountSupports7702, getStrategies, messenger, transactionData, @@ -480,6 +486,7 @@ async function refreshPaymentTokenBalance({ * * @param transaction - Transaction metadata. * @param requests - Quote requests. + * @param accountSupports7702 - Whether the account supports EIP-7702. * @param getStrategies - Callback to get ordered strategy names for a transaction. * @param messenger - Controller messenger. * @param fiatPaymentMethod - Selected fiat payment method ID, if applicable. @@ -488,6 +495,7 @@ async function refreshPaymentTokenBalance({ async function getQuotes( transaction: TransactionMeta, requests: QuoteRequest[], + accountSupports7702: boolean, getStrategies: (transaction: TransactionMeta) => TransactionPayStrategy[], messenger: TransactionPayControllerMessenger, fiatPaymentMethod?: string, @@ -514,6 +522,7 @@ async function getQuotes( } const request = { + accountSupports7702, fiatPaymentMethod, messenger, requests, From 900b3224aba2e2bc512bab2ecd4bbb22abe6fbce Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 12:33:22 +0200 Subject: [PATCH 13/48] Revert KC --- packages/keyring-controller/src/KeyringController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 65dfbd1aa14..3fde1f3ef60 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -2215,7 +2215,7 @@ export class KeyringController< } /** - * Constructor helper for registering this controller's messenger + * Constructor helper for registering this controller's messeger * actions. */ #registerMessageHandlers(): void { From 1f6fb3847887d9b9a72856aa57702fc0628766c6 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 12:45:55 +0200 Subject: [PATCH 14/48] Update --- .../src/hooks/SequentialPublishBatchHook.ts | 5 ++++- .../src/strategy/relay/relay-quotes.ts | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/transaction-controller/src/hooks/SequentialPublishBatchHook.ts b/packages/transaction-controller/src/hooks/SequentialPublishBatchHook.ts index ee703029558..844b9d2344e 100644 --- a/packages/transaction-controller/src/hooks/SequentialPublishBatchHook.ts +++ b/packages/transaction-controller/src/hooks/SequentialPublishBatchHook.ts @@ -97,7 +97,10 @@ export class SequentialPublishBatchHook { } catch (error) { log('Batch transaction failed', { transaction, error }); pendingTransactionTracker.stop(); - throw rpcErrors.internal(`Failed to publish batch transaction`); + const reason = error instanceof Error ? error.message : String(error); + throw rpcErrors.internal( + `Failed to publish batch transaction: ${reason}`, + ); } } diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 65bee520764..1dc9e74b1b4 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -454,6 +454,7 @@ async function normalizeQuote( messenger, request, fullRequest.transaction, + fullRequest.accountSupports7702, ); const targetNetwork = { @@ -589,6 +590,7 @@ function getFiatRates( * @param messenger - Controller messenger. * @param request - Quote request. * @param transaction - Original transaction metadata. + * @param accountSupports7702 - Whether the account supports EIP-7702. * @returns Total source network cost in USD and fiat. */ async function calculateSourceNetworkCost( @@ -596,6 +598,7 @@ async function calculateSourceNetworkCost( messenger: TransactionPayControllerMessenger, request: QuoteRequest, transaction: TransactionMeta, + accountSupports7702: boolean, ): Promise< TransactionPayQuote['fees']['sourceNetwork'] & { gasLimits: number[]; @@ -652,6 +655,7 @@ async function calculateSourceNetworkCost( relayParams, messenger, fromOverride, + accountSupports7702, ); const { gasLimits, is7702, totalGasEstimate, totalGasLimit } = @@ -803,12 +807,14 @@ async function calculateSourceNetworkCost( * @param fromOverride - Optional address to use as `from` in gas estimation * instead of the address in the relay params. Used in predict withdraw flows * to estimate with the proxy/Safe address that holds the source token balance. + * @param accountSupports7702 - Whether the account supports EIP-7702. * @returns Total gas estimates and per-transaction gas limits. */ async function calculateSourceNetworkGasLimit( params: RelayTransactionStep['items'][0]['data'][], messenger: TransactionPayControllerMessenger, - fromOverride?: Hex, + fromOverride: Hex | undefined, + accountSupports7702: boolean, ): Promise<{ totalGasEstimate: number; totalGasLimit: number; @@ -820,6 +826,7 @@ async function calculateSourceNetworkGasLimit( ); const relayGasResult = await estimateQuoteGasLimits({ + accountSupports7702, fallbackGas: getFeatureFlags(messenger).relayFallbackGas, fallbackOnSimulationFailure: true, messenger, From 3fce28beffb47dbc34175b6f2eff3c709f92b04a Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:04:38 +0200 Subject: [PATCH 15/48] Revert KC changes --- .../KeyringController-method-action-types.ts | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController-method-action-types.ts b/packages/keyring-controller/src/KeyringController-method-action-types.ts index 9ba9b0378b5..e5799d22cb0 100644 --- a/packages/keyring-controller/src/KeyringController-method-action-types.ts +++ b/packages/keyring-controller/src/KeyringController-method-action-types.ts @@ -321,7 +321,25 @@ export type KeyringControllerWithKeyringUnsafeAction = { }; /** - * {@inheritDoc KeyringController.withKeyringV2} + * Select a keyring using its `KeyringV2` adapter, and execute + * the given operation with the wrapped keyring as a mutually + * exclusive atomic operation. + * + * The cached `KeyringV2` adapter is retrieved from the keyring + * entry. + * + * A `KeyringV2Builder` for the selected keyring's type must exist + * (either as a default or registered via the `keyringV2Builders` + * constructor option); otherwise an error is thrown. + * + * The method automatically persists changes at the end of the + * function execution, or rolls back the changes if an error + * is thrown. + * + * @param selector - Keyring selector object. + * @param operation - Function to execute with the wrapped V2 keyring. + * @returns Promise resolving to the result of the function execution. + * @template CallbackResult - The type of the value resolved by the callback function. */ export type KeyringControllerWithKeyringV2Action = { type: `KeyringController:withKeyringV2`; @@ -329,7 +347,38 @@ export type KeyringControllerWithKeyringV2Action = { }; /** - * {@inheritDoc KeyringController.withKeyringV2Unsafe} + * Select a keyring, wrap it in a `KeyringV2` adapter, and execute + * the given read-only operation **without** acquiring the controller's + * mutual exclusion lock. + * + * ## When to use this method + * + * This method is an escape hatch for read-only access to keyring data that + * is immutable once the keyring is initialized. A typical safe use case is + * reading immutable fields from a `KeyringV2` adapter: data that is set + * during initialization and never mutated afterwards. + * + * ## Why it is "unsafe" + * + * The "unsafe" designation mirrors the semantics of `unsafe { }` blocks in + * Rust: the method itself does not enforce thread-safety guarantees. By + * calling this method the **caller** explicitly takes responsibility for + * ensuring that: + * + * - The operation is **read-only** — no state is mutated. + * - The data being read is **immutable** after the keyring is initialized, + * so concurrent locked operations cannot alter it while this callback + * runs. + * + * Do **not** use this method to: + * - Mutate keyring state (add accounts, sign, etc.) — use `withKeyringV2`. + * - Read mutable fields that could change during concurrent operations. + * + * @param selector - Keyring selector object. + * @param operation - Read-only function to execute with the wrapped V2 keyring. + * @returns Promise resolving to the result of the function execution. + * @template SelectedKeyring - The type of the selected V2 keyring. + * @template CallbackResult - The type of the value resolved by the callback function. */ export type KeyringControllerWithKeyringV2UnsafeAction = { type: `KeyringController:withKeyringV2Unsafe`; From f7e84214a28886c4b4b6f4d529b84312691cd06d Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:05:30 +0200 Subject: [PATCH 16/48] Revert TC changes --- .../src/hooks/SequentialPublishBatchHook.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/transaction-controller/src/hooks/SequentialPublishBatchHook.ts b/packages/transaction-controller/src/hooks/SequentialPublishBatchHook.ts index 844b9d2344e..ee703029558 100644 --- a/packages/transaction-controller/src/hooks/SequentialPublishBatchHook.ts +++ b/packages/transaction-controller/src/hooks/SequentialPublishBatchHook.ts @@ -97,10 +97,7 @@ export class SequentialPublishBatchHook { } catch (error) { log('Batch transaction failed', { transaction, error }); pendingTransactionTracker.stop(); - const reason = error instanceof Error ? error.message : String(error); - throw rpcErrors.internal( - `Failed to publish batch transaction: ${reason}`, - ); + throw rpcErrors.internal(`Failed to publish batch transaction`); } } From bcb78707f98de286d3e2b287c0344f2f78c9b431 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:10:12 +0200 Subject: [PATCH 17/48] Fix lint --- .../src/strategy/across/across-quotes.test.ts | 792 +++++---- .../src/strategy/across/across-submit.test.ts | 338 ++-- .../src/strategy/relay/relay-quotes.test.ts | 1568 ++++++++++------- .../src/strategy/relay/relay-submit.test.ts | 1 - .../src/strategy/relay/relay-submit.ts | 7 +- .../src/tests/messenger-mock.ts | 9 +- .../transaction-pay-controller/src/types.ts | 4 +- .../src/utils/quote-gas.ts | 5 +- .../src/utils/quotes.test.ts | 5 +- .../src/utils/quotes.ts | 4 +- 10 files changed, 1641 insertions(+), 1092 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts index 3b83924acc8..7e376cbb795 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts @@ -216,9 +216,12 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result).toHaveLength(1); expect(result[0].strategy).toBe(TransactionPayStrategy.Across); @@ -227,28 +230,34 @@ describe('Across Quotes', () => { }); it('filters out requests with zero target amount', async () => { - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - }, - ], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + }, + ], + transaction: TRANSACTION_META_MOCK, + }); expect(result).toStrictEqual([]); expect(successfulFetchMock).not.toHaveBeenCalled(); }); it('filters out non-max requests with missing target amount', async () => { - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: undefined, - } as unknown as QuoteRequest, - ], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: undefined, + } as unknown as QuoteRequest, + ], + transaction: TRANSACTION_META_MOCK, + }); expect(result).toStrictEqual([]); expect(successfulFetchMock).not.toHaveBeenCalled(); @@ -258,9 +267,12 @@ describe('Across Quotes', () => { successfulFetchMock.mockRejectedValue(new Error('Network error')); await expect( - getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }), + getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }), ).rejects.toThrow(/Failed to fetch Across quotes/u); }); @@ -269,9 +281,12 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], - transaction: TRANSACTION_META_MOCK, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], + transaction: TRANSACTION_META_MOCK, + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -285,9 +300,12 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -303,9 +321,12 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -337,9 +358,12 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -354,9 +378,12 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); const [, options] = successfulFetchMock.mock.calls[0]; const body = getRequestBody(); @@ -376,24 +403,27 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '1000000', - targetChainId: CHAIN_ID_ARBITRUM, - targetTokenAddress: ARBITRUM_USDC_ADDRESS, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - type: TransactionType.perpsDeposit, - txParams: { - from: FROM_MOCK, - to: ARBITRUM_USDC_ADDRESS, - data: buildTransferData(TRANSFER_RECIPIENT, 1), - }, - } as TransactionMeta, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '1000000', + targetChainId: CHAIN_ID_ARBITRUM, + targetTokenAddress: ARBITRUM_USDC_ADDRESS, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.perpsDeposit, + txParams: { + from: FROM_MOCK, + to: ARBITRUM_USDC_ADDRESS, + data: buildTransferData(TRANSFER_RECIPIENT, 1), + }, + } as TransactionMeta, + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -416,15 +446,18 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - from: FROM_MOCK, - data: transferData, + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + from: FROM_MOCK, + data: transferData, + }, }, - }, }); + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -440,12 +473,15 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [{ data: transferData }], - } as TransactionMeta, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [{ data: transferData }], + } as TransactionMeta, + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -461,16 +497,19 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - type: TransactionType.predictDeposit, - txParams: { - from: FROM_MOCK, - data: transferData, + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.predictDeposit, + txParams: { + from: FROM_MOCK, + data: transferData, + }, }, - }, }); + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -486,13 +525,16 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - type: TransactionType.predictDeposit, - nestedTransactions: [{ data: transferData }], - } as TransactionMeta, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.predictDeposit, + nestedTransactions: [{ data: transferData }], + } as TransactionMeta, + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -510,16 +552,19 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { to: FACTORY_ADDRESS, data: createProxyData }, - { to: SAFE_ADDRESS, data: execTransactionData }, - { to: QUOTE_REQUEST_MOCK.targetTokenAddress, data: transferData }, - ], - } as TransactionMeta, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { to: FACTORY_ADDRESS, data: createProxyData }, + { to: SAFE_ADDRESS, data: execTransactionData }, + { to: QUOTE_REQUEST_MOCK.targetTokenAddress, data: transferData }, + ], + } as TransactionMeta, + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -611,15 +656,18 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { to: FACTORY_ADDRESS, data: createProxyData }, - { data: transferData }, - ], - } as TransactionMeta, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { to: FACTORY_ADDRESS, data: createProxyData }, + { data: transferData }, + ], + } as TransactionMeta, + }); const body = getRequestBody(); const [url] = successfulFetchMock.mock.calls[0]; @@ -646,21 +694,24 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { - to: QUOTE_REQUEST_MOCK.targetTokenAddress, - data: firstTransferData, - }, - { - to: QUOTE_REQUEST_MOCK.targetTokenAddress, - data: secondTransferData, - }, - ], - } as TransactionMeta, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + to: QUOTE_REQUEST_MOCK.targetTokenAddress, + data: firstTransferData, + }, + { + to: QUOTE_REQUEST_MOCK.targetTokenAddress, + data: secondTransferData, + }, + ], + } as TransactionMeta, + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -694,9 +745,12 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -712,16 +766,19 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - from: FROM_MOCK, - to: FACTORY_ADDRESS, - data: createProxyData, + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + from: FROM_MOCK, + to: FACTORY_ADDRESS, + data: createProxyData, + }, }, - }, }); + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -762,17 +819,20 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { - to: FACTORY_ADDRESS, - data: '0xdeadbeef' as Hex, - }, - ], - } as TransactionMeta, }), + getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + to: FACTORY_ADDRESS, + data: '0xdeadbeef' as Hex, + }, + ], + } as TransactionMeta, + }), ).rejects.toThrow(/Destination selector: 0xdeadbeef/u); }); @@ -784,12 +844,15 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [{ data: createProxyData }], - } as TransactionMeta, }), + getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [{ data: createProxyData }], + } as TransactionMeta, + }), ).rejects.toThrow(/Across only supports direct token transfers/u); }); @@ -801,41 +864,50 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [{ data: execTransactionData }], - } as TransactionMeta, }), + getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [{ data: execTransactionData }], + } as TransactionMeta, + }), ).rejects.toThrow(/Across only supports direct token transfers/u); }); it('throws when destination flow is not transfer-style', async () => { await expect( - getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - from: FROM_MOCK, - data: '0xabc' as Hex, + getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + from: FROM_MOCK, + data: '0xabc' as Hex, + }, }, - }, }), + }), ).rejects.toThrow(/Across only supports direct token transfers/u); }); it('throws when txParams include authorization list', async () => { await expect( - getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - from: FROM_MOCK, - data: '0xabc' as Hex, - authorizationList: [{ address: '0xabc' as Hex }], - }, - } as TransactionMeta, }), + getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + from: FROM_MOCK, + data: '0xabc' as Hex, + authorizationList: [{ address: '0xabc' as Hex }], + }, + } as TransactionMeta, + }), ).rejects.toThrow(/Across does not support type-4\/EIP-7702/u); expect(successfulFetchMock).not.toHaveBeenCalled(); @@ -850,9 +922,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(parseFloat(result[0].dust.usd)).toBeGreaterThan(0); }); @@ -868,9 +943,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.provider.usd).toBe('0.5'); expect(result[0].fees.provider.fiat).toBe('1'); @@ -887,9 +965,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.provider.usd).toBe('1.9996'); expect(result[0].fees.provider.fiat).toBe('3.9992'); @@ -908,9 +989,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.provider.usd).toBe('0'); expect(result[0].fees.provider.fiat).toBe('0'); @@ -929,9 +1013,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.provider.usd).toBe('0'); expect(result[0].fees.provider.fiat).toBe('0'); @@ -951,9 +1038,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [request], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [request], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].dust.usd).toBe('0.0004'); }); @@ -966,9 +1056,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].estimatedDuration).toBe(0); }); @@ -983,9 +1076,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.targetNetwork.usd).toBe('0'); }); @@ -998,9 +1094,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].sourceAmount.raw).toBe('0'); }); @@ -1030,9 +1129,12 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result).toHaveLength(1); expect(calculateGasCostMock).toHaveBeenNthCalledWith( @@ -1075,9 +1177,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(calculateGasCostMock).toHaveBeenCalledTimes(6); expect(calculateGasCostMock).toHaveBeenCalledWith( @@ -1135,9 +1240,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(estimateGasBatchMock).toHaveBeenCalledWith({ chainId: '0x1', @@ -1248,9 +1356,12 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }), + getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }), ).rejects.toThrow( 'Failed to fetch Across quotes: Error: Across combined batch gas estimate missing', ); @@ -1278,9 +1389,12 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }), + getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }), ).rejects.toThrow( 'Failed to fetch Across quotes: Error: Batch estimation failed', ); @@ -1300,9 +1414,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(estimateGasMock).not.toHaveBeenCalled(); expect(result[0].original.metamask.gasLimits).toStrictEqual([ @@ -1330,9 +1447,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(estimateGasMock).toHaveBeenCalledTimes(1); expect(result[0].original.metamask.gasLimits).toStrictEqual([ @@ -1395,9 +1515,12 @@ describe('Across Quotes', () => { }), } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(calculateGasCostMock).toHaveBeenNthCalledWith( 1, @@ -1427,9 +1550,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].original.metamask.gasLimits).toStrictEqual([ { @@ -1466,9 +1592,12 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result).toHaveLength(1); }); @@ -1486,9 +1615,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].targetAmount.usd).toBe('0.0003'); expect(result[0].dust.usd).toBe('0'); @@ -1509,9 +1641,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.provider.usd).toBe('0.5'); expect(result[0].fees.provider.fiat).toBe('1'); @@ -1526,9 +1661,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].targetAmount.usd).toBe('0.000246'); }); @@ -1548,9 +1686,12 @@ describe('Across Quotes', () => { }), } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [request], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [request], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].targetAmount.usd).toBe('0'); }); @@ -1560,9 +1701,12 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -1578,19 +1722,22 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { data: transferData }, - { data: '0xbeef' as Hex }, - ], - txParams: { - from: FROM_MOCK, - data: '0xabc' as Hex, - }, - } as TransactionMeta, }), + getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { data: transferData }, + { data: '0xbeef' as Hex }, + ], + txParams: { + from: FROM_MOCK, + data: '0xabc' as Hex, + }, + } as TransactionMeta, + }), ).rejects.toThrow(/Across only supports direct token transfers/u); }); @@ -1600,16 +1747,19 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [{ to: '0xabc' as Hex }], - txParams: { - from: FROM_MOCK, - data: '0xdeadbeef' as Hex, - }, - } as TransactionMeta, }), + getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [{ to: '0xabc' as Hex }], + txParams: { + from: FROM_MOCK, + data: '0xdeadbeef' as Hex, + }, + } as TransactionMeta, + }), ).rejects.toThrow(/Destination selector: 0xdeadbeef/u); }); @@ -1620,9 +1770,12 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -1638,9 +1791,12 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }), + getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }), ).rejects.toThrow(/Failed to fetch Across quotes/u); }); @@ -1656,9 +1812,12 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - const result = await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result).toHaveLength(1); }); @@ -1671,19 +1830,22 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { data: '0xother' as Hex }, - { data: transferData }, - ], - txParams: { - from: FROM_MOCK, - data: '0xnonTransferData' as Hex, - }, - } as TransactionMeta, }), + getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { data: '0xother' as Hex }, + { data: transferData }, + ], + txParams: { + from: FROM_MOCK, + data: '0xnonTransferData' as Hex, + }, + } as TransactionMeta, + }), ).rejects.toThrow(/Across only supports direct token transfers/u); }); @@ -1694,16 +1856,19 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [{ to: '0xabc' as Hex }, { data: transferData }], - txParams: { - from: FROM_MOCK, - data: '0xnonTransferData' as Hex, - }, - } as TransactionMeta, }); + await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [{ to: '0xabc' as Hex }, { data: transferData }], + txParams: { + from: FROM_MOCK, + data: '0xnonTransferData' as Hex, + }, + } as TransactionMeta, + }); const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; @@ -1717,19 +1882,22 @@ describe('Across Quotes', () => { } as Response); await expect( - getAcrossQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { data: '0xdeadbeef' as Hex }, - { data: '0xcafebabe' as Hex }, - ], - txParams: { - from: FROM_MOCK, - data: undefined, - }, - } as TransactionMeta, }), + getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { data: '0xdeadbeef' as Hex }, + { data: '0xcafebabe' as Hex }, + ], + txParams: { + from: FROM_MOCK, + data: undefined, + }, + } as TransactionMeta, + }), ).rejects.toThrow(/Destination selector: 0xdeadbeef/u); }); }); diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts index 0465c27042a..d4d4e61436a 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts @@ -190,10 +190,13 @@ describe('Across Submit', () => { }) as TransactionPayQuote; it('submits a batch when approvals exist', async () => { - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [QUOTE_MOCK], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [QUOTE_MOCK], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); expect(addTransactionBatchMock).toHaveBeenCalledWith( @@ -227,10 +230,13 @@ describe('Across Submit', () => { }, } as unknown as TransactionPayQuote; - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [batchGasQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [batchGasQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -257,7 +263,6 @@ describe('Across Submit', () => { }); it('submits batch sequentially when account does not support 7702', async () => { - const nonIs7702Quote = { ...QUOTE_MOCK, original: { @@ -272,10 +277,13 @@ describe('Across Submit', () => { }, } as unknown as TransactionPayQuote; - await submitAcrossQuotes({ accountSupports7702: false, messenger, - quotes: [nonIs7702Quote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: false, + messenger, + quotes: [nonIs7702Quote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); expect(addTransactionBatchMock).toHaveBeenCalledWith( @@ -299,10 +307,13 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(addTransactionMock).toHaveBeenCalledTimes(1); expect(addTransactionMock).toHaveBeenCalledWith( @@ -326,10 +337,13 @@ describe('Across Submit', () => { } as TransactionPayQuote; await expect( - submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [missingBatchGasQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }), + submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [missingBatchGasQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }), ).rejects.toThrow('Missing quote gas limit for Across 7702 batch'); expect(addTransactionBatchMock).not.toHaveBeenCalled(); @@ -347,13 +361,16 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [noApprovalQuote], - transaction: { - ...TRANSACTION_META_MOCK, - type: TransactionType.predictDeposit, - }, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [noApprovalQuote], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.predictDeposit, + }, + isSmartTransaction: jest.fn(), + }); expect(addTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -375,13 +392,16 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [noApprovalQuote], - transaction: { - ...TRANSACTION_META_MOCK, - type: TransactionType.swap, - }, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [noApprovalQuote], + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.swap, + }, + isSmartTransaction: jest.fn(), + }); expect(addTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -403,13 +423,16 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [noApprovalQuote], - transaction: { - ...TRANSACTION_META_MOCK, - type: undefined, - }, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [noApprovalQuote], + transaction: { + ...TRANSACTION_META_MOCK, + type: undefined, + }, + isSmartTransaction: jest.fn(), + }); expect(addTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -443,10 +466,13 @@ describe('Across Submit', () => { }, ]); - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(addTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -470,10 +496,13 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(updateTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -522,10 +551,13 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - const result = await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + const result = await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(updateTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -575,10 +607,13 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(updateTransactionMock).toHaveBeenCalledWith( expect.anything(), @@ -635,10 +670,13 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - const result = await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + const result = await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(successfulFetchMock).toHaveBeenCalledWith( expect.stringContaining('/deposit/status?depositTxnRef=0xconfirmed'), @@ -742,10 +780,13 @@ describe('Across Submit', () => { } as TransactionPayQuote; await expect( - submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }), + submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }), ).rejects.toThrow('Across request failed with status: failed'); }); @@ -763,10 +804,13 @@ describe('Across Submit', () => { }), } as Response); - const resultPromise = submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [buildDepositQuote()], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + const resultPromise = submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [buildDepositQuote()], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); await jest.runAllTimersAsync(); const result = await resultPromise; @@ -787,10 +831,13 @@ describe('Across Submit', () => { }), } as Response); - const result = await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [buildDepositQuote()], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + const result = await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [buildDepositQuote()], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(result.transactionHash).toBe('0xfill'); }); @@ -823,10 +870,13 @@ describe('Across Submit', () => { }), } as Response); - const result = await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [buildDepositQuote()], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + const result = await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [buildDepositQuote()], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(result.transactionHash).toBe('0xbridge'); }); @@ -839,10 +889,13 @@ describe('Across Submit', () => { }), } as Response); - const result = await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [buildDepositQuote()], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + const result = await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [buildDepositQuote()], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(result.transactionHash).toBe('0xconfirmed'); }); @@ -867,10 +920,13 @@ describe('Across Submit', () => { }), } as Response); - const resultPromise = submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [buildDepositQuote()], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + const resultPromise = submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [buildDepositQuote()], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); await jest.runAllTimersAsync(); const result = await resultPromise; @@ -899,10 +955,13 @@ describe('Across Submit', () => { }), } as Response); - const resultPromise = submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [buildDepositQuote()], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + const resultPromise = submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [buildDepositQuote()], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); await jest.runAllTimersAsync(); const result = await resultPromise; @@ -929,10 +988,13 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); const params = addTransactionMock.mock.calls[0][0] as { gas: Hex }; @@ -956,10 +1018,13 @@ describe('Across Submit', () => { } as unknown as TransactionPayQuote; await expect( - submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [missingSwapGasQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }), + submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [missingSwapGasQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }), ).rejects.toThrow('Missing quote gas limit for Across swap transaction'); expect(addTransactionMock).not.toHaveBeenCalled(); @@ -977,10 +1042,13 @@ describe('Across Submit', () => { } as TransactionPayQuote; await expect( - submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [missingApprovalGasQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }), + submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [missingApprovalGasQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }), ).rejects.toThrow( 'Missing quote gas limit for Across approval transaction at index 0', ); @@ -1001,10 +1069,13 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); const params = addTransactionMock.mock.calls[0][0] as { maxFeePerGas: Hex; @@ -1032,10 +1103,13 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [decimalGasQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [decimalGasQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); const params = addTransactionMock.mock.calls[0][0] as { maxFeePerGas: Hex; @@ -1064,10 +1138,13 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [quoteWithApproval], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [quoteWithApproval], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(addTransactionBatchMock).toHaveBeenCalled(); }); @@ -1093,10 +1170,13 @@ describe('Across Submit', () => { }, } as TransactionPayQuote; - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [quoteWithoutValue], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [quoteWithoutValue], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(addTransactionMock).toHaveBeenCalled(); const params = addTransactionMock.mock.calls[0][0] as { value: Hex }; @@ -1159,10 +1239,13 @@ describe('Across Submit', () => { }; }); - await submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [quote1, quote2], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }); + await submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [quote1, quote2], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); expect(addTransactionMock).toHaveBeenCalledTimes(2); }); @@ -1183,10 +1266,13 @@ describe('Across Submit', () => { addTransactionMock.mockRejectedValue(new Error('submission failed')); await expect( - submitAcrossQuotes({ accountSupports7702: true, messenger, - quotes: [noApprovalQuote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), }), + submitAcrossQuotes({ + accountSupports7702: true, + messenger, + quotes: [noApprovalQuote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }), ).rejects.toThrow('submission failed'); expect(unsubscribeSpy).toHaveBeenCalledWith( diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index ee1aa07751c..9318482e2f9 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -230,9 +230,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result).toStrictEqual([ expect.objectContaining({ @@ -246,9 +249,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(successfulFetchMock).toHaveBeenCalledWith( DEFAULT_RELAY_QUOTE_URL, @@ -282,9 +288,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -301,9 +310,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -319,9 +331,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: false, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: false, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -335,9 +350,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], + transaction: TRANSACTION_META_MOCK, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -353,14 +371,17 @@ describe('Relay Quotes Utils', () => { it('throws if isMaxAmount is true and transaction includes data', async () => { await expect( - getRelayQuotes({ accountSupports7702: true, messenger, - requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - data: '0xabc' as Hex, - }, - } as TransactionMeta, }), + getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: '0xabc' as Hex, + }, + } as TransactionMeta, + }), ).rejects.toThrow( 'Max amount quotes do not support included transactions', ); @@ -371,14 +392,17 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - data: '0xabc' as Hex, - }, - } as TransactionMeta, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: '0xabc' as Hex, + }, + } as TransactionMeta, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -415,9 +439,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].original.request).toStrictEqual({ amount: QUOTE_REQUEST_MOCK.targetAmountMinimum, @@ -437,14 +464,17 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - data: TOKEN_TRANSFER_DATA_MOCK, - }, - } as TransactionMeta, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: TOKEN_TRANSFER_DATA_MOCK, + }, + } as TransactionMeta, + }); expect(getDelegationTransactionMock).not.toHaveBeenCalled(); }); @@ -454,14 +484,17 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - data: TOKEN_TRANSFER_DATA_MOCK, - }, - } as TransactionMeta, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: TOKEN_TRANSFER_DATA_MOCK, + }, + } as TransactionMeta, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -475,19 +508,22 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { - data: NESTED_TRANSACTION_DATA_MOCK, + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + data: NESTED_TRANSACTION_DATA_MOCK, + }, + ], + txParams: { + data: '0xabc' as Hex, }, - ], - txParams: { - data: '0xabc' as Hex, - }, - } as TransactionMeta, }); + } as TransactionMeta, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -525,19 +561,22 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { - data: TOKEN_TRANSFER_DATA_MOCK, + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + data: TOKEN_TRANSFER_DATA_MOCK, + }, + ], + txParams: { + data: '0xabc' as Hex, }, - ], - txParams: { - data: '0xabc' as Hex, - }, - } as TransactionMeta, }); + } as TransactionMeta, + }); expect(getDelegationTransactionMock).not.toHaveBeenCalled(); }); @@ -547,19 +586,22 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { - data: TOKEN_TRANSFER_DATA_MOCK, + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + data: TOKEN_TRANSFER_DATA_MOCK, + }, + ], + txParams: { + data: '0xabc' as Hex, }, - ], - txParams: { - data: '0xabc' as Hex, - }, - } as TransactionMeta, }); + } as TransactionMeta, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -573,22 +615,25 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { - data: NESTED_TRANSACTION_DATA_MOCK, - }, - { - data: TOKEN_TRANSFER_DATA_MOCK, + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + data: NESTED_TRANSACTION_DATA_MOCK, + }, + { + data: TOKEN_TRANSFER_DATA_MOCK, + }, + ], + txParams: { + data: '0xabc' as Hex, }, - ], - txParams: { - data: '0xabc' as Hex, - }, - } as TransactionMeta, }); + } as TransactionMeta, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -603,19 +648,22 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetChainId: CHAIN_ID_HYPERCORE, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - data: '0xabc' as Hex, - }, - } as TransactionMeta, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetChainId: CHAIN_ID_HYPERCORE, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: '0xabc' as Hex, + }, + } as TransactionMeta, + }); expect(getDelegationTransactionMock).not.toHaveBeenCalled(); }); @@ -625,19 +673,22 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetChainId: CHAIN_ID_HYPERCORE, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - txParams: { - data: TOKEN_TRANSFER_DATA_MOCK, - }, - } as TransactionMeta, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetChainId: CHAIN_ID_HYPERCORE, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: TOKEN_TRANSFER_DATA_MOCK, + }, + } as TransactionMeta, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -664,9 +715,12 @@ describe('Relay Quotes Utils', () => { }, }); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(successfulFetchMock).toHaveBeenCalledWith( relayQuoteUrl, @@ -679,15 +733,18 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - sourceTokenAmount: '0', - }, - ], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + sourceTokenAmount: '0', + }, + ], + transaction: TRANSACTION_META_MOCK, + }); expect(successfulFetchMock).not.toHaveBeenCalled(); }); @@ -697,15 +754,18 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: TRANSACTION_META_MOCK, + }); expect(successfulFetchMock).toHaveBeenCalled(); @@ -724,16 +784,19 @@ describe('Relay Quotes Utils', () => { const refundTo = '0xsafe000000000000000000000000000000000001' as Hex; - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo, - }, - ], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo, + }, + ], + transaction: TRANSACTION_META_MOCK, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -747,15 +810,18 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: TRANSACTION_META_MOCK, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -781,15 +847,18 @@ describe('Relay Quotes Utils', () => { }, } as TransactionMeta; - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: postQuoteTransaction, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: postQuoteTransaction, + }); // Original transaction should NOT be included in gas estimation. // Only relay step params are estimated. @@ -803,29 +872,32 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - gas: '0x13498', - value: '0', - }, - } as TransactionMeta, }); - - expect(result[0].original.metamask.gasLimits).toStrictEqual([ - 79000, 21000, - ]); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + gas: '0x13498', + value: '0', + }, + } as TransactionMeta, + }); + + expect(result[0].original.metamask.gasLimits).toStrictEqual([ + 79000, 21000, + ]); expect(result[0].original.metamask.is7702).toBe(false); }); @@ -836,26 +908,29 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - gas: '0x13498', - value: '0', - }, - nestedTransactions: [{ gas: '0xC350' }], - } as TransactionMeta, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + gas: '0x13498', + value: '0', + }, + nestedTransactions: [{ gas: '0xC350' }], + } as TransactionMeta, + }); expect(result[0].original.metamask.gasLimits).toStrictEqual([ 50000, 21000, @@ -895,25 +970,28 @@ describe('Relay Quotes Utils', () => { gasLimits: [51000], }); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - gas: '0x13498', - value: '0', - }, - } as TransactionMeta, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + gas: '0x13498', + value: '0', + }, + } as TransactionMeta, + }); // EIP-7702: original tx gas (79000) added to combined relay gas (51000) expect(result[0].original.metamask.gasLimits).toStrictEqual([130000]); @@ -952,25 +1030,28 @@ describe('Relay Quotes Utils', () => { gasLimits: [21000, 30000], }); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - gas: '0x13498', - value: '0', - }, - } as TransactionMeta, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + gas: '0x13498', + value: '0', + }, + } as TransactionMeta, + }); // Original tx gas (79000) prepended to relay gas limits [21000, 30000] expect(result[0].original.metamask.gasLimits).toStrictEqual([ @@ -984,24 +1065,27 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - value: '0', - }, - } as TransactionMeta, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + value: '0', + }, + } as TransactionMeta, + }); // No gas on txParams or nestedTransactions — only relay gas limits expect(result[0].original.metamask.gasLimits).toStrictEqual([21000]); @@ -1021,25 +1105,28 @@ describe('Relay Quotes Utils', () => { estimateGasMock.mockRejectedValue(new Error('Estimation failed')); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: { - ...TRANSACTION_META_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - gas: '0x13498', // 79 000 - value: '0', - }, - } as TransactionMeta, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + gas: '0x13498', // 79 000 + value: '0', + }, + } as TransactionMeta, + }); // Fallback: estimate=900000, max=1500000. // With originalTxGas=79000 added independently: @@ -1061,15 +1148,18 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - }, - ], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + }, + ], + transaction: TRANSACTION_META_MOCK, + }); // With no txParams.to the original tx should be skipped, so only // the relay step params are sent to gas estimation (single path). @@ -1091,16 +1181,19 @@ describe('Relay Quotes Utils', () => { const proxyAddress = '0xproxyAddress1234567890123456789012345678' as Hex; - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: proxyAddress, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: proxyAddress, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, + }); expect(estimateGasMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1122,16 +1215,19 @@ describe('Relay Quotes Utils', () => { const proxyAddress = '0xproxyAddress1234567890123456789012345678' as Hex; - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: proxyAddress, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: proxyAddress, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, + }); expect(estimateGasMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1165,16 +1261,19 @@ describe('Relay Quotes Utils', () => { const proxyAddress = '0xproxyAddress1234567890123456789012345678' as Hex; - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: proxyAddress, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: proxyAddress, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, + }); expect(estimateGasBatchMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1208,16 +1307,19 @@ describe('Relay Quotes Utils', () => { const proxyAddress = '0xproxyAddress1234567890123456789012345678' as Hex; - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: proxyAddress, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: proxyAddress, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, + }); expect(estimateGasBatchMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1241,16 +1343,19 @@ describe('Relay Quotes Utils', () => { const proxyAddress = '0xproxyAddress1234567890123456789012345678' as Hex; - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: proxyAddress, - }, - ], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: proxyAddress, + }, + ], + transaction: TRANSACTION_META_MOCK, + }); expect(estimateGasMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1273,9 +1378,12 @@ describe('Relay Quotes Utils', () => { simulationFails: undefined, }); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(estimateGasMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1305,9 +1413,12 @@ describe('Relay Quotes Utils', () => { gasLimits: [50000, 50000], }); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(estimateGasBatchMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1324,16 +1435,19 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('0'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, + }); expect(result[0].fees.isSourceGasFeeToken).toBe(true); }); @@ -1346,16 +1460,19 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('0'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, + }); expect(getGasFeeTokensMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1377,16 +1494,19 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('0'); getGasFeeTokensMock.mockResolvedValue([]); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, + }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); }); @@ -1398,16 +1518,19 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('0'); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); }); @@ -1439,16 +1562,19 @@ describe('Relay Quotes Utils', () => { usd: '4.45', }); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, + }); expect(successfulFetchMock).toHaveBeenCalledTimes(2); @@ -1482,17 +1608,20 @@ describe('Relay Quotes Utils', () => { usd: '999', }); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - sourceTokenAmount: '1', - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + sourceTokenAmount: '1', + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, + }); expect(successfulFetchMock).toHaveBeenCalledTimes(1); expect(result).toHaveLength(1); @@ -1506,16 +1635,19 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('0'); getGasFeeTokensMock.mockRejectedValue(new Error('Simulation failed')); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, + }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); }); @@ -1535,16 +1667,19 @@ describe('Relay Quotes Utils', () => { usd: '4.45', }); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: TRANSACTION_META_MOCK, + }); expect(successfulFetchMock).toHaveBeenCalledTimes(2); expect(result).toHaveLength(1); @@ -1572,16 +1707,19 @@ describe('Relay Quotes Utils', () => { usd: '4.45', }); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: PREDICT_WITHDRAW_TRANSACTION_MOCK, + }); expect(successfulFetchMock).toHaveBeenCalledTimes(2); expect(result[0].fees.isSourceGasFeeToken).toBe(true); @@ -1592,9 +1730,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].estimatedDuration).toBe(300); }); @@ -1604,9 +1745,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.metaMask).toStrictEqual({ usd: '0', @@ -1623,9 +1767,12 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.metaMask).toStrictEqual({ usd: '0.75', @@ -1642,9 +1789,12 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.provider).toStrictEqual({ usd: '1.11', @@ -1657,9 +1807,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.provider).toStrictEqual({ usd: '1.11', @@ -1685,9 +1838,12 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.provider).toStrictEqual({ usd: '0', @@ -1730,9 +1886,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].dust).toStrictEqual({ usd: '0.0246', @@ -1746,9 +1905,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.sourceNetwork).toStrictEqual({ estimate: { @@ -1774,9 +1936,12 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 900000 }), @@ -1828,9 +1993,12 @@ describe('Relay Quotes Utils', () => { gasLimits: [21000, 480000, 1000, 2000], }); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 504000 }), @@ -1845,9 +2013,12 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('1724999999999999'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.isSourceGasFeeToken).toBe(true); expect(result[0].fees.sourceNetwork).toStrictEqual({ @@ -1890,9 +2061,12 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('1724999999999999'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(calculateGasFeeTokenCostMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1912,26 +2086,29 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('1724999999999999'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetAmountMinimum: '0', - isPostQuote: true, - refundTo: '0xproxy' as Hex, - }, - ], - transaction: { - ...PREDICT_WITHDRAW_TRANSACTION_MOCK, - chainId: '0x1' as Hex, - txParams: { - from: FROM_MOCK, - to: '0x9' as Hex, - data: '0xaaa' as Hex, - gas: '0x5208', - value: '0', - }, - } as TransactionMeta, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetAmountMinimum: '0', + isPostQuote: true, + refundTo: '0xproxy' as Hex, + }, + ], + transaction: { + ...PREDICT_WITHDRAW_TRANSACTION_MOCK, + chainId: '0x1' as Hex, + txParams: { + from: FROM_MOCK, + to: '0x9' as Hex, + data: '0xaaa' as Hex, + gas: '0x5208', + value: '0', + }, + } as TransactionMeta, + }); expect(getGasFeeTokensMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -1953,9 +2130,12 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('1725000000000000'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); expect(result[0].fees.sourceNetwork).toStrictEqual({ @@ -1984,9 +2164,12 @@ describe('Relay Quotes Utils', () => { { ...GAS_FEE_TOKEN_MOCK, tokenAddress: '0xdef' as Hex }, ]); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); expect(result[0].fees.sourceNetwork).toStrictEqual({ @@ -2014,9 +2197,12 @@ describe('Relay Quotes Utils', () => { getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); calculateGasFeeTokenCostMock.mockReturnValue(undefined); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); expect(result[0].fees.sourceNetwork).toStrictEqual({ @@ -2046,9 +2232,12 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('1724999999999999'); getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(getGasFeeTokensMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -2074,9 +2263,12 @@ describe('Relay Quotes Utils', () => { }, }); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); expect(result[0].fees.sourceNetwork).toStrictEqual({ @@ -2108,9 +2300,12 @@ describe('Relay Quotes Utils', () => { getTokenBalanceMock.mockReturnValue('1724999999999999'); isEIP7702ChainMock.mockReturnValue(true); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [lineaQuoteRequest], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [lineaQuoteRequest], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); expect(result[0].fees.sourceNetwork).toStrictEqual({ @@ -2141,9 +2336,12 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.sourceNetwork).toStrictEqual({ estimate: ZERO_AMOUNT, @@ -2159,9 +2357,12 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].original.metamask.isExecute).toBe(true); }); @@ -2171,9 +2372,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.sourceNetwork).not.toStrictEqual({ estimate: ZERO_AMOUNT, @@ -2189,9 +2393,12 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].original.metamask.gasLimits).toStrictEqual([]); }); @@ -2212,9 +2419,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [HL_REQUEST], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [HL_REQUEST], + transaction: TRANSACTION_META_MOCK, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -2229,9 +2439,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [HL_REQUEST], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [HL_REQUEST], + transaction: TRANSACTION_META_MOCK, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -2245,9 +2458,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [HL_REQUEST], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [HL_REQUEST], + transaction: TRANSACTION_META_MOCK, + }); const zeroAmount = { fiat: '0', human: '0', raw: '0', usd: '0' }; @@ -2260,9 +2476,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [HL_REQUEST], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [HL_REQUEST], + transaction: TRANSACTION_META_MOCK, + }); expect(getTokenFiatRateMock).toHaveBeenCalledWith( expect.anything(), @@ -2277,9 +2496,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].fees.targetNetwork).toStrictEqual({ usd: '0', @@ -2292,9 +2514,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].targetAmount).toStrictEqual({ usd: '1.23', @@ -2320,12 +2545,14 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].targetAmount).toStrictEqual({ - usd: '1.23', fiat: '2.46', }); @@ -2336,15 +2563,18 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [ - { - ...QUOTE_REQUEST_MOCK, - targetChainId: CHAIN_ID_ARBITRUM, - targetTokenAddress: ARBITRUM_USDC_ADDRESS, - }, - ], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + targetChainId: CHAIN_ID_ARBITRUM, + targetTokenAddress: ARBITRUM_USDC_ADDRESS, + }, + ], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].targetAmount).toStrictEqual({ usd: '1', @@ -2357,9 +2587,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].targetAmount).toStrictEqual({ usd: '1.23', @@ -2371,9 +2604,12 @@ describe('Relay Quotes Utils', () => { successfulFetchMock.mockRejectedValue(new Error('Fetch error')); await expect( - getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }), + getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }), ).rejects.toThrow('Fetch error'); }); @@ -2385,9 +2621,12 @@ describe('Relay Quotes Utils', () => { } as never); await expect( - getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }), + getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }), ).rejects.toThrow(`Source token fiat rate not found`); }); @@ -2402,9 +2641,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [arbitrumToHyperliquidRequest], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [arbitrumToHyperliquidRequest], + transaction: TRANSACTION_META_MOCK, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -2432,9 +2674,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [postQuoteRequest], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [postQuoteRequest], + transaction: TRANSACTION_META_MOCK, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -2461,9 +2706,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [polygonToHyperliquidRequest], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [polygonToHyperliquidRequest], + transaction: TRANSACTION_META_MOCK, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -2487,9 +2735,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [polygonTargetRequest], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [polygonTargetRequest], + transaction: TRANSACTION_META_MOCK, + }); const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, @@ -2515,9 +2766,12 @@ describe('Relay Quotes Utils', () => { simulationFails: undefined, }); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(estimateGasMock).toHaveBeenCalledWith( { @@ -2544,9 +2798,12 @@ describe('Relay Quotes Utils', () => { estimateGasMock.mockRejectedValue(new Error('Estimation failed')); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 900000 }), @@ -2568,9 +2825,12 @@ describe('Relay Quotes Utils', () => { }, }); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 900000 }), @@ -2597,9 +2857,12 @@ describe('Relay Quotes Utils', () => { gasLimits: [50000, 50000], }); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(estimateGasBatchMock).toHaveBeenCalledWith({ chainId: '0x1', @@ -2643,9 +2906,12 @@ describe('Relay Quotes Utils', () => { gasLimits: [30000, 50000], }); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(estimateGasBatchMock).toHaveBeenCalledTimes(1); expect(estimateGasMock).not.toHaveBeenCalled(); @@ -2674,9 +2940,12 @@ describe('Relay Quotes Utils', () => { ); await expect( - getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }), + getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }), ).rejects.toThrow( 'Failed to fetch Relay quotes: Error: Batch estimation failed', ); @@ -2687,9 +2956,12 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - const result = await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(result[0].original.metamask).toStrictEqual({ gasLimits: [21000], @@ -2706,9 +2978,12 @@ describe('Relay Quotes Utils', () => { json: async () => quoteMock, } as never); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(estimateGasMock).toHaveBeenCalledWith( expect.objectContaining({ value: '0x0' }), @@ -2727,9 +3002,12 @@ describe('Relay Quotes Utils', () => { } as never); await expect( - getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }), + getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }), ).rejects.toThrow('Failed to fetch Relay quotes'); }); @@ -2747,9 +3025,12 @@ describe('Relay Quotes Utils', () => { } as never); await expect( - getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }), + getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }), ).rejects.toThrow('Failed to fetch Relay quotes'); }); @@ -2769,9 +3050,12 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1.5); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 75000 }), @@ -2801,9 +3085,12 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1.5); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 120000 }), @@ -2834,9 +3121,12 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1.5); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 70000 }), @@ -2867,9 +3157,12 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1.5); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 90000 }), @@ -2899,9 +3192,12 @@ describe('Relay Quotes Utils', () => { getGasBufferMock.mockReturnValue(1.5); - await getRelayQuotes({ accountSupports7702: true, messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, }); + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); expect(calculateGasCostMock).toHaveBeenCalledWith( expect.objectContaining({ gas: 105000 }), diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index d8ceda214f9..873dd64c788 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -4,7 +4,6 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; - import { getMessengerMock } from '../../tests/messenger-mock'; import type { PayStrategyExecuteRequest, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index bf14ab54570..3c98336ee51 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -107,7 +107,12 @@ async function executeSingleQuote( if (quote.request.isHyperliquidSource) { await submitHyperliquidWithdraw(quote, quote.request.from, messenger); } else { - await submitTransactions(quote, transaction, messenger, accountSupports7702); + await submitTransactions( + quote, + transaction, + messenger, + accountSupports7702, + ); } const targetHash = await waitForRelayCompletion( diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index 55ca446cc86..1239088f84b 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -46,7 +46,9 @@ type MessengerMockResult = { estimateGasBatchMock: jest.MockedFn< TransactionControllerEstimateGasBatchAction['handler'] >; - estimateGasMock: jest.MockedFn; + estimateGasMock: jest.MockedFn< + TransactionControllerEstimateGasAction['handler'] + >; fetchQuotesMock: jest.Mock; findNetworkClientIdByChainIdMock: jest.MockedFn< NetworkControllerFindNetworkClientIdByChainIdAction['handler'] @@ -75,7 +77,9 @@ type MessengerMockResult = { getRemoteFeatureFlagControllerStateMock: jest.MockedFn< RemoteFeatureFlagControllerGetStateAction['handler'] >; - getStrategyMock: jest.MockedFn; + getStrategyMock: jest.MockedFn< + TransactionPayControllerGetStrategyAction['handler'] + >; getTokenBalanceControllerStateMock: jest.MockedFn< TokenBalancesControllerGetStateAction['handler'] >; @@ -312,7 +316,6 @@ export function getMessengerMock({ 'AssetsController:getStateForTransactionPay', getAssetsControllerStateMock, ); - } const publish = messenger.publish.bind(messenger); diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index b9201e40116..1c77b8dd4ac 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -137,9 +137,7 @@ export type TransactionPayControllerMessenger = Messenger< >; /** Callback to check whether an account supports EIP-7702 authorization signing. */ -export type AccountSupports7702Callback = ( - account: string, -) => Promise; +export type AccountSupports7702Callback = (account: string) => Promise; /** Options for the TransactionPayController. */ export type TransactionPayControllerOptions = { diff --git a/packages/transaction-pay-controller/src/utils/quote-gas.ts b/packages/transaction-pay-controller/src/utils/quote-gas.ts index 3b1f69d1722..5b40e2b7837 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.ts @@ -56,10 +56,7 @@ export async function estimateQuoteGasLimits({ const useBatch = transactions.length > 1; if (useBatch) { - const result = await estimateQuoteGasLimitsBatch( - transactions, - messenger, - ); + const result = await estimateQuoteGasLimitsBatch(transactions, messenger); // If the batch returned a combined 7702 gas limit but the account cannot // sign EIP-7702 authorizations (e.g. hardware wallet), re-estimate each diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index ad7a1433d5c..5530d12184c 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -103,9 +103,8 @@ const BATCH_TRANSACTION_MOCK = { describe('Quotes Utils', () => { const { messenger, getControllerStateMock } = getMessengerMock(); - const accountSupports7702Mock: jest.MockedFunction< - AccountSupports7702Callback - > = jest.fn(); + const accountSupports7702Mock: jest.MockedFunction = + jest.fn(); const updateTransactionDataMock = jest.fn(); const getStrategyByNameMock = jest.mocked(getStrategyByName); const getStrategiesByNameMock = jest.mocked(getStrategiesByName); diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index 7c219ba496e..e873ea1f037 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -195,9 +195,7 @@ function syncTransaction({ }, (tx: TransactionMeta) => { tx.batchTransactions = batchTransactions; - tx.batchTransactionsOptions = batchTransactions?.length - ? {} - : undefined; + tx.batchTransactionsOptions = batchTransactions?.length ? {} : undefined; tx.metamaskPay = { bridgeFeeFiat: totals.fees.provider.usd, From 77576cc3838bffd5f5c7ddc455d5fe71fe9a80c3 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:11:43 +0200 Subject: [PATCH 18/48] Remove unnecessary mock type --- .../src/tests/messenger-mock.ts | 68 +------------------ 1 file changed, 1 insertion(+), 67 deletions(-) diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index 1239088f84b..7ebda275cde 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -36,72 +36,6 @@ type AllActions = MessengerActions; type AllEvents = MessengerEvents; type RootMessenger = Messenger; -type MessengerMockResult = { - addTransactionBatchMock: jest.MockedFn< - TransactionControllerAddTransactionBatchAction['handler'] - >; - addTransactionMock: jest.MockedFn< - TransactionControllerAddTransactionAction['handler'] - >; - estimateGasBatchMock: jest.MockedFn< - TransactionControllerEstimateGasBatchAction['handler'] - >; - estimateGasMock: jest.MockedFn< - TransactionControllerEstimateGasAction['handler'] - >; - fetchQuotesMock: jest.Mock; - findNetworkClientIdByChainIdMock: jest.MockedFn< - NetworkControllerFindNetworkClientIdByChainIdAction['handler'] - >; - getAccountTrackerControllerStateMock: jest.MockedFn< - AccountTrackerControllerGetStateAction['handler'] - >; - getAssetsControllerStateMock: jest.Mock; - getBridgeStatusControllerStateMock: jest.MockedFn< - BridgeStatusControllerGetStateAction['handler'] - >; - getControllerStateMock: jest.MockedFn< - TransactionPayControllerGetStateAction['handler'] - >; - getCurrencyRateControllerStateMock: jest.Mock; - getDelegationTransactionMock: jest.MockedFn< - TransactionPayControllerGetDelegationTransactionAction['handler'] - >; - getGasFeeControllerStateMock: jest.Mock; - getGasFeeTokensMock: jest.MockedFn< - TransactionControllerGetGasFeeTokensAction['handler'] - >; - getNetworkClientByIdMock: jest.MockedFn< - NetworkControllerGetNetworkClientByIdAction['handler'] - >; - getRemoteFeatureFlagControllerStateMock: jest.MockedFn< - RemoteFeatureFlagControllerGetStateAction['handler'] - >; - getStrategyMock: jest.MockedFn< - TransactionPayControllerGetStrategyAction['handler'] - >; - getTokenBalanceControllerStateMock: jest.MockedFn< - TokenBalancesControllerGetStateAction['handler'] - >; - getTokenRatesControllerStateMock: jest.MockedFn< - TokenRatesControllerGetStateAction['handler'] - >; - getTokensControllerStateMock: jest.MockedFn< - TokensControllerGetStateAction['handler'] - >; - getTransactionControllerStateMock: jest.MockedFn< - TransactionControllerGetStateAction['handler'] - >; - messenger: TransactionPayControllerMessenger; - publish: RootMessenger['publish']; - submitTransactionMock: jest.MockedFunction< - BridgeStatusControllerSubmitTxAction['handler'] - >; - updateTransactionMock: jest.MockedFn< - TransactionControllerUpdateTransactionAction['handler'] - >; -}; - /** * Creates a mock controller messenger for testing. * @@ -112,7 +46,7 @@ type MessengerMockResult = { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function getMessengerMock({ skipRegister, -}: { skipRegister?: boolean } = {}): MessengerMockResult { +}: { skipRegister?: boolean } = {}) { const getControllerStateMock: jest.MockedFn< TransactionPayControllerGetStateAction['handler'] > = jest.fn(); From 6cd9019ffe4039de50792dfd81f774e98aec0e09 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:14:37 +0200 Subject: [PATCH 19/48] Fix changelog --- packages/transaction-pay-controller/CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 077cbd41f13..f2910c77e7d 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -41,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Resolve correct `networkClientId` for source chain in Relay execute flow ([#8492](https://github.com/MetaMask/core/pull/8492)) - Stop double-counting subsidized fees in Relay quote target amounts ([#8488](https://github.com/MetaMask/core/pull/8488)) +- Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on a new `accountSupports7702` callback option ([#8388](https://github.com/MetaMask/core/pull/8388)) ## [19.2.0] @@ -80,10 +81,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Set `submittedTime` at the start of `TransactionPayPublishHook` before strategy execution for accurate `mm_pay_time_to_complete_s` metrics in intent-based flows ([#8439](https://github.com/MetaMask/core/pull/8439)) -### Fixed - -- Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on a new `accountSupports7702` callback option ([#8388](https://github.com/MetaMask/core/pull/8388)) - ## [19.1.0] ### Added From 175d5b9ea61ab2eac96584f72a7393fcbf2e6ea1 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:18:06 +0200 Subject: [PATCH 20/48] Fix tests --- .../src/TransactionPayController.test.ts | 1 + .../src/helpers/TransactionPayPublishHook.test.ts | 2 ++ .../src/strategy/across/across-submit.test.ts | 2 +- .../src/strategy/relay/relay-submit.test.ts | 10 +++++----- .../src/utils/quotes.test.ts | 4 ++-- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 9151367bdb5..077f9a03899 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -582,6 +582,7 @@ describe('TransactionPayController', () => { ); expect(updateQuotesMock).toHaveBeenCalledWith({ + accountSupports7702: expect.any(Function), getStrategies: expect.any(Function), messenger, transactionData: expect.objectContaining({ diff --git a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts index f880dcb4af3..7144201245e 100644 --- a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts +++ b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts @@ -52,6 +52,8 @@ describe('TransactionPayPublishHook', () => { beforeEach(() => { jest.resetAllMocks(); + accountSupports7702Mock.mockResolvedValue(true); + hook = new TransactionPayPublishHook({ accountSupports7702: accountSupports7702Mock, isSmartTransaction: isSmartTransactionMock, diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts index d4d4e61436a..60eeae06991 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts @@ -241,7 +241,7 @@ describe('Across Submit', () => { expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ disable7702: false, - disableHook: true, + disableHook: false, disableSequential: true, gasLimit7702: toHex(64000), transactions: [ diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index 873dd64c788..9c1d1a8cc8c 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -502,7 +502,7 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); expect(addTransactionBatchMock).toHaveBeenCalledWith({ disable7702: true, - disableHook: false, + disableHook: true, disableSequential: false, from: FROM_MOCK, networkClientId: NETWORK_CLIENT_ID_MOCK, @@ -1013,7 +1013,7 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ disable7702: true, - disableHook: false, + disableHook: true, disableSequential: false, gasLimit7702: undefined, transactions: [ @@ -1043,7 +1043,7 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ disable7702: false, - disableHook: true, + disableHook: false, disableSequential: true, gasLimit7702: '0x31955', transactions: [ @@ -1079,7 +1079,7 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ disable7702: false, - disableHook: true, + disableHook: false, disableSequential: true, gasLimit7702: '0xa410', transactions: [ @@ -1213,7 +1213,7 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ disable7702: true, - disableHook: false, + disableHook: true, disableSequential: false, gasLimit7702: undefined, transactions: [ diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 5530d12184c..fe30f8cf095 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -677,8 +677,8 @@ describe('Quotes Utils', () => { expect(transactionMetaMock).toMatchObject( expect.objectContaining({ - batchTransactions: [], - batchTransactionsOptions: {}, + batchTransactions: undefined, + batchTransactionsOptions: undefined, }), ); }); From 4753f78f47fefc0f43c78ba491932427150c63a3 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:22:31 +0200 Subject: [PATCH 21/48] Remove unnecessary let --- .../src/strategy/across/across-quotes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts index 88294e1d2c3..d4c76d7b8fb 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -401,7 +401,7 @@ async function calculateSourceNetworkCost( const orderedTransactions = getAcrossOrderedTransactions({ quote }); const { swapTx } = quote; const swapChainId = toHex(swapTx.chainId); - let gasEstimates = await estimateQuoteGasLimits({ + const gasEstimates = await estimateQuoteGasLimits({ fallbackGas: acrossFallbackGas, messenger, transactions: orderedTransactions.map((transaction) => ({ From 126b8f98dee76b0e98b90edab3b3255d6f255dc3 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:27:29 +0200 Subject: [PATCH 22/48] Fix lint --- .../src/strategy/across/across-submit.ts | 1 + .../src/strategy/relay/relay-submit.ts | 3 +++ packages/transaction-pay-controller/src/utils/quote-gas.ts | 6 +++--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts index f08fe2e017c..107f1abeac0 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts @@ -113,6 +113,7 @@ async function executeSingleQuote( * @param parentTransactionId - ID of the parent transaction. * @param acrossDepositType - Transaction type used for the swap/deposit step. * @param messenger - Controller messenger. + * @param accountSupports7702 - Whether the account supports EIP-7702. * @returns Hash of the last submitted transaction, if available. */ async function submitTransactions( diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 3c98336ee51..661b3486ab7 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -83,6 +83,7 @@ export async function submitRelayQuotes( * @param quote - Relay quote to execute. * @param messenger - Controller messenger. * @param transaction - Original transaction meta. + * @param accountSupports7702 - Whether the account supports EIP-7702. * @returns An object containing the transaction hash if available. */ async function executeSingleQuote( @@ -316,6 +317,7 @@ async function validateSourceBalance( * @param quote - Relay quote. * @param transaction - Original transaction meta. * @param messenger - Controller messenger. + * @param accountSupports7702 - Whether the account supports EIP-7702. * @returns Hash of the last submitted transaction. */ async function submitTransactions( @@ -480,6 +482,7 @@ async function submitViaRelayExecute( * @param quote - Relay quote. * @param transaction - Original transaction meta. * @param messenger - Controller messenger. + * @param accountSupports7702 - Whether the account supports EIP-7702. * @param normalizedParams - Normalized relay-only params (without prepended original tx). * @param allParams - All params including any prepended original tx for post-quote flows. * @returns Hash of the last submitted transaction. diff --git a/packages/transaction-pay-controller/src/utils/quote-gas.ts b/packages/transaction-pay-controller/src/utils/quote-gas.ts index 5b40e2b7837..af053075faa 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.ts @@ -74,14 +74,14 @@ export async function estimateQuoteGasLimits({ ); return { - gasLimits: individualResults.map((r) => r.gasLimits[0]), + gasLimits: individualResults.map((res) => res.gasLimits[0]), is7702: false, totalGasEstimate: individualResults.reduce( - (acc, r) => acc + r.totalGasEstimate, + (acc, res) => acc + res.totalGasEstimate, 0, ), totalGasLimit: individualResults.reduce( - (acc, r) => acc + r.totalGasLimit, + (acc, res) => acc + res.totalGasLimit, 0, ), usedBatch: true, From fec03287995432072bad43c8b143132e134c1987 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:44:37 +0200 Subject: [PATCH 23/48] Update --- .../src/strategy/across/across-quotes.ts | 4 +--- .../src/strategy/across/across-submit.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts index d4c76d7b8fb..015f2e810b5 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -414,9 +414,7 @@ async function calculateSourceNetworkCost( })), accountSupports7702, }); - const { batchGasLimit } = gasEstimates; - - const { is7702 } = gasEstimates; + const { batchGasLimit, is7702 } = gasEstimates; if (is7702) { if (!batchGasLimit) { diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts index 107f1abeac0..f0ccc91d389 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts @@ -202,9 +202,11 @@ async function submitTransactions( }, ); + let result: { result: Promise } | undefined; + try { if (transactions.length === 1) { - await messenger.call( + result = await messenger.call( 'TransactionController:addTransaction', transactions[0].params, { @@ -236,6 +238,11 @@ async function submitTransactions( end(); } + if (result) { + const txHash = await result.result; + log('Submitted transaction', txHash); + } + await Promise.all( transactionIds.map((txId) => waitForTransactionConfirmed(txId, messenger)), ); From fd674e79a9620210cfa22680d03fee755a954d82 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 15:24:02 +0200 Subject: [PATCH 24/48] Update --- packages/transaction-controller/CHANGELOG.md | 1 + .../src/TransactionController.ts | 6 +- .../src/TransactionPayController.test.ts | 102 +++++++++++++++++- .../src/TransactionPayController.ts | 21 ++-- .../src/helpers/QuoteRefresher.ts | 2 +- .../helpers/TransactionPayPublishHook.test.ts | 50 ++++++++- .../src/helpers/TransactionPayPublishHook.ts | 16 +-- .../src/tests/messenger-mock.ts | 20 ++++ .../transaction-pay-controller/src/types.ts | 19 +++- 9 files changed, 212 insertions(+), 25 deletions(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 4cdca835551..94938dc674e 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 - Snapshot `txParamsOriginal` in `updateEditableParams` when `containerTypes` are first applied ([#8546](https://github.com/MetaMask/core/pull/8546)) - Add `requiresAuthorizationList` to `TransactionController:estimateGasBatch` results when EIP-7702 batch gas estimation requires a first-time account upgrade ([#8577](https://github.com/MetaMask/core/pull/8577)) +- Add `KeyringControllerGetStateAction` to `AllowedActions` to support publish hooks that need to check keyring type for EIP-7702 compatibility ([#8388](https://github.com/MetaMask/core/pull/8388)) ### Fixed diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index ac6e9561d99..64f57f20c8b 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -31,7 +31,10 @@ import type { FetchGasFeeEstimateOptions, GasFeeState, } from '@metamask/gas-fee-controller'; -import type { KeyringControllerSignEip7702AuthorizationAction } from '@metamask/keyring-controller'; +import type { + KeyringControllerGetStateAction, + KeyringControllerSignEip7702AuthorizationAction, +} from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; import type { BlockTracker, @@ -492,6 +495,7 @@ export type AllowedActions = | AccountsControllerGetSelectedAccountAction | AccountsControllerGetStateAction | ApprovalControllerAddRequestAction + | KeyringControllerGetStateAction | KeyringControllerSignEip7702AuthorizationAction | NetworkControllerFindNetworkClientIdByChainIdAction | NetworkControllerGetNetworkClientByIdAction diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 077f9a03899..57be5c16900 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -37,6 +37,7 @@ describe('TransactionPayController', () => { const pollTransactionChangesMock = jest.mocked(pollTransactionChanges); const getStrategyOrderMock = jest.mocked(getStrategyOrder); let messenger: TransactionPayControllerMessenger; + let getKeyringControllerStateMock: jest.Mock; /** * Create a TransactionPayController. @@ -45,7 +46,6 @@ describe('TransactionPayController', () => { */ function createController(): TransactionPayController { return new TransactionPayController({ - accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: jest.fn(), messenger, }); @@ -54,7 +54,21 @@ describe('TransactionPayController', () => { beforeEach(() => { jest.resetAllMocks(); - messenger = getMessengerMock({ skipRegister: true }).messenger; + const mocks = getMessengerMock({ skipRegister: true }); + messenger = mocks.messenger; + getKeyringControllerStateMock = mocks.getKeyringControllerStateMock; + + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: ['0x1234567890123456789012345678901234567891'], + metadata: { id: 'hd-keyring', name: 'HD Key Tree' }, + }, + ], + }); + getStrategyOrderMock.mockReturnValue([TransactionPayStrategy.Relay]); updateQuotesMock.mockResolvedValue(true); }); @@ -594,6 +608,90 @@ describe('TransactionPayController', () => { }); }); + describe('accountSupports7702', () => { + it('returns true for HD keyring account', async () => { + const controller = createController(); + + controller.updatePaymentToken({ + transactionId: TRANSACTION_ID_MOCK, + tokenAddress: TOKEN_ADDRESS_MOCK, + chainId: CHAIN_ID_MOCK, + }); + + const { updateTransactionData } = updatePaymentTokenMock.mock.calls[0][1]; + + updateTransactionData(TRANSACTION_ID_MOCK, (data) => { + data.sourceAmounts = [ + { sourceAmountHuman: '1.23' } as TransactionPaySourceAmount, + ]; + }); + + const { accountSupports7702 } = updateQuotesMock.mock.calls[0][0]; + const result = await accountSupports7702( + '0x1234567890123456789012345678901234567891', + ); + + expect(result).toBe(true); + }); + + it('returns false for Ledger keyring account', async () => { + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'Ledger Hardware', + accounts: ['0xledger'], + metadata: { id: 'ledger', name: 'Ledger' }, + }, + ], + }); + + const controller = createController(); + + controller.updatePaymentToken({ + transactionId: TRANSACTION_ID_MOCK, + tokenAddress: TOKEN_ADDRESS_MOCK, + chainId: CHAIN_ID_MOCK, + }); + + const { updateTransactionData } = updatePaymentTokenMock.mock.calls[0][1]; + + updateTransactionData(TRANSACTION_ID_MOCK, (data) => { + data.sourceAmounts = [ + { sourceAmountHuman: '1.23' } as TransactionPaySourceAmount, + ]; + }); + + const { accountSupports7702 } = updateQuotesMock.mock.calls[0][0]; + const result = await accountSupports7702('0xledger'); + + expect(result).toBe(false); + }); + + it('returns true when keyring is not found', async () => { + const controller = createController(); + + controller.updatePaymentToken({ + transactionId: TRANSACTION_ID_MOCK, + tokenAddress: TOKEN_ADDRESS_MOCK, + chainId: CHAIN_ID_MOCK, + }); + + const { updateTransactionData } = updatePaymentTokenMock.mock.calls[0][1]; + + updateTransactionData(TRANSACTION_ID_MOCK, (data) => { + data.sourceAmounts = [ + { sourceAmountHuman: '1.23' } as TransactionPaySourceAmount, + ]; + }); + + const { accountSupports7702 } = updateQuotesMock.mock.calls[0][0]; + const result = await accountSupports7702('0xunknown'); + + expect(result).toBe(true); + }); + }); + describe('transaction data removal', () => { it('removes state', async () => { const controller = createController(); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 594dade958b..ed068b5446b 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -13,7 +13,6 @@ import { } from './constants'; import { QuoteRefresher } from './helpers/QuoteRefresher'; import type { - AccountSupports7702Callback, GetDelegationTransactionCallback, TransactionConfigCallback, TransactionData, @@ -23,6 +22,7 @@ import type { UpdateFiatPaymentRequest, UpdatePaymentTokenRequest, } from './types'; +import { KEYRING_TYPES_SUPPORTING_7702 } from './types'; import { getStrategyOrder } from './utils/feature-flags'; import { updateQuotes } from './utils/quotes'; import { updateSourceAmounts } from './utils/source-amounts'; @@ -54,8 +54,6 @@ export class TransactionPayController extends BaseController< TransactionPayControllerState, TransactionPayControllerMessenger > { - readonly #accountSupports7702: AccountSupports7702Callback; - readonly #getDelegationTransaction: GetDelegationTransactionCallback; readonly #getStrategy?: ( @@ -67,7 +65,6 @@ export class TransactionPayController extends BaseController< ) => TransactionPayStrategy[]; constructor({ - accountSupports7702, getDelegationTransaction, getStrategy, getStrategies, @@ -81,7 +78,6 @@ export class TransactionPayController extends BaseController< state: { ...getDefaultState(), ...state }, }); - this.#accountSupports7702 = accountSupports7702; this.#getDelegationTransaction = getDelegationTransaction; this.#getStrategy = getStrategy; this.#getStrategies = getStrategies; @@ -99,7 +95,7 @@ export class TransactionPayController extends BaseController< // eslint-disable-next-line no-new new QuoteRefresher({ - accountSupports7702: this.#accountSupports7702, + accountSupports7702: this.#accountSupports7702.bind(this), getStrategies: this.#getStrategiesWithFallback.bind(this), messenger, updateTransactionData: this.#updateTransactionData.bind(this), @@ -269,7 +265,7 @@ export class TransactionPayController extends BaseController< if (shouldUpdateQuotes) { updateQuotes({ - accountSupports7702: this.#accountSupports7702, + accountSupports7702: this.#accountSupports7702.bind(this), getStrategies: this.#getStrategiesWithFallback.bind(this), messenger: this.messenger, transactionData: this.state.transactionData[transactionId], @@ -279,6 +275,17 @@ export class TransactionPayController extends BaseController< } } + async #accountSupports7702(account: string): Promise { + const { keyrings } = this.messenger.call('KeyringController:getState'); + const keyring = keyrings.find((k) => + k.accounts.some((a) => a.toLowerCase() === account.toLowerCase()), + ); + + return keyring + ? (KEYRING_TYPES_SUPPORTING_7702 as string[]).includes(keyring.type) + : true; + } + #getStrategiesWithFallback( transaction: TransactionMeta, ): TransactionPayStrategy[] { diff --git a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts index 36408c3793b..55844910894 100644 --- a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts +++ b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts @@ -6,7 +6,7 @@ import type { AccountSupports7702Callback, TransactionPayControllerMessenger, TransactionPayControllerState, -} from '..'; +} from '../types'; import { TransactionPayStrategy } from '../constants'; import { projectLogger } from '../logger'; import type { UpdateTransactionDataCallback } from '../types'; diff --git a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts index 7144201245e..0f940caac53 100644 --- a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts +++ b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts @@ -26,7 +26,6 @@ const QUOTE_MOCK = { } as TransactionPayQuote; describe('TransactionPayPublishHook', () => { - const accountSupports7702Mock = jest.fn().mockResolvedValue(true); const isSmartTransactionMock = jest.fn(); const executeMock = jest.fn(); const getStrategyByNameMock = jest.mocked(getStrategyByName); @@ -34,6 +33,7 @@ describe('TransactionPayPublishHook', () => { const { messenger, getControllerStateMock, + getKeyringControllerStateMock, getTransactionControllerStateMock, updateTransactionMock, } = getMessengerMock(); @@ -52,10 +52,18 @@ describe('TransactionPayPublishHook', () => { beforeEach(() => { jest.resetAllMocks(); - accountSupports7702Mock.mockResolvedValue(true); + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: ['0xabc'], + metadata: { id: 'hd-keyring', name: 'HD Key Tree' }, + }, + ], + }); hook = new TransactionPayPublishHook({ - accountSupports7702: accountSupports7702Mock, isSmartTransaction: isSmartTransactionMock, messenger, }); @@ -146,6 +154,42 @@ describe('TransactionPayPublishHook', () => { expect(updateTransactionMock).not.toHaveBeenCalled(); }); + it('defaults to accountSupports7702 true when keyring not found', async () => { + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [], + }); + + await runHook(); + + expect(executeMock).toHaveBeenCalledWith( + expect.objectContaining({ + accountSupports7702: true, + }), + ); + }); + + it('sets accountSupports7702 false for hardware wallet keyring', async () => { + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'Ledger Hardware', + accounts: ['0xabc'], + metadata: { id: 'ledger', name: 'Ledger' }, + }, + ], + }); + + await runHook(); + + expect(executeMock).toHaveBeenCalledWith( + expect.objectContaining({ + accountSupports7702: false, + }), + ); + }); + it('throws errors from submit', async () => { executeMock.mockRejectedValue(new Error('Test error')); diff --git a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts index 3b45fdc000c..7bc1febdde2 100644 --- a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts +++ b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts @@ -6,10 +6,10 @@ import { createModuleLogger } from '@metamask/utils'; import { projectLogger } from '../logger'; import type { - AccountSupports7702Callback, TransactionPayControllerMessenger, TransactionPayQuote, } from '../types'; +import { KEYRING_TYPES_SUPPORTING_7702 } from '../types'; import { getStrategyByName } from '../utils/strategy'; import { updateTransaction } from '../utils/transaction'; @@ -24,18 +24,13 @@ export class TransactionPayPublishHook { readonly #messenger: TransactionPayControllerMessenger; - readonly #accountSupports7702: AccountSupports7702Callback; - constructor({ - accountSupports7702, isSmartTransaction, messenger, }: { - accountSupports7702: AccountSupports7702Callback; isSmartTransaction: (chainId: Hex) => boolean; messenger: TransactionPayControllerMessenger; }) { - this.#accountSupports7702 = accountSupports7702; this.#isSmartTransaction = isSmartTransaction; this.#messenger = messenger; } @@ -88,7 +83,14 @@ export class TransactionPayPublishHook { const strategy = getStrategyByName(quotes[0].strategy); const from = transactionMeta.txParams.from as Hex; - const accountSupports7702 = await this.#accountSupports7702(from); + + const { keyrings } = this.#messenger.call('KeyringController:getState'); + const keyring = keyrings.find((k) => + k.accounts.some((a) => a.toLowerCase() === from.toLowerCase()), + ); + const accountSupports7702 = keyring + ? (KEYRING_TYPES_SUPPORTING_7702 as string[]).includes(keyring.type) + : true; return await strategy.execute({ accountSupports7702, diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index 7ebda275cde..cf09614a154 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -6,6 +6,7 @@ import type { BridgeStatusControllerGetStateAction, BridgeStatusControllerSubmitTxAction, } from '@metamask/bridge-status-controller'; +import type { KeyringControllerGetStateAction } from '@metamask/keyring-controller'; import type { MessengerActions, MessengerEvents, @@ -131,6 +132,19 @@ export function getMessengerMock({ const getAssetsControllerStateMock = jest.fn(); + const getKeyringControllerStateMock: jest.MockedFn< + KeyringControllerGetStateAction['handler'] + > = jest.fn().mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: ['0x1234567890123456789012345678901234567891'], + metadata: { id: 'hd-keyring', name: 'HD Key Tree' }, + }, + ], + }); + const messenger: RootMessenger = new Messenger({ namespace: MOCK_ANY_NAMESPACE, }); @@ -252,6 +266,11 @@ export function getMessengerMock({ ); } + messenger.registerActionHandler( + 'KeyringController:getState', + getKeyringControllerStateMock, + ); + const publish = messenger.publish.bind(messenger); return { @@ -269,6 +288,7 @@ export function getMessengerMock({ getDelegationTransactionMock, getGasFeeControllerStateMock, getGasFeeTokensMock, + getKeyringControllerStateMock, getNetworkClientByIdMock, getRemoteFeatureFlagControllerStateMock, getStrategyMock, diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 1c77b8dd4ac..b8a96988d7f 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -12,7 +12,11 @@ import type { BridgeControllerActions } from '@metamask/bridge-controller'; import type { BridgeStatusControllerStateChangeEvent } from '@metamask/bridge-status-controller'; import type { BridgeStatusControllerActions } from '@metamask/bridge-status-controller'; import type { GasFeeControllerActions } from '@metamask/gas-fee-controller'; -import type { KeyringControllerSignTypedMessageAction } from '@metamask/keyring-controller'; +import type { + KeyringControllerGetStateAction, + KeyringControllerSignTypedMessageAction, + KeyringTypes, +} from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; import type { NetworkControllerFindNetworkClientIdByChainIdAction } from '@metamask/network-controller'; import type { NetworkControllerGetNetworkClientByIdAction } from '@metamask/network-controller'; @@ -47,6 +51,7 @@ export type AllowedActions = | BridgeStatusControllerActions | CurrencyRateControllerActions | GasFeeControllerActions + | KeyringControllerGetStateAction | KeyringControllerSignTypedMessageAction | NetworkControllerFindNetworkClientIdByChainIdAction | NetworkControllerGetNetworkClientByIdAction @@ -139,11 +144,17 @@ export type TransactionPayControllerMessenger = Messenger< /** Callback to check whether an account supports EIP-7702 authorization signing. */ export type AccountSupports7702Callback = (account: string) => Promise; +/** + * Keyring types that support EIP-7702 authorization signing. + * Hardware wallets, snap keyrings, and money keyrings do not support 7702. + */ +export const KEYRING_TYPES_SUPPORTING_7702: `${KeyringTypes}`[] = [ + 'HD Key Tree', + 'Simple Key Pair', +]; + /** Options for the TransactionPayController. */ export type TransactionPayControllerOptions = { - /** Callback to check whether an account supports EIP-7702. */ - accountSupports7702: AccountSupports7702Callback; - /** Callback to convert a transaction into a redeem delegation. */ getDelegationTransaction: GetDelegationTransactionCallback; From 4d22efd0716bb1f86940d9e57ac02b5dfa3b4f91 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 15:26:37 +0200 Subject: [PATCH 25/48] Update changelog --- packages/transaction-pay-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index f2910c77e7d..ca9da6b495b 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -41,7 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Resolve correct `networkClientId` for source chain in Relay execute flow ([#8492](https://github.com/MetaMask/core/pull/8492)) - Stop double-counting subsidized fees in Relay quote target amounts ([#8488](https://github.com/MetaMask/core/pull/8488)) -- Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on a new `accountSupports7702` callback option ([#8388](https://github.com/MetaMask/core/pull/8388)) +- Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) ## [19.2.0] From f22d0456abc7cdbb10ba95139c23e3588e9707e2 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 15:33:02 +0200 Subject: [PATCH 26/48] Update --- packages/transaction-pay-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index ca9da6b495b..ed472156440 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fall back from Across to later pay strategies when Across quotes would require a first-time EIP-7702 authorization list ([#8577](https://github.com/MetaMask/core/pull/8577)) +- Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) ## [19.3.0] @@ -41,7 +42,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Resolve correct `networkClientId` for source chain in Relay execute flow ([#8492](https://github.com/MetaMask/core/pull/8492)) - Stop double-counting subsidized fees in Relay quote target amounts ([#8488](https://github.com/MetaMask/core/pull/8488)) -- Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) ## [19.2.0] From 17bcb31b537d29892e14e6e6ecea6b84de37a446 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 15:36:01 +0200 Subject: [PATCH 27/48] Update lint --- .../src/helpers/QuoteRefresher.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts index 55844910894..207205fa67e 100644 --- a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts +++ b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts @@ -2,14 +2,14 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import { createModuleLogger } from '@metamask/utils'; import { noop } from 'lodash'; +import { TransactionPayStrategy } from '../constants'; +import { projectLogger } from '../logger'; import type { AccountSupports7702Callback, TransactionPayControllerMessenger, TransactionPayControllerState, + UpdateTransactionDataCallback, } from '../types'; -import { TransactionPayStrategy } from '../constants'; -import { projectLogger } from '../logger'; -import type { UpdateTransactionDataCallback } from '../types'; import { refreshQuotes } from '../utils/quotes'; const CHECK_INTERVAL = 1000; // 1 Second From 253c745c70c09390406bc29c64ab06cad28a191d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20G=C3=B6ktu=C4=9F=20Poyraz?= Date: Thu, 23 Apr 2026 11:04:48 +0200 Subject: [PATCH 28/48] Ogp/conf 1151 2 (#8558) - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > [Cursor Bugbot](https://cursor.com/bugbot) is generating a summary for commit c372c6d803e858425b31bd96687fb3e1089ac49b. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/transaction-controller/CHANGELOG.md | 4 +- .../transaction-controller/src/utils/batch.ts | 8 +- .../src/utils/eip7702.ts | 31 +++++++ .../transaction-pay-controller/CHANGELOG.md | 3 + .../src/TransactionPayController.test.ts | 90 ------------------- .../src/TransactionPayController.ts | 14 --- .../src/helpers/QuoteRefresher.test.ts | 7 -- .../src/helpers/QuoteRefresher.ts | 15 +--- .../src/helpers/TransactionPayPublishHook.ts | 12 +-- .../transaction-pay-controller/src/index.ts | 1 - .../src/strategy/across/across-quotes.ts | 5 +- .../src/strategy/across/across-submit.test.ts | 8 +- .../src/strategy/across/across-submit.ts | 13 +-- .../src/strategy/relay/relay-quotes.ts | 13 +-- .../src/strategy/relay/relay-submit.test.ts | 57 ++++++------ .../src/strategy/relay/relay-submit.ts | 30 ++----- .../transaction-pay-controller/src/types.ts | 3 - .../src/utils/7702.ts | 28 ++++++ .../src/utils/quote-gas.ts | 77 +++++++++------- .../src/utils/quotes.test.ts | 82 ++++++++++------- .../src/utils/quotes.ts | 11 +-- 21 files changed, 218 insertions(+), 294 deletions(-) create mode 100644 packages/transaction-pay-controller/src/utils/7702.ts diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 94938dc674e..0b83b4cfec6 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -11,7 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Snapshot `txParamsOriginal` in `updateEditableParams` when `containerTypes` are first applied ([#8546](https://github.com/MetaMask/core/pull/8546)) - Add `requiresAuthorizationList` to `TransactionController:estimateGasBatch` results when EIP-7702 batch gas estimation requires a first-time account upgrade ([#8577](https://github.com/MetaMask/core/pull/8577)) -- Add `KeyringControllerGetStateAction` to `AllowedActions` to support publish hooks that need to check keyring type for EIP-7702 compatibility ([#8388](https://github.com/MetaMask/core/pull/8388)) +- **BREAKING:** Add `KeyringControllerGetStateAction` to `AllowedActions` to enable keyring-based EIP-7702 account compatibility checks in `addTransactionBatch` ([#8388](https://github.com/MetaMask/core/pull/8388)) + - `addTransactionBatch` now automatically checks whether the account's keyring supports EIP-7702 before attempting the 7702 batch path, falling back to STX/sequential when unsupported + - Clients must add `KeyringController:getState` to the TransactionController messenger's allowed actions ### Fixed diff --git a/packages/transaction-controller/src/utils/batch.ts b/packages/transaction-controller/src/utils/batch.ts index 708f8bef792..637149d8932 100644 --- a/packages/transaction-controller/src/utils/batch.ts +++ b/packages/transaction-controller/src/utils/batch.ts @@ -51,6 +51,7 @@ import type { import type { TransactionBatchResult, TransactionParams } from '../types'; import { ERROR_MESSGE_PUBLIC_KEY, + doesAccountSupportEIP7702, doesChainSupportEIP7702, generateEIP7702BatchTransaction, isAccountUpgradedToEIP7702, @@ -136,7 +137,12 @@ export async function addTransactionBatch( log('Adding', transactionBatchRequest); - if (!transactionBatchRequest.disable7702) { + const accountCanUse7702 = doesAccountSupportEIP7702( + messenger, + transactionBatchRequest.from, + ); + + if (!transactionBatchRequest.disable7702 && accountCanUse7702) { try { return await addTransactionBatchWith7702(request); } catch (error: unknown) { diff --git a/packages/transaction-controller/src/utils/eip7702.ts b/packages/transaction-controller/src/utils/eip7702.ts index 5410997e04a..9ae339aa7ac 100644 --- a/packages/transaction-controller/src/utils/eip7702.ts +++ b/packages/transaction-controller/src/utils/eip7702.ts @@ -40,6 +40,37 @@ const ERC7579_EXEC_TYPE_TRY = '01'; const log = createModuleLogger(projectLogger, 'eip-7702'); +const KEYRING_TYPES_SUPPORTING_7702 = ['HD Key Tree', 'Simple Key Pair']; + +/** + * Check whether a given account's keyring supports EIP-7702 authorization + * signing. + * + * Looks up the account's keyring via `KeyringController:getState` and returns + * `true` only when the keyring type is in the supported list. + * Falls back to `true` when the keyring cannot be resolved. + * + * @param messenger - Controller messenger. + * @param account - The account address to check. + * @returns Whether the account supports EIP-7702. + */ +export function doesAccountSupportEIP7702( + messenger: TransactionControllerMessenger, + account: string, +): boolean { + const { keyrings } = messenger.call('KeyringController:getState'); + const keyring = keyrings.find( + (k: { type: string; accounts: string[] }) => + k.accounts.some( + (a: string) => a.toLowerCase() === account.toLowerCase(), + ), + ); + + return keyring + ? KEYRING_TYPES_SUPPORTING_7702.includes(keyring.type) + : true; +} + /** * Determine if a chain supports EIP-7702 using LaunchDarkly feature flag. * diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index ed472156440..66c616b1d04 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -35,6 +35,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ignore synthetic gas legs when determining Across support for perps direct deposits ([#8527](https://github.com/MetaMask/core/pull/8527)) - Route Across status polling through the configured Across API base and support `depositTxnRef`/`fillTxnRef` for Across status responses ([#8512](https://github.com/MetaMask/core/pull/8512)) +- **BREAKING:** Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) + - `AccountSupports7702Callback` type export has been removed. Use the `accountSupports7702` util from `utils/7702` instead. + - The `TransactionPayControllerMessenger` now requires `KeyringController:getState` permission (previously only needed in the publish hook). ## [19.2.1] diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 57be5c16900..eba792cae24 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -369,7 +369,6 @@ describe('TransactionPayController', () => { .mockResolvedValue(resultMock); new TransactionPayController({ - accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: getDelegationTransactionMock, messenger, }); @@ -400,7 +399,6 @@ describe('TransactionPayController', () => { it('returns callback value if provided', async () => { new TransactionPayController({ - accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: jest.fn(), getStrategy: (): TransactionPayStrategy => TransactionPayStrategy.Test, messenger, @@ -416,7 +414,6 @@ describe('TransactionPayController', () => { it('does not query feature flag strategy order when getStrategies callback returns values', async () => { new TransactionPayController({ - accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: jest.fn(), getStrategies: (): TransactionPayStrategy[] => [ TransactionPayStrategy.Test, @@ -440,7 +437,6 @@ describe('TransactionPayController', () => { getStrategyOrderMock.mockReturnValue([TransactionPayStrategy.Test]); new TransactionPayController({ - accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: jest.fn(), getStrategies: (): TransactionPayStrategy[] => [], messenger, @@ -458,7 +454,6 @@ describe('TransactionPayController', () => { getStrategyOrderMock.mockReturnValue([TransactionPayStrategy.Bridge]); new TransactionPayController({ - accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: jest.fn(), getStrategies: (): TransactionPayStrategy[] => [undefined] as unknown as TransactionPayStrategy[], @@ -596,7 +591,6 @@ describe('TransactionPayController', () => { ); expect(updateQuotesMock).toHaveBeenCalledWith({ - accountSupports7702: expect.any(Function), getStrategies: expect.any(Function), messenger, transactionData: expect.objectContaining({ @@ -608,90 +602,6 @@ describe('TransactionPayController', () => { }); }); - describe('accountSupports7702', () => { - it('returns true for HD keyring account', async () => { - const controller = createController(); - - controller.updatePaymentToken({ - transactionId: TRANSACTION_ID_MOCK, - tokenAddress: TOKEN_ADDRESS_MOCK, - chainId: CHAIN_ID_MOCK, - }); - - const { updateTransactionData } = updatePaymentTokenMock.mock.calls[0][1]; - - updateTransactionData(TRANSACTION_ID_MOCK, (data) => { - data.sourceAmounts = [ - { sourceAmountHuman: '1.23' } as TransactionPaySourceAmount, - ]; - }); - - const { accountSupports7702 } = updateQuotesMock.mock.calls[0][0]; - const result = await accountSupports7702( - '0x1234567890123456789012345678901234567891', - ); - - expect(result).toBe(true); - }); - - it('returns false for Ledger keyring account', async () => { - getKeyringControllerStateMock.mockReturnValue({ - isUnlocked: true, - keyrings: [ - { - type: 'Ledger Hardware', - accounts: ['0xledger'], - metadata: { id: 'ledger', name: 'Ledger' }, - }, - ], - }); - - const controller = createController(); - - controller.updatePaymentToken({ - transactionId: TRANSACTION_ID_MOCK, - tokenAddress: TOKEN_ADDRESS_MOCK, - chainId: CHAIN_ID_MOCK, - }); - - const { updateTransactionData } = updatePaymentTokenMock.mock.calls[0][1]; - - updateTransactionData(TRANSACTION_ID_MOCK, (data) => { - data.sourceAmounts = [ - { sourceAmountHuman: '1.23' } as TransactionPaySourceAmount, - ]; - }); - - const { accountSupports7702 } = updateQuotesMock.mock.calls[0][0]; - const result = await accountSupports7702('0xledger'); - - expect(result).toBe(false); - }); - - it('returns true when keyring is not found', async () => { - const controller = createController(); - - controller.updatePaymentToken({ - transactionId: TRANSACTION_ID_MOCK, - tokenAddress: TOKEN_ADDRESS_MOCK, - chainId: CHAIN_ID_MOCK, - }); - - const { updateTransactionData } = updatePaymentTokenMock.mock.calls[0][1]; - - updateTransactionData(TRANSACTION_ID_MOCK, (data) => { - data.sourceAmounts = [ - { sourceAmountHuman: '1.23' } as TransactionPaySourceAmount, - ]; - }); - - const { accountSupports7702 } = updateQuotesMock.mock.calls[0][0]; - const result = await accountSupports7702('0xunknown'); - - expect(result).toBe(true); - }); - }); - describe('transaction data removal', () => { it('removes state', async () => { const controller = createController(); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index ed068b5446b..d52a50466c8 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -22,7 +22,6 @@ import type { UpdateFiatPaymentRequest, UpdatePaymentTokenRequest, } from './types'; -import { KEYRING_TYPES_SUPPORTING_7702 } from './types'; import { getStrategyOrder } from './utils/feature-flags'; import { updateQuotes } from './utils/quotes'; import { updateSourceAmounts } from './utils/source-amounts'; @@ -95,7 +94,6 @@ export class TransactionPayController extends BaseController< // eslint-disable-next-line no-new new QuoteRefresher({ - accountSupports7702: this.#accountSupports7702.bind(this), getStrategies: this.#getStrategiesWithFallback.bind(this), messenger, updateTransactionData: this.#updateTransactionData.bind(this), @@ -265,7 +263,6 @@ export class TransactionPayController extends BaseController< if (shouldUpdateQuotes) { updateQuotes({ - accountSupports7702: this.#accountSupports7702.bind(this), getStrategies: this.#getStrategiesWithFallback.bind(this), messenger: this.messenger, transactionData: this.state.transactionData[transactionId], @@ -275,17 +272,6 @@ export class TransactionPayController extends BaseController< } } - async #accountSupports7702(account: string): Promise { - const { keyrings } = this.messenger.call('KeyringController:getState'); - const keyring = keyrings.find((k) => - k.accounts.some((a) => a.toLowerCase() === account.toLowerCase()), - ); - - return keyring - ? (KEYRING_TYPES_SUPPORTING_7702 as string[]).includes(keyring.type) - : true; - } - #getStrategiesWithFallback( transaction: TransactionMeta, ): TransactionPayStrategy[] { diff --git a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.test.ts b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.test.ts index 6b88a257ad3..1882d52d1d7 100644 --- a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.test.ts +++ b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.test.ts @@ -52,7 +52,6 @@ describe('QuoteRefresher', () => { it('polls if quotes detected in state', async () => { new QuoteRefresher({ - accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData: jest.fn(), @@ -68,7 +67,6 @@ describe('QuoteRefresher', () => { it('does not poll if no quotes in state', async () => { new QuoteRefresher({ - accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData: jest.fn(), @@ -84,7 +82,6 @@ describe('QuoteRefresher', () => { it('polls again after interval', async () => { new QuoteRefresher({ - accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData: jest.fn(), @@ -103,7 +100,6 @@ describe('QuoteRefresher', () => { it('stops polling if quotes removed', async () => { new QuoteRefresher({ - accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData: jest.fn(), @@ -122,7 +118,6 @@ describe('QuoteRefresher', () => { const updateTransactionData = jest.fn(); new QuoteRefresher({ - accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData, @@ -145,7 +140,6 @@ describe('QuoteRefresher', () => { const updateTransactionData = jest.fn(); new QuoteRefresher({ - accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData, @@ -172,7 +166,6 @@ describe('QuoteRefresher', () => { const updateTransactionData = jest.fn(); new QuoteRefresher({ - accountSupports7702: jest.fn().mockResolvedValue(true), getStrategies: jest.fn().mockReturnValue([TransactionPayStrategy.Relay]), messenger, updateTransactionData, diff --git a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts index 207205fa67e..ec93f5c22db 100644 --- a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts +++ b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts @@ -2,14 +2,13 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import { createModuleLogger } from '@metamask/utils'; import { noop } from 'lodash'; -import { TransactionPayStrategy } from '../constants'; -import { projectLogger } from '../logger'; import type { - AccountSupports7702Callback, TransactionPayControllerMessenger, TransactionPayControllerState, - UpdateTransactionDataCallback, -} from '../types'; +} from '..'; +import { TransactionPayStrategy } from '../constants'; +import { projectLogger } from '../logger'; +import type { UpdateTransactionDataCallback } from '../types'; import { refreshQuotes } from '../utils/quotes'; const CHECK_INTERVAL = 1000; // 1 Second @@ -31,24 +30,19 @@ export class QuoteRefresher { readonly #updateTransactionData: UpdateTransactionDataCallback; - readonly #accountSupports7702: AccountSupports7702Callback; - constructor({ getStrategies, messenger, updateTransactionData, - accountSupports7702, }: { getStrategies: (transaction: TransactionMeta) => TransactionPayStrategy[]; messenger: TransactionPayControllerMessenger; updateTransactionData: UpdateTransactionDataCallback; - accountSupports7702: AccountSupports7702Callback; }) { this.#getStrategies = getStrategies; this.#messenger = messenger; this.#isRunning = false; this.#isUpdating = false; - this.#accountSupports7702 = accountSupports7702; this.#updateTransactionData = updateTransactionData; messenger.subscribe( @@ -87,7 +81,6 @@ export class QuoteRefresher { this.#messenger, this.#updateTransactionData, this.#getStrategies, - this.#accountSupports7702, ); } catch (error) { log('Error refreshing quotes', error); diff --git a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts index 7bc1febdde2..4988a742df8 100644 --- a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts +++ b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts @@ -9,7 +9,7 @@ import type { TransactionPayControllerMessenger, TransactionPayQuote, } from '../types'; -import { KEYRING_TYPES_SUPPORTING_7702 } from '../types'; +import { accountSupports7702 } from '../utils/7702'; import { getStrategyByName } from '../utils/strategy'; import { updateTransaction } from '../utils/transaction'; @@ -84,16 +84,8 @@ export class TransactionPayPublishHook { const strategy = getStrategyByName(quotes[0].strategy); const from = transactionMeta.txParams.from as Hex; - const { keyrings } = this.#messenger.call('KeyringController:getState'); - const keyring = keyrings.find((k) => - k.accounts.some((a) => a.toLowerCase() === from.toLowerCase()), - ); - const accountSupports7702 = keyring - ? (KEYRING_TYPES_SUPPORTING_7702 as string[]).includes(keyring.type) - : true; - return await strategy.execute({ - accountSupports7702, + accountSupports7702: accountSupports7702(this.#messenger, from), isSmartTransaction: this.#isSmartTransaction, quotes, messenger: this.#messenger, diff --git a/packages/transaction-pay-controller/src/index.ts b/packages/transaction-pay-controller/src/index.ts index 930cb9c3b9f..53cc04fa203 100644 --- a/packages/transaction-pay-controller/src/index.ts +++ b/packages/transaction-pay-controller/src/index.ts @@ -1,5 +1,4 @@ export type { - AccountSupports7702Callback, TransactionConfig, TransactionConfigCallback, TransactionFiatPayment, diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts index 015f2e810b5..deac72d753d 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -189,7 +189,7 @@ async function normalizeQuote( request: QuoteRequest, fullRequest: PayStrategyGetQuotesRequest, ): Promise> { - const { accountSupports7702, messenger } = fullRequest; + const { messenger } = fullRequest; const { quote } = original; const { usdToFiatRate, sourceFiatRate, targetFiatRate } = getFiatRates( @@ -204,7 +204,6 @@ async function normalizeQuote( quote, messenger, request, - accountSupports7702, ); const targetNetwork = getFiatValueFromUsd(new BigNumber(0), usdToFiatRate); @@ -388,7 +387,6 @@ async function calculateSourceNetworkCost( quote: AcrossSwapApprovalResponse, messenger: TransactionPayControllerMessenger, request: QuoteRequest, - accountSupports7702: boolean, ): Promise<{ sourceNetwork: TransactionPayQuote['fees']['sourceNetwork']; gasLimits: AcrossGasLimits; @@ -412,7 +410,6 @@ async function calculateSourceNetworkCost( to: transaction.to, value: transaction.value ?? '0x0', })), - accountSupports7702, }); const { batchGasLimit, is7702 } = gasEstimates; diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts index 60eeae06991..1de7d310d92 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts @@ -241,7 +241,7 @@ describe('Across Submit', () => { expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ disable7702: false, - disableHook: false, + disableHook: true, disableSequential: true, gasLimit7702: toHex(64000), transactions: [ @@ -262,7 +262,7 @@ describe('Across Submit', () => { ); }); - it('submits batch sequentially when account does not support 7702', async () => { + it('submits batch without 7702 when quote is7702 is false', async () => { const nonIs7702Quote = { ...QUOTE_MOCK, original: { @@ -278,7 +278,7 @@ describe('Across Submit', () => { } as unknown as TransactionPayQuote; await submitAcrossQuotes({ - accountSupports7702: false, + accountSupports7702: true, messenger, quotes: [nonIs7702Quote], transaction: TRANSACTION_META_MOCK, @@ -289,7 +289,9 @@ describe('Across Submit', () => { expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ disable7702: true, + disableHook: false, disableSequential: false, + gasLimit7702: undefined, }), ); expect(addTransactionMock).not.toHaveBeenCalled(); diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts index f0ccc91d389..78d3cd17a5c 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts @@ -48,8 +48,6 @@ export async function submitAcrossQuotes( log('Executing quotes', request); const { quotes, messenger, transaction } = request; - const { accountSupports7702 } = request; - let transactionHash: Hex | undefined; for (const quote of quotes) { @@ -57,7 +55,6 @@ export async function submitAcrossQuotes( quote, messenger, transaction, - accountSupports7702, )); } @@ -68,7 +65,6 @@ async function executeSingleQuote( quote: TransactionPayQuote, messenger: TransactionPayControllerMessenger, transaction: TransactionMeta, - accountSupports7702: boolean, ): Promise<{ transactionHash?: Hex }> { log('Executing single quote', quote); @@ -89,7 +85,6 @@ async function executeSingleQuote( transaction.id, acrossDepositType, messenger, - accountSupports7702, ); updateTransaction( @@ -113,7 +108,6 @@ async function executeSingleQuote( * @param parentTransactionId - ID of the parent transaction. * @param acrossDepositType - Transaction type used for the swap/deposit step. * @param messenger - Controller messenger. - * @param accountSupports7702 - Whether the account supports EIP-7702. * @returns Hash of the last submitted transaction, if available. */ async function submitTransactions( @@ -121,13 +115,10 @@ async function submitTransactions( parentTransactionId: string, acrossDepositType: TransactionType, messenger: TransactionPayControllerMessenger, - accountSupports7702: boolean, ): Promise { const { swapTx } = quote.original.quote; - const { gasLimits: quoteGasLimits, is7702: apiIs7702 } = - quote.original.metamask; + const { gasLimits: quoteGasLimits, is7702 } = quote.original.metamask; const { from } = quote.request; - const is7702 = apiIs7702 && accountSupports7702; const chainId = toHex(swapTx.chainId); const orderedTransactions = getAcrossOrderedTransactions({ quote: quote.original.quote, @@ -224,7 +215,7 @@ async function submitTransactions( await messenger.call('TransactionController:addTransactionBatch', { disable7702: !gasLimit7702, - disableHook: !gasLimit7702, + disableHook: Boolean(gasLimit7702), disableSequential: Boolean(gasLimit7702), from, gasLimit7702, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 1dc9e74b1b4..6b62bf8e506 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -221,8 +221,10 @@ async function getSingleQuote( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const useExactInput = isMaxAmount || request.isPostQuote; + const { accountSupports7702: supports7702 } = fullRequest; + const useExecute = - fullRequest.accountSupports7702 && + supports7702 && isRelayExecuteEnabled(messenger) && isEIP7702Chain(messenger, sourceChainId); @@ -454,7 +456,6 @@ async function normalizeQuote( messenger, request, fullRequest.transaction, - fullRequest.accountSupports7702, ); const targetNetwork = { @@ -590,7 +591,6 @@ function getFiatRates( * @param messenger - Controller messenger. * @param request - Quote request. * @param transaction - Original transaction metadata. - * @param accountSupports7702 - Whether the account supports EIP-7702. * @returns Total source network cost in USD and fiat. */ async function calculateSourceNetworkCost( @@ -598,7 +598,6 @@ async function calculateSourceNetworkCost( messenger: TransactionPayControllerMessenger, request: QuoteRequest, transaction: TransactionMeta, - accountSupports7702: boolean, ): Promise< TransactionPayQuote['fees']['sourceNetwork'] & { gasLimits: number[]; @@ -655,7 +654,6 @@ async function calculateSourceNetworkCost( relayParams, messenger, fromOverride, - accountSupports7702, ); const { gasLimits, is7702, totalGasEstimate, totalGasLimit } = @@ -807,14 +805,12 @@ async function calculateSourceNetworkCost( * @param fromOverride - Optional address to use as `from` in gas estimation * instead of the address in the relay params. Used in predict withdraw flows * to estimate with the proxy/Safe address that holds the source token balance. - * @param accountSupports7702 - Whether the account supports EIP-7702. * @returns Total gas estimates and per-transaction gas limits. */ async function calculateSourceNetworkGasLimit( params: RelayTransactionStep['items'][0]['data'][], messenger: TransactionPayControllerMessenger, - fromOverride: Hex | undefined, - accountSupports7702: boolean, + fromOverride?: Hex, ): Promise<{ totalGasEstimate: number; totalGasLimit: number; @@ -826,7 +822,6 @@ async function calculateSourceNetworkGasLimit( ); const relayGasResult = await estimateQuoteGasLimits({ - accountSupports7702, fallbackGas: getFeatureFlags(messenger).relayFallbackGas, fallbackOnSimulationFailure: true, messenger, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index 9c1d1a8cc8c..356b614799d 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -465,8 +465,8 @@ describe('Relay Submit Utils', () => { ); }); - it('does not add authorization list when account does not support 7702', async () => { - request.accountSupports7702 = false; + it('does not add authorization list when quote is7702 is false', async () => { + request.quotes[0].original.metamask.is7702 = false; request.quotes[0].original.details.currencyOut.currency.chainId = 1; request.quotes[0].original.request = { authorizationList: [ @@ -502,12 +502,14 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); expect(addTransactionBatchMock).toHaveBeenCalledWith({ disable7702: true, - disableHook: true, + disableHook: false, disableSequential: false, from: FROM_MOCK, + gasFeeToken: undefined, + gasLimit7702: undefined, networkClientId: NETWORK_CLIENT_ID_MOCK, origin: ORIGIN_METAMASK, - overwriteUpgrade: false, + overwriteUpgrade: true, requireApproval: false, transactions: [ { @@ -899,11 +901,14 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ + disable7702: true, + disableHook: false, + disableSequential: false, from: FROM_MOCK, gasFeeToken: undefined, networkClientId: NETWORK_CLIENT_ID_MOCK, origin: ORIGIN_METAMASK, - overwriteUpgrade: false, + overwriteUpgrade: true, requireApproval: false, transactions: [ { @@ -1013,9 +1018,10 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ disable7702: true, - disableHook: true, + disableHook: false, disableSequential: false, gasLimit7702: undefined, + overwriteUpgrade: true, transactions: [ expect.objectContaining({ params: expect.objectContaining({ @@ -1042,10 +1048,8 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ - disable7702: false, - disableHook: false, - disableSequential: true, gasLimit7702: '0x31955', + overwriteUpgrade: true, transactions: [ expect.objectContaining({ params: expect.objectContaining({ @@ -1078,10 +1082,8 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ - disable7702: false, - disableHook: false, - disableSequential: true, gasLimit7702: '0xa410', + overwriteUpgrade: true, transactions: [ expect.objectContaining({ params: expect.objectContaining({ @@ -1098,9 +1100,7 @@ describe('Relay Submit Utils', () => { ); }); - it('submits batch sequentially when account does not support 7702', async () => { - request.accountSupports7702 = false; - + it('uses 7702 mode based on quote metadata regardless of account support', async () => { request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], }); @@ -1113,16 +1113,14 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ - disable7702: true, - disableSequential: false, + overwriteUpgrade: true, + gasLimit7702: '0x5208', }), ); expect(addTransactionMock).not.toHaveBeenCalled(); }); it('omits per-transaction gas when entry is missing in batch submission', async () => { - request.accountSupports7702 = false; - request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], }); @@ -1149,9 +1147,7 @@ describe('Relay Submit Utils', () => { ); }); - it('waits for on-chain confirmation for non-7702 accounts with multiple transactions', async () => { - request.accountSupports7702 = false; - + it('waits for on-chain confirmation with multiple transactions', async () => { request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], }); @@ -1164,9 +1160,7 @@ describe('Relay Submit Utils', () => { expect(waitForTransactionConfirmedMock).toHaveBeenCalled(); }); - it('uses addTransactionBatch for non-7702 accounts with multiple transactions', async () => { - request.accountSupports7702 = false; - + it('uses addTransactionBatch with multiple transactions', async () => { request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], }); @@ -1179,21 +1173,19 @@ describe('Relay Submit Utils', () => { expect(addTransactionMock).not.toHaveBeenCalled(); }); - it('still waits for on-chain confirmation for 7702 accounts with multiple transactions', async () => { - request.accountSupports7702 = true; - + it('waits for on-chain confirmation with multiple transactions in 7702 mode', async () => { request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], }); + request.quotes[0].original.metamask.is7702 = true; + await submitRelayQuotes(request); expect(waitForTransactionConfirmedMock).toHaveBeenCalled(); }); - it('still waits for on-chain confirmation for non-7702 accounts with single transaction', async () => { - request.accountSupports7702 = false; - + it('waits for on-chain confirmation with single transaction', async () => { await submitRelayQuotes(request); expect(addTransactionMock).toHaveBeenCalledTimes(1); @@ -1213,9 +1205,10 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ disable7702: true, - disableHook: true, + disableHook: false, disableSequential: false, gasLimit7702: undefined, + overwriteUpgrade: true, transactions: [ expect.objectContaining({ params: expect.objectContaining({ diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 661b3486ab7..9694ce83a99 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -61,7 +61,6 @@ export async function submitRelayQuotes( log('Executing quotes', request); const { quotes, messenger, transaction } = request; - const { accountSupports7702 } = request; let transactionHash: Hex | undefined; @@ -70,7 +69,6 @@ export async function submitRelayQuotes( quote, messenger, transaction, - accountSupports7702, )); } @@ -83,14 +81,12 @@ export async function submitRelayQuotes( * @param quote - Relay quote to execute. * @param messenger - Controller messenger. * @param transaction - Original transaction meta. - * @param accountSupports7702 - Whether the account supports EIP-7702. * @returns An object containing the transaction hash if available. */ async function executeSingleQuote( quote: TransactionPayQuote, messenger: TransactionPayControllerMessenger, transaction: TransactionMeta, - accountSupports7702: boolean, ): Promise<{ transactionHash?: Hex }> { log('Executing single quote', quote); @@ -108,12 +104,7 @@ async function executeSingleQuote( if (quote.request.isHyperliquidSource) { await submitHyperliquidWithdraw(quote, quote.request.from, messenger); } else { - await submitTransactions( - quote, - transaction, - messenger, - accountSupports7702, - ); + await submitTransactions(quote, transaction, messenger); } const targetHash = await waitForRelayCompletion( @@ -317,14 +308,12 @@ async function validateSourceBalance( * @param quote - Relay quote. * @param transaction - Original transaction meta. * @param messenger - Controller messenger. - * @param accountSupports7702 - Whether the account supports EIP-7702. * @returns Hash of the last submitted transaction. */ async function submitTransactions( quote: TransactionPayQuote, transaction: TransactionMeta, messenger: TransactionPayControllerMessenger, - accountSupports7702: boolean, ): Promise { const { steps } = quote.original; const txSteps = steps.filter( @@ -382,7 +371,6 @@ async function submitTransactions( quote, transaction, messenger, - accountSupports7702, normalizedParams, allParams, ); @@ -482,7 +470,6 @@ async function submitViaRelayExecute( * @param quote - Relay quote. * @param transaction - Original transaction meta. * @param messenger - Controller messenger. - * @param accountSupports7702 - Whether the account supports EIP-7702. * @param normalizedParams - Normalized relay-only params (without prepended original tx). * @param allParams - All params including any prepended original tx for post-quote flows. * @returns Hash of the last submitted transaction. @@ -491,7 +478,6 @@ async function submitViaTransactionController( quote: TransactionPayQuote, transaction: TransactionMeta, messenger: TransactionPayControllerMessenger, - accountSupports7702: boolean, normalizedParams: TransactionParams[], allParams: TransactionParams[], ): Promise { @@ -549,9 +535,7 @@ async function submitViaTransactionController( quote.original.details.currencyOut.currency.chainId; const authorizationList: AuthorizationList | undefined = - accountSupports7702 && - isSameChain && - quote.original.request.authorizationList?.length + isSameChain && quote.original.request.authorizationList?.length ? quote.original.request.authorizationList.map((a) => ({ address: a.address, chainId: toHex(a.chainId), @@ -561,8 +545,6 @@ async function submitViaTransactionController( const { metamask } = quote.original; const { gasLimits } = metamask; - const is7702 = metamask.is7702 && accountSupports7702; - if (allParams.length === 1) { const transactionParams = { ...allParams[0], @@ -582,7 +564,9 @@ async function submitViaTransactionController( }, ); } else { - const gasLimit7702 = is7702 ? toHex(metamask.gasLimits[0]) : undefined; + const gasLimit7702 = metamask.is7702 + ? toHex(metamask.gasLimits[0]) + : undefined; const transactions = allParams.map((singleParams, index) => { const gasLimit = gasLimits[index]; @@ -610,13 +594,13 @@ async function submitViaTransactionController( await messenger.call('TransactionController:addTransactionBatch', { from, disable7702: !gasLimit7702, - disableHook: !gasLimit7702, + disableHook: Boolean(gasLimit7702), disableSequential: Boolean(gasLimit7702), gasFeeToken, gasLimit7702, networkClientId, origin: ORIGIN_METAMASK, - overwriteUpgrade: is7702, + overwriteUpgrade: true, requireApproval: false, transactions, }); diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index b8a96988d7f..7c6171125b4 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -141,9 +141,6 @@ export type TransactionPayControllerMessenger = Messenger< TransactionPayControllerEvents | AllowedEvents >; -/** Callback to check whether an account supports EIP-7702 authorization signing. */ -export type AccountSupports7702Callback = (account: string) => Promise; - /** * Keyring types that support EIP-7702 authorization signing. * Hardware wallets, snap keyrings, and money keyrings do not support 7702. diff --git a/packages/transaction-pay-controller/src/utils/7702.ts b/packages/transaction-pay-controller/src/utils/7702.ts new file mode 100644 index 00000000000..277bda810bf --- /dev/null +++ b/packages/transaction-pay-controller/src/utils/7702.ts @@ -0,0 +1,28 @@ +import type { TransactionPayControllerMessenger } from '../types'; +import { KEYRING_TYPES_SUPPORTING_7702 } from '../types'; + +/** + * Check whether a given account supports EIP-7702 authorization signing. + * + * Looks up the account's keyring via `KeyringController:getState` and returns + * `true` only when the keyring type is in the supported list (HD Key Tree, + * Simple Key Pair). Hardware wallets, snap keyrings, and other types return + * `false`. Falls back to `true` when the keyring cannot be resolved. + * + * @param messenger - Controller messenger used to call KeyringController. + * @param account - The account address to check. + * @returns Whether the account supports EIP-7702. + */ +export function accountSupports7702( + messenger: TransactionPayControllerMessenger, + account: string, +): boolean { + const { keyrings } = messenger.call('KeyringController:getState'); + const keyring = keyrings.find((k) => + k.accounts.some((a) => a.toLowerCase() === account.toLowerCase()), + ); + + return keyring + ? (KEYRING_TYPES_SUPPORTING_7702 as string[]).includes(keyring.type) + : true; +} diff --git a/packages/transaction-pay-controller/src/utils/quote-gas.ts b/packages/transaction-pay-controller/src/utils/quote-gas.ts index af053075faa..f2c2aff7aa3 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.ts @@ -6,6 +6,7 @@ import { BigNumber } from 'bignumber.js'; import type { TransactionPayControllerMessenger } from '..'; import { projectLogger } from '../logger'; +import { accountSupports7702 } from './7702'; import { getGasBuffer } from './feature-flags'; import { estimateGasLimit } from './gas'; @@ -26,13 +27,11 @@ export type QuoteGasLimit = { }; export async function estimateQuoteGasLimits({ - accountSupports7702 = true, fallbackGas, fallbackOnSimulationFailure = false, messenger, transactions, }: { - accountSupports7702?: boolean; fallbackGas?: { estimate: number; max: number; @@ -56,37 +55,12 @@ export async function estimateQuoteGasLimits({ const useBatch = transactions.length > 1; if (useBatch) { - const result = await estimateQuoteGasLimitsBatch(transactions, messenger); - - // If the batch returned a combined 7702 gas limit but the account cannot - // sign EIP-7702 authorizations (e.g. hardware wallet), re-estimate each - // transaction individually so callers receive per-transaction gas limits. - if (result.is7702 && !accountSupports7702) { - const individualResults = await Promise.all( - transactions.map((transaction) => - estimateQuoteGasLimitSingle({ - fallbackGas, - fallbackOnSimulationFailure, - messenger, - transaction, - }), - ), - ); - - return { - gasLimits: individualResults.map((res) => res.gasLimits[0]), - is7702: false, - totalGasEstimate: individualResults.reduce( - (acc, res) => acc + res.totalGasEstimate, - 0, - ), - totalGasLimit: individualResults.reduce( - (acc, res) => acc + res.totalGasLimit, - 0, - ), - usedBatch: true, - }; - } + const result = await estimateQuoteGasLimitsBatch( + transactions, + messenger, + fallbackGas, + fallbackOnSimulationFailure, + ); return { ...result, @@ -109,6 +83,8 @@ export async function estimateQuoteGasLimits({ async function estimateQuoteGasLimitsBatch( transactions: QuoteGasTransaction[], messenger: TransactionPayControllerMessenger, + fallbackGas?: { estimate: number; max: number }, + fallbackOnSimulationFailure?: boolean, ): Promise<{ batchGasLimit?: QuoteGasLimit; gasLimits: QuoteGasLimit[]; @@ -161,6 +137,41 @@ async function estimateQuoteGasLimitsBatch( 0, ); const is7702 = bufferedGasLimits.length === 1; + + // If the batch returned a combined 7702 gas limit but the account cannot + // sign EIP-7702 authorizations (e.g. hardware wallet), re-estimate each + // transaction individually so callers never see is7702=true. + const supports7702 = accountSupports7702( + messenger, + firstTransaction.from, + ); + + if (is7702 && !supports7702) { + const individualResults = await Promise.all( + transactions.map((transaction) => + estimateQuoteGasLimitSingle({ + fallbackGas, + fallbackOnSimulationFailure: fallbackOnSimulationFailure ?? false, + messenger, + transaction, + }), + ), + ); + + return { + gasLimits: individualResults.map((res) => res.gasLimits[0]), + is7702: false, + totalGasEstimate: individualResults.reduce( + (acc, res) => acc + res.totalGasEstimate, + 0, + ), + totalGasLimit: individualResults.reduce( + (acc, res) => acc + res.totalGasLimit, + 0, + ), + }; + } + const batchGasLimit = is7702 ? bufferedGasLimits[0] : undefined; return { diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index fe30f8cf095..6de69992815 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -7,7 +7,6 @@ import { cloneDeep } from 'lodash'; import { TransactionPayStrategy } from '../constants'; import { getMessengerMock } from '../tests/messenger-mock'; import type { - AccountSupports7702Callback, TransactionPaySourceAmount, TransactionData, TransactionPayQuote, @@ -103,8 +102,6 @@ const BATCH_TRANSACTION_MOCK = { describe('Quotes Utils', () => { const { messenger, getControllerStateMock } = getMessengerMock(); - const accountSupports7702Mock: jest.MockedFunction = - jest.fn(); const updateTransactionDataMock = jest.fn(); const getStrategyByNameMock = jest.mocked(getStrategyByName); const getStrategiesByNameMock = jest.mocked(getStrategiesByName); @@ -127,7 +124,6 @@ describe('Quotes Utils', () => { */ async function run(params?: Partial): Promise { return await updateQuotes({ - accountSupports7702: accountSupports7702Mock, getStrategies: getStrategiesMock, messenger, transactionData: cloneDeep(TRANSACTION_DATA_MOCK), @@ -181,7 +177,6 @@ describe('Quotes Utils', () => { getQuotesMock.mockResolvedValue([QUOTE_MOCK]); getBatchTransactionsMock.mockResolvedValue([BATCH_TRANSACTION_MOCK]); calculateTotalsMock.mockReturnValue(TOTALS_MOCK); - accountSupports7702Mock.mockResolvedValue(true); getLiveTokenBalanceMock.mockResolvedValue('5000000'); getTokenFiatRateMock.mockReturnValue({ @@ -605,6 +600,31 @@ describe('Quotes Utils', () => { }); }); + it('gets quotes with no minimum if allowUnderMinimum is true', async () => { + await run({ + transactionData: { + ...TRANSACTION_DATA_MOCK, + tokens: [ + { + ...TRANSACTION_DATA_MOCK.tokens?.[0], + allowUnderMinimum: true, + } as TransactionPayRequiredToken, + ], + }, + }); + + expect(getQuotesMock).toHaveBeenCalledWith({ + accountSupports7702: true, + messenger, + requests: [ + expect.objectContaining({ + targetAmountMinimum: '0', + }), + ], + transaction: TRANSACTION_META_MOCK, + }); + }); + it('resolves strategies via getStrategiesByName', async () => { await run(); @@ -667,9 +687,7 @@ describe('Quotes Utils', () => { ); }); - it('clears batch transactions when account does not support 7702', async () => { - accountSupports7702Mock.mockResolvedValue(false); - + it('always passes batch transactions regardless of 7702 support', async () => { await run(); const transactionMetaMock = {} as TransactionMeta; @@ -677,8 +695,8 @@ describe('Quotes Utils', () => { expect(transactionMetaMock).toMatchObject( expect.objectContaining({ - batchTransactions: undefined, - batchTransactionsOptions: undefined, + batchTransactions: [BATCH_TRANSACTION_MOCK], + batchTransactionsOptions: {}, }), ); }); @@ -864,7 +882,6 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, - accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(4); @@ -894,7 +911,6 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, - accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(4); @@ -925,7 +941,6 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, - accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(0); @@ -947,7 +962,6 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, - accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(0); @@ -999,25 +1013,27 @@ describe('Quotes Utils', () => { transactionData: POST_QUOTE_TRANSACTION_DATA, }); - expect(getQuotesMock).toHaveBeenCalledWith({ - accountSupports7702: true, - messenger, - requests: [ - { - from: TRANSACTION_META_MOCK.txParams.from, - isMaxAmount: false, - isPostQuote: true, - sourceBalanceRaw: SOURCE_TOKEN_MOCK.balanceRaw, - sourceChainId: SOURCE_TOKEN_MOCK.chainId, - sourceTokenAddress: SOURCE_TOKEN_MOCK.address, - sourceTokenAmount: '10000000', - targetAmountMinimum: '0', - targetChainId: DESTINATION_TOKEN_MOCK.chainId, - targetTokenAddress: DESTINATION_TOKEN_MOCK.address, - }, - ], - transaction: TRANSACTION_META_MOCK, - }); + expect(getQuotesMock).toHaveBeenCalledWith( + expect.objectContaining({ + accountSupports7702: true, + messenger, + requests: [ + { + from: TRANSACTION_META_MOCK.txParams.from, + isMaxAmount: false, + isPostQuote: true, + sourceBalanceRaw: SOURCE_TOKEN_MOCK.balanceRaw, + sourceChainId: SOURCE_TOKEN_MOCK.chainId, + sourceTokenAddress: SOURCE_TOKEN_MOCK.address, + sourceTokenAmount: '10000000', + targetAmountMinimum: '0', + targetChainId: DESTINATION_TOKEN_MOCK.chainId, + targetTokenAddress: DESTINATION_TOKEN_MOCK.address, + }, + ], + transaction: TRANSACTION_META_MOCK, + }), + ); }); it('includes refundTo in post-quote request when set in transaction data', async () => { diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index e873ea1f037..3c79e6df39a 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -7,7 +7,6 @@ import { createModuleLogger } from '@metamask/utils'; import { TransactionPayStrategy } from '../constants'; import { projectLogger } from '../logger'; import type { - AccountSupports7702Callback, QuoteRequest, TransactionData, TransactionPayControllerMessenger, @@ -18,6 +17,7 @@ import type { TransactionPaymentToken, UpdateTransactionDataCallback, } from '../types'; +import { accountSupports7702 } from './7702'; import { checkStrategyQuoteSupport, checkStrategySupport, @@ -37,7 +37,6 @@ const DEFAULT_REFRESH_INTERVAL = 30 * 1000; // 30 Seconds const log = createModuleLogger(projectLogger, 'quotes'); export type UpdateQuotesRequest = { - accountSupports7702: AccountSupports7702Callback; getStrategies: (transaction: TransactionMeta) => TransactionPayStrategy[]; messenger: TransactionPayControllerMessenger; transactionData: TransactionData | undefined; @@ -55,7 +54,6 @@ export async function updateQuotes( request: UpdateQuotesRequest, ): Promise { const { - accountSupports7702, getStrategies, messenger, transactionData, @@ -113,7 +111,7 @@ export async function updateQuotes( transactionId, }); - const supports7702 = await accountSupports7702(from); + const supports7702 = accountSupports7702(messenger, from); const { batchTransactions, quotes } = await getQuotes( transaction, @@ -135,7 +133,7 @@ export async function updateQuotes( log('Calculated totals', { transactionId, totals }); syncTransaction({ - batchTransactions: supports7702 ? batchTransactions : undefined, + batchTransactions, isPostQuote, messenger: messenger as never, paymentToken, @@ -216,13 +214,11 @@ function syncTransaction({ * @param messenger - Messenger instance. * @param updateTransactionData - Callback to update transaction data. * @param getStrategies - Callback to get ordered strategy names for a transaction. - * @param accountSupports7702 - Callback to check account EIP-7702 support. */ export async function refreshQuotes( messenger: TransactionPayControllerMessenger, updateTransactionData: UpdateTransactionDataCallback, getStrategies: (transaction: TransactionMeta) => TransactionPayStrategy[], - accountSupports7702: AccountSupports7702Callback, ): Promise { const state = messenger.call('TransactionPayController:getState'); const transactionIds = Object.keys(state.transactionData); @@ -251,7 +247,6 @@ export async function refreshQuotes( } const isUpdated = await updateQuotes({ - accountSupports7702, getStrategies, messenger, transactionData, From bcb537fcff024b7fcdef18d821b8c14ac145002c Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Thu, 23 Apr 2026 12:02:21 +0200 Subject: [PATCH 29/48] Update --- .../src/strategy/across/across-quotes.test.ts | 31 +++- .../src/strategy/across/across-submit.test.ts | 35 ---- .../src/strategy/relay/relay-quotes.test.ts | 13 ++ .../src/strategy/relay/relay-submit.test.ts | 163 ++---------------- .../src/utils/quote-gas.test.ts | 65 ++++++- .../src/utils/quote-gas.ts | 4 +- .../src/utils/quotes.test.ts | 89 +++++----- 7 files changed, 157 insertions(+), 243 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts index 7e376cbb795..f03bbc73726 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts @@ -168,12 +168,24 @@ describe('Across Quotes', () => { estimateGasMock, estimateGasBatchMock, findNetworkClientIdByChainIdMock, + getKeyringControllerStateMock, getRemoteFeatureFlagControllerStateMock, } = getMessengerMock(); beforeEach(() => { jest.resetAllMocks(); + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: ['0x1234567890123456789012345678901234567891'], + metadata: { id: 'hd-keyring', name: 'HD Key Tree' }, + }, + ], + }); + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { @@ -1292,6 +1304,17 @@ describe('Across Quotes', () => { }); it('re-estimates individually when batch returns 7702 but account does not support it', async () => { + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'Ledger Hardware', + accounts: ['0x1234567890123456789012345678901234567891'], + metadata: { id: 'ledger', name: 'Ledger' }, + }, + ], + }); + estimateGasBatchMock.mockResolvedValue({ totalGasLimit: 51000, gasLimits: [51000], @@ -1622,7 +1645,7 @@ describe('Across Quotes', () => { transaction: TRANSACTION_META_MOCK, }); - expect(result[0].targetAmount.usd).toBe('0.0003'); + expect(result[0].targetAmount.raw).toBe('150'); expect(result[0].dust.usd).toBe('0'); expect(result[0].fees.provider.usd).toBe('0'); expect(result[0].fees.provider.fiat).toBe('0'); @@ -1668,7 +1691,9 @@ describe('Across Quotes', () => { transaction: TRANSACTION_META_MOCK, }); - expect(result[0].targetAmount.usd).toBe('0.000246'); + expect(result[0].targetAmount.raw).toBe( + QUOTE_REQUEST_MOCK.targetAmountMinimum, + ); }); it('handles missing target amount minimum for max amount requests', async () => { @@ -1693,7 +1718,7 @@ describe('Across Quotes', () => { transaction: TRANSACTION_META_MOCK, }); - expect(result[0].targetAmount.usd).toBe('0'); + expect(result[0].targetAmount.raw).toBe('0'); }); it('uses from address as recipient when no transfer data', async () => { diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts index 1de7d310d92..b6247e09966 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.test.ts @@ -262,41 +262,6 @@ describe('Across Submit', () => { ); }); - it('submits batch without 7702 when quote is7702 is false', async () => { - const nonIs7702Quote = { - ...QUOTE_MOCK, - original: { - ...QUOTE_MOCK.original, - metamask: { - gasLimits: [ - { estimate: 21000, max: 21000 }, - { estimate: 22000, max: 22000 }, - ], - is7702: false, - }, - }, - } as unknown as TransactionPayQuote; - - await submitAcrossQuotes({ - accountSupports7702: true, - messenger, - quotes: [nonIs7702Quote], - transaction: TRANSACTION_META_MOCK, - isSmartTransaction: jest.fn(), - }); - - expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); - expect(addTransactionBatchMock).toHaveBeenCalledWith( - expect.objectContaining({ - disable7702: true, - disableHook: false, - disableSequential: false, - gasLimit7702: undefined, - }), - ); - expect(addTransactionMock).not.toHaveBeenCalled(); - }); - it('submits a single transaction when no approvals', async () => { const noApprovalQuote = { ...QUOTE_MOCK, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 9318482e2f9..3ea0a32fb2b 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -186,12 +186,24 @@ describe('Relay Quotes Utils', () => { findNetworkClientIdByChainIdMock, getDelegationTransactionMock, getGasFeeTokensMock, + getKeyringControllerStateMock, getRemoteFeatureFlagControllerStateMock, } = getMessengerMock(); beforeEach(() => { jest.resetAllMocks(); + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: ['0x1234567890123456789012345678901234567891'], + metadata: { id: 'hd-keyring', name: 'HD Key Tree' }, + }, + ], + }); + getTokenFiatRateMock.mockReturnValue({ usdRate: '2.0', fiatRate: '4.0', @@ -1870,6 +1882,7 @@ describe('Relay Quotes Utils', () => { } as never); const result = await getRelayQuotes({ + accountSupports7702: true, messenger, requests: [QUOTE_REQUEST_MOCK], transaction: TRANSACTION_META_MOCK, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index 356b614799d..c03ada1c3f6 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -25,7 +25,7 @@ import { } from '../../utils/transaction'; import { RELAY_STATUS_URL } from './constants'; import { submitRelayQuotes } from './relay-submit'; -import type { RelayQuote, RelayTransactionStep } from './types'; +import type { RelayQuote } from './types'; jest.mock('../../utils/token'); jest.mock('../../utils/transaction'); @@ -51,18 +51,16 @@ const TRANSACTION_META_MOCK = { hash: TRANSACTION_HASH_MOCK, } as TransactionMeta; -const ORIGINAL_QUOTE_MOCK: RelayQuote & { steps: RelayTransactionStep[] } = { +const ORIGINAL_QUOTE_MOCK = { details: { currencyIn: { currency: { chainId: 1, - decimals: 6, }, }, currencyOut: { currency: { chainId: 2, - decimals: 6, }, }, }, @@ -70,17 +68,7 @@ const ORIGINAL_QUOTE_MOCK: RelayQuote & { steps: RelayTransactionStep[] } = { gasLimits: [21000, 21000], is7702: false, }, - request: { - amount: '1', - destinationChainId: 2, - destinationCurrency: '0xdef' as Hex, - originChainId: 1, - originCurrency: '0xabc' as Hex, - recipient: FROM_MOCK, - slippageTolerance: '100', - tradeType: 'EXPECTED_OUTPUT', - user: FROM_MOCK, - }, + request: {}, steps: [ { id: 'swap', @@ -88,10 +76,6 @@ const ORIGINAL_QUOTE_MOCK: RelayQuote & { steps: RelayTransactionStep[] } = { requestId: REQUEST_ID_MOCK, items: [ { - check: { - endpoint: '/test', - method: 'GET', - }, data: { chainId: 1, data: '0x1234' as Hex, @@ -107,7 +91,7 @@ const ORIGINAL_QUOTE_MOCK: RelayQuote & { steps: RelayTransactionStep[] } = { ], }, ], -}; +} as RelayQuote; const STATUS_RESPONSE_MOCK = { status: 'success', @@ -465,33 +449,6 @@ describe('Relay Submit Utils', () => { ); }); - it('does not add authorization list when quote is7702 is false', async () => { - request.quotes[0].original.metamask.is7702 = false; - request.quotes[0].original.details.currencyOut.currency.chainId = 1; - request.quotes[0].original.request = { - authorizationList: [ - { - address: '0xabc' as Hex, - chainId: 1, - nonce: 2, - r: '0xr' as Hex, - s: '0xs' as Hex, - yParity: 1, - }, - ], - } as never; - - await submitRelayQuotes(request); - - expect(addTransactionMock).toHaveBeenCalledTimes(1); - expect(addTransactionMock).toHaveBeenCalledWith( - expect.not.objectContaining({ - authorizationList: expect.anything(), - }), - expect.anything(), - ); - }); - it('adds transaction batch if multiple params', async () => { request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], @@ -505,8 +462,6 @@ describe('Relay Submit Utils', () => { disableHook: false, disableSequential: false, from: FROM_MOCK, - gasFeeToken: undefined, - gasLimit7702: undefined, networkClientId: NETWORK_CLIENT_ID_MOCK, origin: ORIGIN_METAMASK, overwriteUpgrade: true, @@ -898,17 +853,13 @@ describe('Relay Submit Utils', () => { it('adds transaction batch with original transaction prepended', async () => { await submitRelayQuotes(request); - expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); + expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ - disable7702: true, - disableHook: false, - disableSequential: false, from: FROM_MOCK, gasFeeToken: undefined, networkClientId: NETWORK_CLIENT_ID_MOCK, origin: ORIGIN_METAMASK, - overwriteUpgrade: true, requireApproval: false, transactions: [ { @@ -1017,11 +968,9 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ - disable7702: true, disableHook: false, disableSequential: false, gasLimit7702: undefined, - overwriteUpgrade: true, transactions: [ expect.objectContaining({ params: expect.objectContaining({ @@ -1048,8 +997,10 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ + disable7702: false, + disableHook: true, + disableSequential: true, gasLimit7702: '0x31955', - overwriteUpgrade: true, transactions: [ expect.objectContaining({ params: expect.objectContaining({ @@ -1082,8 +1033,10 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ + disable7702: false, + disableHook: true, + disableSequential: true, gasLimit7702: '0xa410', - overwriteUpgrade: true, transactions: [ expect.objectContaining({ params: expect.objectContaining({ @@ -1100,98 +1053,6 @@ describe('Relay Submit Utils', () => { ); }); - it('uses 7702 mode based on quote metadata regardless of account support', async () => { - request.quotes[0].original.steps[0].items.push({ - ...request.quotes[0].original.steps[0].items[0], - }); - - request.quotes[0].original.metamask.gasLimits = [21000, 22000]; - request.quotes[0].original.metamask.is7702 = true; - - await submitRelayQuotes(request); - - expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); - expect(addTransactionBatchMock).toHaveBeenCalledWith( - expect.objectContaining({ - overwriteUpgrade: true, - gasLimit7702: '0x5208', - }), - ); - expect(addTransactionMock).not.toHaveBeenCalled(); - }); - - it('omits per-transaction gas when entry is missing in batch submission', async () => { - request.quotes[0].original.steps[0].items.push({ - ...request.quotes[0].original.steps[0].items[0], - }); - - request.quotes[0].original.metamask.gasLimits = [21000]; - - await submitRelayQuotes(request); - - expect(addTransactionBatchMock).toHaveBeenCalledWith( - expect.objectContaining({ - transactions: [ - expect.objectContaining({ - params: expect.objectContaining({ - gas: '0x5208', - }), - }), - expect.objectContaining({ - params: expect.objectContaining({ - gas: undefined, - }), - }), - ], - }), - ); - }); - - it('waits for on-chain confirmation with multiple transactions', async () => { - request.quotes[0].original.steps[0].items.push({ - ...request.quotes[0].original.steps[0].items[0], - }); - - request.quotes[0].original.metamask.gasLimits = [21000, 22000]; - - await submitRelayQuotes(request); - - expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); - expect(waitForTransactionConfirmedMock).toHaveBeenCalled(); - }); - - it('uses addTransactionBatch with multiple transactions', async () => { - request.quotes[0].original.steps[0].items.push({ - ...request.quotes[0].original.steps[0].items[0], - }); - - request.quotes[0].original.metamask.gasLimits = [21000, 22000]; - - await submitRelayQuotes(request); - - expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); - expect(addTransactionMock).not.toHaveBeenCalled(); - }); - - it('waits for on-chain confirmation with multiple transactions in 7702 mode', async () => { - request.quotes[0].original.steps[0].items.push({ - ...request.quotes[0].original.steps[0].items[0], - }); - - request.quotes[0].original.metamask.is7702 = true; - - await submitRelayQuotes(request); - - expect(waitForTransactionConfirmedMock).toHaveBeenCalled(); - }); - - it('waits for on-chain confirmation with single transaction', async () => { - await submitRelayQuotes(request); - - expect(addTransactionMock).toHaveBeenCalledTimes(1); - expect(waitForTransactionConfirmedMock).toHaveBeenCalled(); - }); - it('adds transaction batch without gasLimit7702 when multiple gas limits', async () => { request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], @@ -1204,11 +1065,9 @@ describe('Relay Submit Utils', () => { expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ - disable7702: true, disableHook: false, disableSequential: false, gasLimit7702: undefined, - overwriteUpgrade: true, transactions: [ expect.objectContaining({ params: expect.objectContaining({ diff --git a/packages/transaction-pay-controller/src/utils/quote-gas.test.ts b/packages/transaction-pay-controller/src/utils/quote-gas.test.ts index 64cef9bccae..2857c2b9031 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.test.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.test.ts @@ -19,7 +19,11 @@ describe('quote gas estimation', () => { const getGasBufferMock = jest.mocked(getGasBuffer); const estimateGasLimitMock = jest.mocked(estimateGasLimit); - const { estimateGasBatchMock, messenger } = getMessengerMock(); + const { + estimateGasBatchMock, + getKeyringControllerStateMock, + messenger, + } = getMessengerMock(); const TRANSACTIONS_MOCK = [ { @@ -42,6 +46,17 @@ describe('quote gas estimation', () => { beforeEach(() => { jest.resetAllMocks(); + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: ['0x1234567890123456789012345678901234567891'], + metadata: { id: 'hd-keyring', name: 'HD Key Tree' }, + }, + ], + }); + getGasBufferMock.mockReturnValue(1); }); @@ -244,6 +259,54 @@ describe('quote gas estimation', () => { }); }); + it('re-estimates individually when account does not support 7702', async () => { + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'Ledger Hardware', + accounts: ['0x1234567890123456789012345678901234567891'], + metadata: { id: 'ledger-keyring', name: 'Ledger Hardware' }, + }, + ], + }); + + getGasBufferMock.mockReturnValue(1.5); + + estimateGasBatchMock.mockResolvedValue({ + totalGasLimit: 50000, + gasLimits: [50000], + }); + + estimateGasLimitMock.mockResolvedValueOnce({ + estimate: 21000, + max: 21000, + usedFallback: false, + }); + + const result = await estimateQuoteGasLimits({ + messenger, + transactions: TRANSACTIONS_MOCK, + }); + + expect(estimateGasLimitMock).toHaveBeenCalledTimes(1); + expect(estimateGasLimitMock).toHaveBeenCalledWith( + expect.objectContaining({ + fallbackOnSimulationFailure: false, + }), + ); + expect(result).toStrictEqual({ + gasLimits: [ + { estimate: 21000, max: 21000 }, + { estimate: 30000, max: 30000 }, + ], + is7702: false, + totalGasEstimate: 51000, + totalGasLimit: 51000, + usedBatch: true, + }); + }); + it('throws when batch estimation fails', async () => { estimateGasBatchMock.mockRejectedValue( new Error('Batch estimation failed'), diff --git a/packages/transaction-pay-controller/src/utils/quote-gas.ts b/packages/transaction-pay-controller/src/utils/quote-gas.ts index f2c2aff7aa3..48c68f36343 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.ts @@ -84,7 +84,7 @@ async function estimateQuoteGasLimitsBatch( transactions: QuoteGasTransaction[], messenger: TransactionPayControllerMessenger, fallbackGas?: { estimate: number; max: number }, - fallbackOnSimulationFailure?: boolean, + fallbackOnSimulationFailure: boolean, ): Promise<{ batchGasLimit?: QuoteGasLimit; gasLimits: QuoteGasLimit[]; @@ -151,7 +151,7 @@ async function estimateQuoteGasLimitsBatch( transactions.map((transaction) => estimateQuoteGasLimitSingle({ fallbackGas, - fallbackOnSimulationFailure: fallbackOnSimulationFailure ?? false, + fallbackOnSimulationFailure, messenger, transaction, }), diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 6de69992815..230fa7acb49 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -101,7 +101,11 @@ const BATCH_TRANSACTION_MOCK = { } as BatchTransaction; describe('Quotes Utils', () => { - const { messenger, getControllerStateMock } = getMessengerMock(); + const { + messenger, + getControllerStateMock, + getKeyringControllerStateMock, + } = getMessengerMock(); const updateTransactionDataMock = jest.fn(); const getStrategyByNameMock = jest.mocked(getStrategyByName); const getStrategiesByNameMock = jest.mocked(getStrategiesByName); @@ -137,6 +141,17 @@ describe('Quotes Utils', () => { jest.resetAllMocks(); jest.clearAllTimers(); + getKeyringControllerStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: ['0x1234567890123456789012345678901234567891'], + metadata: { id: 'hd-keyring', name: 'HD Key Tree' }, + }, + ], + }); + getStrategiesMock.mockReturnValue([TransactionPayStrategy.Test]); getStrategyByNameMock.mockReturnValue({ @@ -600,31 +615,6 @@ describe('Quotes Utils', () => { }); }); - it('gets quotes with no minimum if allowUnderMinimum is true', async () => { - await run({ - transactionData: { - ...TRANSACTION_DATA_MOCK, - tokens: [ - { - ...TRANSACTION_DATA_MOCK.tokens?.[0], - allowUnderMinimum: true, - } as TransactionPayRequiredToken, - ], - }, - }); - - expect(getQuotesMock).toHaveBeenCalledWith({ - accountSupports7702: true, - messenger, - requests: [ - expect.objectContaining({ - targetAmountMinimum: '0', - }), - ], - transaction: TRANSACTION_META_MOCK, - }); - }); - it('resolves strategies via getStrategiesByName', async () => { await run(); @@ -687,7 +677,8 @@ describe('Quotes Utils', () => { ); }); - it('always passes batch transactions regardless of 7702 support', async () => { + it('sets batchTransactionsOptions to undefined when there are no batch transactions', async () => { + getBatchTransactionsMock.mockResolvedValue([]); await run(); const transactionMetaMock = {} as TransactionMeta; @@ -695,8 +686,8 @@ describe('Quotes Utils', () => { expect(transactionMetaMock).toMatchObject( expect.objectContaining({ - batchTransactions: [BATCH_TRANSACTION_MOCK], - batchTransactionsOptions: {}, + batchTransactions: [], + batchTransactionsOptions: undefined, }), ); }); @@ -1013,27 +1004,25 @@ describe('Quotes Utils', () => { transactionData: POST_QUOTE_TRANSACTION_DATA, }); - expect(getQuotesMock).toHaveBeenCalledWith( - expect.objectContaining({ - accountSupports7702: true, - messenger, - requests: [ - { - from: TRANSACTION_META_MOCK.txParams.from, - isMaxAmount: false, - isPostQuote: true, - sourceBalanceRaw: SOURCE_TOKEN_MOCK.balanceRaw, - sourceChainId: SOURCE_TOKEN_MOCK.chainId, - sourceTokenAddress: SOURCE_TOKEN_MOCK.address, - sourceTokenAmount: '10000000', - targetAmountMinimum: '0', - targetChainId: DESTINATION_TOKEN_MOCK.chainId, - targetTokenAddress: DESTINATION_TOKEN_MOCK.address, - }, - ], - transaction: TRANSACTION_META_MOCK, - }), - ); + expect(getQuotesMock).toHaveBeenCalledWith({ + accountSupports7702: true, + messenger, + requests: [ + { + from: TRANSACTION_META_MOCK.txParams.from, + isMaxAmount: false, + isPostQuote: true, + sourceBalanceRaw: SOURCE_TOKEN_MOCK.balanceRaw, + sourceChainId: SOURCE_TOKEN_MOCK.chainId, + sourceTokenAddress: SOURCE_TOKEN_MOCK.address, + sourceTokenAmount: '10000000', + targetAmountMinimum: '0', + targetChainId: DESTINATION_TOKEN_MOCK.chainId, + targetTokenAddress: DESTINATION_TOKEN_MOCK.address, + }, + ], + transaction: TRANSACTION_META_MOCK, + }); }); it('includes refundTo in post-quote request when set in transaction data', async () => { From 6d74ddeba9406b25fdf7ad4c96a5601b3405cc61 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Thu, 23 Apr 2026 12:08:08 +0200 Subject: [PATCH 30/48] Add missing unit tests --- .../src/utils/batch.test.ts | 13 ++ .../src/utils/eip7702.test.ts | 114 +++++++++++++++++- 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/packages/transaction-controller/src/utils/batch.test.ts b/packages/transaction-controller/src/utils/batch.test.ts index 870d9b46cab..0136a3851fb 100644 --- a/packages/transaction-controller/src/utils/batch.test.ts +++ b/packages/transaction-controller/src/utils/batch.test.ts @@ -32,6 +32,7 @@ import { } from './batch'; import { ERROR_MESSGE_PUBLIC_KEY, + doesAccountSupportEIP7702, doesChainSupportEIP7702, generateEIP7702BatchTransaction, isAccountUpgradedToEIP7702, @@ -275,6 +276,7 @@ function createGasFeeFlowMock(): jest.Mocked { } describe('Batch Utils', () => { + const doesAccountSupportEIP7702Mock = jest.mocked(doesAccountSupportEIP7702); const doesChainSupportEIP7702Mock = jest.mocked(doesChainSupportEIP7702); const getEIP7702SupportedChainsMock = jest.mocked(getEIP7702SupportedChains); const validateBatchRequestMock = jest.mocked(validateBatchRequest); @@ -381,6 +383,7 @@ describe('Batch Utils', () => { gasLimits: [GAS_TOTAL_MOCK], }); + doesAccountSupportEIP7702Mock.mockReturnValue(true); doesChainSupportEIP7702Mock.mockReturnValue(true); signTransactionMock.mockResolvedValue(TRANSACTION_SIGNATURE_3_MOCK); @@ -952,6 +955,16 @@ describe('Batch Utils', () => { ); }); + it('skips 7702 path when account does not support EIP-7702', async () => { + doesAccountSupportEIP7702Mock.mockReturnValue(false); + + await expect(addTransactionBatch(request)).rejects.toThrow( + rpcErrors.internal("Can't process batch"), + ); + + expect(isAccountUpgradedToEIP7702Mock).not.toHaveBeenCalled(); + }); + it('throws if no public key', async () => { await expect( addTransactionBatch({ ...request, publicKeyEIP7702: undefined }), diff --git a/packages/transaction-controller/src/utils/eip7702.test.ts b/packages/transaction-controller/src/utils/eip7702.test.ts index 2dcce540fde..1ff48e38aab 100644 --- a/packages/transaction-controller/src/utils/eip7702.test.ts +++ b/packages/transaction-controller/src/utils/eip7702.test.ts @@ -8,13 +8,17 @@ import type { NetworkClientId } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; import { remove0x } from '@metamask/utils'; -import type { KeyringControllerSignEip7702AuthorizationAction } from '../../../keyring-controller/src'; +import type { + KeyringControllerGetStateAction, + KeyringControllerSignEip7702AuthorizationAction, +} from '../../../keyring-controller/src'; import type { TransactionControllerMessenger } from '../TransactionController'; import { TransactionStatus } from '../types'; import type { AuthorizationList } from '../types'; import type { TransactionMeta } from '../types'; import { DELEGATION_PREFIX, + doesAccountSupportEIP7702, doesChainSupportEIP7702, generateEIP7702BatchTransaction, getDelegationAddress, @@ -95,6 +99,10 @@ describe('EIP-7702 Utils', () => { getEIP7702ContractAddresses, ); + let getKeyringStateMock: jest.MockedFn< + KeyringControllerGetStateAction['handler'] + >; + let signAuthorizationMock: jest.MockedFn< KeyringControllerSignEip7702AuthorizationAction['handler'] >; @@ -104,19 +112,29 @@ describe('EIP-7702 Utils', () => { rootMessenger = new Messenger({ namespace: MOCK_ANY_NAMESPACE }); + getKeyringStateMock = jest.fn().mockReturnValue({ + isUnlocked: true, + keyrings: [], + }); + signAuthorizationMock = jest .fn() .mockResolvedValue(AUTHORIZATION_SIGNATURE_MOCK); const keyringControllerMessenger = new Messenger< 'KeyringController', - KeyringControllerSignEip7702AuthorizationAction, + | KeyringControllerGetStateAction + | KeyringControllerSignEip7702AuthorizationAction, never, typeof rootMessenger >({ namespace: 'KeyringController', parent: rootMessenger, }); + keyringControllerMessenger.registerActionHandler( + 'KeyringController:getState', + getKeyringStateMock, + ); keyringControllerMessenger.registerActionHandler( 'KeyringController:signEip7702Authorization', signAuthorizationMock, @@ -128,7 +146,10 @@ describe('EIP-7702 Utils', () => { }); rootMessenger.delegate({ messenger: controllerMessenger, - actions: ['KeyringController:signEip7702Authorization'], + actions: [ + 'KeyringController:getState', + 'KeyringController:signEip7702Authorization', + ], }); }); @@ -270,6 +291,93 @@ describe('EIP-7702 Utils', () => { }); }); + describe('doesAccountSupportEIP7702', () => { + it('returns true for HD Key Tree keyring', () => { + getKeyringStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: [ADDRESS_MOCK], + metadata: { id: 'hd', name: 'HD Key Tree' }, + }, + ], + }); + + expect( + doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK), + ).toBe(true); + }); + + it('returns true for Simple Key Pair keyring', () => { + getKeyringStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'Simple Key Pair', + accounts: [ADDRESS_MOCK], + metadata: { id: 'simple', name: 'Simple Key Pair' }, + }, + ], + }); + + expect( + doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK), + ).toBe(true); + }); + + it('returns false for unsupported keyring type', () => { + getKeyringStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'Ledger Hardware', + accounts: [ADDRESS_MOCK], + metadata: { id: 'ledger', name: 'Ledger Hardware' }, + }, + ], + }); + + expect( + doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK), + ).toBe(false); + }); + + it('returns true when account is not found in any keyring', () => { + getKeyringStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'Ledger Hardware', + accounts: [ADDRESS_2_MOCK], + metadata: { id: 'ledger', name: 'Ledger Hardware' }, + }, + ], + }); + + expect( + doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK), + ).toBe(true); + }); + + it('matches account addresses case-insensitively', () => { + getKeyringStateMock.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + accounts: [ADDRESS_MOCK.toUpperCase()], + metadata: { id: 'hd', name: 'HD Key Tree' }, + }, + ], + }); + + expect( + doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK), + ).toBe(true); + }); + }); + describe('isAccountUpgradedToEIP7702', () => { it('returns true if delegation matches feature flag', async () => { getEIP7702ContractAddressesMock.mockReturnValue([ADDRESS_2_MOCK]); From 241fe654dfcd70d174a3a6b02bc8f5fd3a654474 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Thu, 23 Apr 2026 12:15:01 +0200 Subject: [PATCH 31/48] Update --- packages/transaction-pay-controller/src/utils/quote-gas.ts | 4 ++-- packages/transaction-pay-controller/src/utils/quotes.test.ts | 4 ++-- packages/transaction-pay-controller/src/utils/quotes.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/transaction-pay-controller/src/utils/quote-gas.ts b/packages/transaction-pay-controller/src/utils/quote-gas.ts index 48c68f36343..3678ba8002e 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.ts @@ -58,8 +58,8 @@ export async function estimateQuoteGasLimits({ const result = await estimateQuoteGasLimitsBatch( transactions, messenger, - fallbackGas, fallbackOnSimulationFailure, + fallbackGas, ); return { @@ -83,8 +83,8 @@ export async function estimateQuoteGasLimits({ async function estimateQuoteGasLimitsBatch( transactions: QuoteGasTransaction[], messenger: TransactionPayControllerMessenger, - fallbackGas?: { estimate: number; max: number }, fallbackOnSimulationFailure: boolean, + fallbackGas?: { estimate: number; max: number }, ): Promise<{ batchGasLimit?: QuoteGasLimit; gasLimits: QuoteGasLimit[]; diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 230fa7acb49..33c1ea8bc50 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -677,7 +677,7 @@ describe('Quotes Utils', () => { ); }); - it('sets batchTransactionsOptions to undefined when there are no batch transactions', async () => { + it('sets batchTransactionsOptions to empty object when there are no batch transactions', async () => { getBatchTransactionsMock.mockResolvedValue([]); await run(); @@ -687,7 +687,7 @@ describe('Quotes Utils', () => { expect(transactionMetaMock).toMatchObject( expect.objectContaining({ batchTransactions: [], - batchTransactionsOptions: undefined, + batchTransactionsOptions: {}, }), ); }); diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index 3c79e6df39a..b639fb5266d 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -174,7 +174,7 @@ function syncTransaction({ totals, transactionId, }: { - batchTransactions: BatchTransaction[] | undefined; + batchTransactions: BatchTransaction[]; isPostQuote?: boolean; messenger: TransactionPayControllerMessenger; paymentToken: TransactionPaymentToken | undefined; @@ -193,7 +193,7 @@ function syncTransaction({ }, (tx: TransactionMeta) => { tx.batchTransactions = batchTransactions; - tx.batchTransactionsOptions = batchTransactions?.length ? {} : undefined; + tx.batchTransactionsOptions = {}; tx.metamaskPay = { bridgeFeeFiat: totals.fees.provider.usd, From 824e01916f93b2190e513a5b0ec5bc8ccc3cb356 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Thu, 23 Apr 2026 13:39:20 +0200 Subject: [PATCH 32/48] Fix batch transaction signing so each transaction is signed sequentially, preventing remaining hardware wallet prompts from appearing after a rejection --- packages/transaction-controller/CHANGELOG.md | 1 + .../src/TransactionController.ts | 1 + .../src/hooks/CollectPublishHook.ts | 24 ++ .../src/utils/batch.test.ts | 327 ++++++++---------- .../transaction-controller/src/utils/batch.ts | 73 ++-- 5 files changed, 225 insertions(+), 201 deletions(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 0b83b4cfec6..4e3751ab8b3 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Gas estimation for EIP-7702 transactions now supports any upgrade-with-data transaction, not only self-targeted ones ([#8467](https://github.com/MetaMask/core/pull/8467)) +- Fix batch transaction signing so each transaction is signed sequentially, preventing remaining hardware wallet prompts from appearing after a rejection ([#8388](https://github.com/MetaMask/core/pull/8388)) ## [64.3.0] diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 64f57f20c8b..ee504e6196b 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -1131,6 +1131,7 @@ export class TransactionController extends BaseController< ); return await addTransactionBatch({ + abortTransactionSigning: this.abortTransactionSigning.bind(this), addTransaction: this.addTransaction.bind(this), estimateGas: this.estimateGas.bind(this), getGasFeeEstimates: this.#getGasFeeEstimates, diff --git a/packages/transaction-controller/src/hooks/CollectPublishHook.ts b/packages/transaction-controller/src/hooks/CollectPublishHook.ts index 4fcd31602a7..48956d1336d 100644 --- a/packages/transaction-controller/src/hooks/CollectPublishHook.ts +++ b/packages/transaction-controller/src/hooks/CollectPublishHook.ts @@ -26,6 +26,8 @@ export class CollectPublishHook { readonly #readyPromise: DeferredPromise; + #signedCountWaiters: { count: number; resolve: () => void }[] = []; + constructor(transactionCount: number) { this.#readyPromise = createDeferredPromise(); this.#results = []; @@ -46,6 +48,20 @@ export class CollectPublishHook { return this.#readyPromise.promise; } + /** + * @param count - The number of signed transactions to wait for. + * @returns A promise that resolves when the given number of transactions have been signed. + */ + waitForSignedCount(count: number): Promise { + if (this.#results.length >= count) { + return Promise.resolve(); + } + + return new Promise((resolve) => { + this.#signedCountWaiters.push({ count, resolve }); + }); + } + /** * Resolve all publish promises with the provided transaction hashes. * @@ -97,6 +113,14 @@ export class CollectPublishHook { this.#results = sortBy(this.#results, (result) => result.nonce); + this.#signedCountWaiters = this.#signedCountWaiters.filter((waiter) => { + if (this.#results.length >= waiter.count) { + waiter.resolve(); + return false; + } + return true; + }); + if (this.#results.length === this.#transactionCount) { log('All transactions signed'); diff --git a/packages/transaction-controller/src/utils/batch.test.ts b/packages/transaction-controller/src/utils/batch.test.ts index 0136a3851fb..eb4dcbea67b 100644 --- a/packages/transaction-controller/src/utils/batch.test.ts +++ b/packages/transaction-controller/src/utils/batch.test.ts @@ -332,15 +332,49 @@ describe('Batch Utils', () => { AddBatchTransactionOptions['signTransaction'] >; + let abortTransactionSigningMock: jest.MockedFn< + AddBatchTransactionOptions['abortTransactionSigning'] + >; + let request: AddBatchTransactionOptions; const estimateGasMock = jest.fn(); + const SIGNATURES_BY_INDEX = [ + TRANSACTION_SIGNATURE_MOCK, + TRANSACTION_SIGNATURE_2_MOCK, + ]; + + function mockAddTransactionWithAutoSign( + ...returnValues: { + transactionMeta: TransactionMeta; + result: Promise; + }[] + ): void { + let callIndex = 0; + for (const returnValue of returnValues) { + const index = callIndex; + addTransactionMock.mockImplementationOnce((_params, options) => { + const signature = SIGNATURES_BY_INDEX[index] ?? SIGNATURES_BY_INDEX[0]; + Promise.resolve().then(() => { + options.publishHook?.(returnValue.transactionMeta, signature).catch( + () => { + // Swallow - handled by batch orchestrator. + }, + ); + }); + return Promise.resolve(returnValue); + }); + callIndex += 1; + } + } + beforeEach(() => { jest.resetAllMocks(); mockMessengerNetworkCalls(); addTransactionMock = jest.fn(); + abortTransactionSigningMock = jest.fn(); getTransactionMock = jest.fn(); updateTransactionMock = jest.fn(); publishTransactionMock = jest.fn(); @@ -389,6 +423,7 @@ describe('Batch Utils', () => { signTransactionMock.mockResolvedValue(TRANSACTION_SIGNATURE_3_MOCK); request = { + abortTransactionSigning: abortTransactionSigningMock, addTransaction: addTransactionMock, estimateGas: estimateGasMock, getGasFeeEstimates: getGasFeeEstimatesMock, @@ -478,21 +513,23 @@ describe('Batch Utils', () => { state: 'approved', }); - addTransactionMock - .mockResolvedValueOnce({ + mockAddTransactionWithAutoSign( + { transactionMeta: { ...TRANSACTION_META_MOCK, id: TRANSACTION_ID_MOCK, }, result: Promise.resolve(''), - }) - .mockResolvedValueOnce({ + }, + { transactionMeta: { ...TRANSACTION_META_MOCK, id: TRANSACTION_ID_2_MOCK, }, result: Promise.resolve(''), - }); + }, + ); + addTransactionBatch({ ...request, publishBatchHook, @@ -1298,21 +1335,22 @@ describe('Batch Utils', () => { it('returns provided batch ID', async () => { const publishBatchHook: jest.MockedFn = jest.fn(); - addTransactionMock - .mockResolvedValueOnce({ + mockAddTransactionWithAutoSign( + { transactionMeta: { ...TRANSACTION_META_MOCK, id: TRANSACTION_ID_MOCK, }, result: Promise.resolve(''), - }) - .mockResolvedValueOnce({ + }, + { transactionMeta: { ...TRANSACTION_META_MOCK, id: TRANSACTION_ID_2_MOCK, }, result: Promise.resolve(''), - }); + }, + ); publishBatchHook.mockResolvedValue({ results: [ @@ -1337,26 +1375,6 @@ describe('Batch Utils', () => { await flushPromises(); - const publishHooks = addTransactionMock.mock.calls.map( - ([, options]) => options.publishHook, - ); - - publishHooks[0]?.( - TRANSACTION_META_MOCK, - TRANSACTION_SIGNATURE_MOCK, - ).catch(() => { - // Intentionally empty - }); - - publishHooks[1]?.( - TRANSACTION_META_MOCK, - TRANSACTION_SIGNATURE_2_MOCK, - ).catch(() => { - // Intentionally empty - }); - - await flushPromises(); - const result = await resultPromise; expect(result.batchId).toBe(BATCH_ID_CUSTOM_MOCK); @@ -1371,10 +1389,19 @@ describe('Batch Utils', () => { it('adds each nested transaction', async () => { const publishBatchHook = jest.fn(); - addTransactionMock.mockResolvedValueOnce({ - transactionMeta: TRANSACTION_META_MOCK, - result: Promise.resolve(''), - }); + mockAddTransactionWithAutoSign( + { + transactionMeta: TRANSACTION_META_MOCK, + result: Promise.resolve(''), + }, + { + transactionMeta: { + ...TRANSACTION_META_MOCK, + id: TRANSACTION_ID_2_MOCK, + }, + result: Promise.resolve(''), + }, + ); addTransactionBatch({ ...request, @@ -1452,21 +1479,22 @@ describe('Batch Utils', () => { it('calls publish batch hook', async () => { const publishBatchHook: jest.MockedFn = jest.fn(); - addTransactionMock - .mockResolvedValueOnce({ + mockAddTransactionWithAutoSign( + { transactionMeta: { ...TRANSACTION_META_MOCK, id: TRANSACTION_ID_MOCK, }, result: Promise.resolve(''), - }) - .mockResolvedValueOnce({ + }, + { transactionMeta: { ...TRANSACTION_META_MOCK, id: TRANSACTION_ID_2_MOCK, }, result: Promise.resolve(''), - }); + }, + ); publishBatchHook.mockResolvedValue({ results: [ @@ -1489,26 +1517,6 @@ describe('Batch Utils', () => { await flushPromises(); - const publishHooks = addTransactionMock.mock.calls.map( - ([, options]) => options.publishHook, - ); - - publishHooks[0]?.( - TRANSACTION_META_MOCK, - TRANSACTION_SIGNATURE_MOCK, - ).catch(() => { - // Intentionally empty - }); - - publishHooks[1]?.( - TRANSACTION_META_MOCK, - TRANSACTION_SIGNATURE_2_MOCK, - ).catch(() => { - // Intentionally empty - }); - - await flushPromises(); - expect(publishBatchHook).toHaveBeenCalledTimes(1); expect(publishBatchHook).toHaveBeenCalledWith( PUBLISH_BATCH_HOOK_PARAMS, @@ -1517,22 +1525,40 @@ describe('Batch Utils', () => { it('resolves individual publish hooks with transaction hashes from publish batch hook', async () => { const publishBatchHook: jest.MockedFn = jest.fn(); + const publishHookPromises: (Promise | undefined)[] = []; - addTransactionMock - .mockResolvedValueOnce({ - transactionMeta: { - ...TRANSACTION_META_MOCK, - id: TRANSACTION_ID_MOCK, - }, - result: Promise.resolve(''), - }) - .mockResolvedValueOnce({ - transactionMeta: { - ...TRANSACTION_META_MOCK, - id: TRANSACTION_ID_2_MOCK, - }, - result: Promise.resolve(''), + const txMeta1 = { + ...TRANSACTION_META_MOCK, + id: TRANSACTION_ID_MOCK, + }; + const txMeta2 = { + ...TRANSACTION_META_MOCK, + id: TRANSACTION_ID_2_MOCK, + }; + + let callIndex = 0; + for (const [meta, sig] of [ + [txMeta1, TRANSACTION_SIGNATURE_MOCK], + [txMeta2, TRANSACTION_SIGNATURE_2_MOCK], + ] as const) { + const idx = callIndex; + addTransactionMock.mockImplementationOnce((_params, options) => { + Promise.resolve().then(() => { + publishHookPromises[idx] = options.publishHook?.( + meta as TransactionMeta, + sig, + ); + publishHookPromises[idx]?.catch(() => { + // Intentionally empty + }); + }); + return Promise.resolve({ + transactionMeta: meta as TransactionMeta, + result: Promise.resolve(''), + }); }); + callIndex += 1; + } publishBatchHook.mockResolvedValue({ results: [ @@ -1555,31 +1581,11 @@ describe('Batch Utils', () => { await flushPromises(); - const publishHooks = addTransactionMock.mock.calls.map( - ([, options]) => options.publishHook, - ); - - const publishHookPromise1 = publishHooks[0]?.( - TRANSACTION_META_MOCK, - TRANSACTION_SIGNATURE_MOCK, - ).catch(() => { - // Intentionally empty - }); - - const publishHookPromise2 = publishHooks[1]?.( - TRANSACTION_META_MOCK, - TRANSACTION_SIGNATURE_2_MOCK, - ).catch(() => { - // Intentionally empty - }); - - await flushPromises(); - - expect(await publishHookPromise1).toStrictEqual({ + expect(await publishHookPromises[0]).toStrictEqual({ transactionHash: TRANSACTION_HASH_MOCK, }); - expect(await publishHookPromise2).toStrictEqual({ + expect(await publishHookPromises[1]).toStrictEqual({ transactionHash: TRANSACTION_HASH_2_MOCK, }); }); @@ -1884,21 +1890,22 @@ describe('Batch Utils', () => { }); it('throws if publish batch hook does not return result', async () => { - addTransactionMock - .mockResolvedValueOnce({ + mockAddTransactionWithAutoSign( + { transactionMeta: { ...TRANSACTION_META_MOCK, id: TRANSACTION_ID_MOCK, }, result: Promise.resolve(''), - }) - .mockResolvedValueOnce({ + }, + { transactionMeta: { ...TRANSACTION_META_MOCK, id: TRANSACTION_ID_2_MOCK, }, result: Promise.resolve(''), - }); + }, + ); const publishBatchHookMock = jest.fn().mockResolvedValue(undefined); sequentialPublishBatchHookMock.mockReturnValue({ @@ -1917,26 +1924,6 @@ describe('Batch Utils', () => { await flushPromises(); - const publishHooks = addTransactionMock.mock.calls.map( - ([, options]) => options.publishHook, - ); - - publishHooks[0]?.( - TRANSACTION_META_MOCK, - TRANSACTION_SIGNATURE_MOCK, - ).catch(() => { - // Intentionally empty - }); - - publishHooks[1]?.( - TRANSACTION_META_MOCK, - TRANSACTION_SIGNATURE_2_MOCK, - ).catch(() => { - // Intentionally empty - }); - - await flushPromises(); - await expect(resultPromise).rejects.toThrow( 'Publish batch hook did not return a result', ); @@ -1944,22 +1931,40 @@ describe('Batch Utils', () => { it('rejects individual publish hooks if batch hook throws', async () => { const publishBatchHook: jest.MockedFn = jest.fn(); + const publishHookPromises: (Promise | undefined)[] = []; - addTransactionMock - .mockResolvedValueOnce({ - transactionMeta: { - ...TRANSACTION_META_MOCK, - id: TRANSACTION_ID_MOCK, - }, - result: Promise.resolve(''), - }) - .mockResolvedValueOnce({ - transactionMeta: { - ...TRANSACTION_META_MOCK, - id: TRANSACTION_ID_2_MOCK, - }, - result: Promise.resolve(''), + const txMeta1 = { + ...TRANSACTION_META_MOCK, + id: TRANSACTION_ID_MOCK, + }; + const txMeta2 = { + ...TRANSACTION_META_MOCK, + id: TRANSACTION_ID_2_MOCK, + }; + + let callIndex = 0; + for (const [meta, sig] of [ + [txMeta1, TRANSACTION_SIGNATURE_MOCK], + [txMeta2, TRANSACTION_SIGNATURE_2_MOCK], + ] as const) { + const idx = callIndex; + addTransactionMock.mockImplementationOnce((_params, options) => { + Promise.resolve().then(() => { + publishHookPromises[idx] = options.publishHook?.( + meta as TransactionMeta, + sig, + ); + publishHookPromises[idx]?.catch(() => { + // Intentionally empty + }); + }); + return Promise.resolve({ + transactionMeta: meta as TransactionMeta, + result: Promise.resolve(''), + }); }); + callIndex += 1; + } publishBatchHook.mockImplementationOnce(() => { throw new Error(ERROR_MESSAGE_MOCK); @@ -1979,32 +1984,12 @@ describe('Batch Utils', () => { await flushPromises(); - const publishHooks = addTransactionMock.mock.calls.map( - ([, options]) => options.publishHook, + await expect(publishHookPromises[0]).rejects.toThrow( + ERROR_MESSAGE_MOCK, ); - - const publishHookPromise1 = publishHooks[0]?.( - TRANSACTION_META_MOCK, - TRANSACTION_SIGNATURE_MOCK, + await expect(publishHookPromises[1]).rejects.toThrow( + ERROR_MESSAGE_MOCK, ); - - publishHookPromise1?.catch(() => { - // Intentionally empty - }); - - const publishHookPromise2 = publishHooks[1]?.( - TRANSACTION_META_MOCK, - TRANSACTION_SIGNATURE_2_MOCK, - ); - - publishHookPromise2?.catch(() => { - // Intentionally empty - }); - - await flushPromises(); - - await expect(publishHookPromise1).rejects.toThrow(ERROR_MESSAGE_MOCK); - await expect(publishHookPromise2).rejects.toThrow(ERROR_MESSAGE_MOCK); }); it('rejects individual publish hooks if add transaction throws', async () => { @@ -2061,21 +2046,22 @@ describe('Batch Utils', () => { sequentialPublishBatchHook = jest.fn(); - addTransactionMock - .mockResolvedValueOnce({ + mockAddTransactionWithAutoSign( + { transactionMeta: { ...TRANSACTION_META_MOCK, id: TRANSACTION_ID_MOCK, }, result: Promise.resolve(''), - }) - .mockResolvedValueOnce({ + }, + { transactionMeta: { ...TRANSACTION_META_MOCK, id: TRANSACTION_ID_2_MOCK, }, result: Promise.resolve(''), - }); + }, + ); }); const setupSequentialPublishBatchHookMock = ( @@ -2087,21 +2073,6 @@ describe('Batch Utils', () => { }; const executePublishHooks = async (): Promise => { - const publishHooks = addTransactionMock.mock.calls.map( - ([, options]) => options.publishHook, - ); - - for (const [index, publishHook] of publishHooks.entries()) { - publishHook?.( - TRANSACTION_META_MOCK, - index === 0 - ? TRANSACTION_SIGNATURE_MOCK - : TRANSACTION_SIGNATURE_2_MOCK, - ).catch(() => { - // Intentionally empty - }); - } - await flushPromises(); }; diff --git a/packages/transaction-controller/src/utils/batch.ts b/packages/transaction-controller/src/utils/batch.ts index 637149d8932..f813693d2d7 100644 --- a/packages/transaction-controller/src/utils/batch.ts +++ b/packages/transaction-controller/src/utils/batch.ts @@ -73,6 +73,7 @@ type UpdateStateCallback = ( ) => void; type AddTransactionBatchRequest = { + abortTransactionSigning: TransactionController['abortTransactionSigning']; addTransaction: TransactionController['addTransaction']; estimateGas: TransactionController['estimateGas']; getGasFeeEstimates: ( @@ -532,6 +533,8 @@ async function addTransactionBatchWithHook( const transactionCount = nestedTransactions.length; const collectHook = new CollectPublishHook(transactionCount); + const hookTransactions: Omit[] = []; + try { if (requireApproval) { txBatchMeta = await prepareApprovalData({ @@ -544,23 +547,33 @@ async function addTransactionBatchWithHook( } const publishHook = collectHook.getHook(); - const hookTransactions: Omit[] = - []; + const signingPromises: Promise[] = []; let index = 0; for (const nestedTransaction of nestedTransactions) { - const hookTransaction = await processTransactionWithHook( - batchId, - nestedTransaction, - publishHook, - request, - txBatchMeta, - index, - ); + const { hookTransaction, signingComplete } = + await processTransactionWithHook( + batchId, + nestedTransaction, + publishHook, + request, + txBatchMeta, + index, + ); hookTransactions.push(hookTransaction); + signingPromises.push(signingComplete); index += 1; + + const signingFailure = new Promise((_resolve, reject) => { + signingComplete.catch(reject); + }); + + await Promise.race([ + collectHook.waitForSignedCount(index), + signingFailure, + ]); } const { signedTransactions } = await collectHook.ready(); @@ -607,6 +620,14 @@ async function addTransactionBatchWithHook( } catch (error) { log('Publish batch hook failed', error); + for (const hookTransaction of hookTransactions) { + try { + request.abortTransactionSigning(String(hookTransaction.id)); + } catch { + // Transaction may not be signing or may already be complete. + } + } + collectHook.error(error); resultCallbacks?.error(error as Error); @@ -635,9 +656,12 @@ async function processTransactionWithHook( request: AddTransactionBatchRequest, txBatchMeta: TransactionBatchMeta | undefined, index: number, -): Promise< - Omit & { type?: TransactionType } -> { +): Promise<{ + hookTransaction: Omit & { + type?: TransactionType; + }; + signingComplete: Promise; +}> { const { assetsFiatValues, existingTransaction, params, type } = nestedTransaction; @@ -683,20 +707,24 @@ async function processTransactionWithHook( transactionMeta = signResult.transactionMeta; } - publishHook(transactionMeta, signedTransaction) - .then(onPublish) - .catch(() => { - // Intentionally empty + const publishResult = publishHook(transactionMeta, signedTransaction) + .then((hookResult) => { + onPublish?.(hookResult); + return ''; }); + publishResult.catch(() => { + // Swallow – error is handled by the batch orchestrator. + }); + log('Processed existing transaction with hook', { id, params, }); return { - id, - params, + hookTransaction: { id, params }, + signingComplete: publishResult, }; } @@ -712,7 +740,7 @@ async function processTransactionWithHook( }); } - const { transactionMeta } = await addTransaction( + const { transactionMeta, result: signingComplete } = await addTransaction( transactionMetaForGasEstimates.txParams, { assetsFiatValues, @@ -750,9 +778,8 @@ async function processTransactionWithHook( }); return { - id, - params: newParams, - type, + hookTransaction: { id, params: newParams, type }, + signingComplete, }; } From c88ab115dd03a8b9a623cf564e930fbfc0fadca3 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Thu, 23 Apr 2026 13:53:57 +0200 Subject: [PATCH 33/48] Fix lint:misc formatting issues --- .../src/utils/batch.test.ts | 11 +++---- .../transaction-controller/src/utils/batch.ts | 7 +++-- .../src/utils/eip7702.test.ts | 30 +++++++++---------- .../src/utils/eip7702.ts | 11 ++----- .../src/strategy/relay/relay-submit.test.ts | 2 +- .../src/utils/quote-gas.test.ts | 7 ++--- .../src/utils/quote-gas.ts | 5 +--- .../src/utils/quotes.test.ts | 7 ++--- 8 files changed, 34 insertions(+), 46 deletions(-) diff --git a/packages/transaction-controller/src/utils/batch.test.ts b/packages/transaction-controller/src/utils/batch.test.ts index eb4dcbea67b..473d94a84cf 100644 --- a/packages/transaction-controller/src/utils/batch.test.ts +++ b/packages/transaction-controller/src/utils/batch.test.ts @@ -355,13 +355,14 @@ describe('Batch Utils', () => { for (const returnValue of returnValues) { const index = callIndex; addTransactionMock.mockImplementationOnce((_params, options) => { - const signature = SIGNATURES_BY_INDEX[index] ?? SIGNATURES_BY_INDEX[0]; + const signature = + SIGNATURES_BY_INDEX[index] ?? SIGNATURES_BY_INDEX[0]; Promise.resolve().then(() => { - options.publishHook?.(returnValue.transactionMeta, signature).catch( - () => { + options + .publishHook?.(returnValue.transactionMeta, signature) + .catch(() => { // Swallow - handled by batch orchestrator. - }, - ); + }); }); return Promise.resolve(returnValue); }); diff --git a/packages/transaction-controller/src/utils/batch.ts b/packages/transaction-controller/src/utils/batch.ts index f813693d2d7..6138776eefa 100644 --- a/packages/transaction-controller/src/utils/batch.ts +++ b/packages/transaction-controller/src/utils/batch.ts @@ -707,11 +707,12 @@ async function processTransactionWithHook( transactionMeta = signResult.transactionMeta; } - const publishResult = publishHook(transactionMeta, signedTransaction) - .then((hookResult) => { + const publishResult = publishHook(transactionMeta, signedTransaction).then( + (hookResult) => { onPublish?.(hookResult); return ''; - }); + }, + ); publishResult.catch(() => { // Swallow – error is handled by the batch orchestrator. diff --git a/packages/transaction-controller/src/utils/eip7702.test.ts b/packages/transaction-controller/src/utils/eip7702.test.ts index 1ff48e38aab..94992c56804 100644 --- a/packages/transaction-controller/src/utils/eip7702.test.ts +++ b/packages/transaction-controller/src/utils/eip7702.test.ts @@ -304,9 +304,9 @@ describe('EIP-7702 Utils', () => { ], }); - expect( - doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK), - ).toBe(true); + expect(doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK)).toBe( + true, + ); }); it('returns true for Simple Key Pair keyring', () => { @@ -321,9 +321,9 @@ describe('EIP-7702 Utils', () => { ], }); - expect( - doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK), - ).toBe(true); + expect(doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK)).toBe( + true, + ); }); it('returns false for unsupported keyring type', () => { @@ -338,9 +338,9 @@ describe('EIP-7702 Utils', () => { ], }); - expect( - doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK), - ).toBe(false); + expect(doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK)).toBe( + false, + ); }); it('returns true when account is not found in any keyring', () => { @@ -355,9 +355,9 @@ describe('EIP-7702 Utils', () => { ], }); - expect( - doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK), - ).toBe(true); + expect(doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK)).toBe( + true, + ); }); it('matches account addresses case-insensitively', () => { @@ -372,9 +372,9 @@ describe('EIP-7702 Utils', () => { ], }); - expect( - doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK), - ).toBe(true); + expect(doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK)).toBe( + true, + ); }); }); diff --git a/packages/transaction-controller/src/utils/eip7702.ts b/packages/transaction-controller/src/utils/eip7702.ts index 9ae339aa7ac..072dffb88ee 100644 --- a/packages/transaction-controller/src/utils/eip7702.ts +++ b/packages/transaction-controller/src/utils/eip7702.ts @@ -59,16 +59,11 @@ export function doesAccountSupportEIP7702( account: string, ): boolean { const { keyrings } = messenger.call('KeyringController:getState'); - const keyring = keyrings.find( - (k: { type: string; accounts: string[] }) => - k.accounts.some( - (a: string) => a.toLowerCase() === account.toLowerCase(), - ), + const keyring = keyrings.find((k: { type: string; accounts: string[] }) => + k.accounts.some((a: string) => a.toLowerCase() === account.toLowerCase()), ); - return keyring - ? KEYRING_TYPES_SUPPORTING_7702.includes(keyring.type) - : true; + return keyring ? KEYRING_TYPES_SUPPORTING_7702.includes(keyring.type) : true; } /** diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index c03ada1c3f6..afc32821fff 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -853,7 +853,7 @@ describe('Relay Submit Utils', () => { it('adds transaction batch with original transaction prepended', async () => { await submitRelayQuotes(request); - expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); + expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); expect(addTransactionBatchMock).toHaveBeenCalledWith( expect.objectContaining({ from: FROM_MOCK, diff --git a/packages/transaction-pay-controller/src/utils/quote-gas.test.ts b/packages/transaction-pay-controller/src/utils/quote-gas.test.ts index 2857c2b9031..b0678edf8a3 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.test.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.test.ts @@ -19,11 +19,8 @@ describe('quote gas estimation', () => { const getGasBufferMock = jest.mocked(getGasBuffer); const estimateGasLimitMock = jest.mocked(estimateGasLimit); - const { - estimateGasBatchMock, - getKeyringControllerStateMock, - messenger, - } = getMessengerMock(); + const { estimateGasBatchMock, getKeyringControllerStateMock, messenger } = + getMessengerMock(); const TRANSACTIONS_MOCK = [ { diff --git a/packages/transaction-pay-controller/src/utils/quote-gas.ts b/packages/transaction-pay-controller/src/utils/quote-gas.ts index 3678ba8002e..6c122a89b63 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.ts @@ -141,10 +141,7 @@ async function estimateQuoteGasLimitsBatch( // If the batch returned a combined 7702 gas limit but the account cannot // sign EIP-7702 authorizations (e.g. hardware wallet), re-estimate each // transaction individually so callers never see is7702=true. - const supports7702 = accountSupports7702( - messenger, - firstTransaction.from, - ); + const supports7702 = accountSupports7702(messenger, firstTransaction.from); if (is7702 && !supports7702) { const individualResults = await Promise.all( diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 33c1ea8bc50..ae0db92e1dc 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -101,11 +101,8 @@ const BATCH_TRANSACTION_MOCK = { } as BatchTransaction; describe('Quotes Utils', () => { - const { - messenger, - getControllerStateMock, - getKeyringControllerStateMock, - } = getMessengerMock(); + const { messenger, getControllerStateMock, getKeyringControllerStateMock } = + getMessengerMock(); const updateTransactionDataMock = jest.fn(); const getStrategyByNameMock = jest.mocked(getStrategyByName); const getStrategiesByNameMock = jest.mocked(getStrategiesByName); From ae992c706b2ddc4ef14b1134fbf2d61e9b3b5184 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Thu, 23 Apr 2026 13:58:56 +0200 Subject: [PATCH 34/48] Fix transaction-pay-controller changelog: move #8388 entry to Unreleased --- packages/transaction-pay-controller/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 66c616b1d04..a77136a6629 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -25,6 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/bridge-controller` from `^70.1.1` to `^70.2.0` ([#8571](https://github.com/MetaMask/core/pull/8571)) - Bump `@metamask/bridge-status-controller` from `^70.0.5` to `^71.0.0` ([#8571](https://github.com/MetaMask/core/pull/8571)) +### Fixed + +- **BREAKING:** Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) + - `AccountSupports7702Callback` type export has been removed. Use the `accountSupports7702` util from `utils/7702` instead. + - The `TransactionPayControllerMessenger` now requires `KeyringController:getState` permission (previously only needed in the publish hook). + ## [19.2.2] ### Changed From 16522b46be1b6e2ac860ccb14017517082445d93 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Thu, 23 Apr 2026 14:01:08 +0200 Subject: [PATCH 35/48] Update --- packages/transaction-controller/src/utils/batch.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/transaction-controller/src/utils/batch.ts b/packages/transaction-controller/src/utils/batch.ts index 6138776eefa..269e9af84b4 100644 --- a/packages/transaction-controller/src/utils/batch.ts +++ b/packages/transaction-controller/src/utils/batch.ts @@ -547,7 +547,6 @@ async function addTransactionBatchWithHook( } const publishHook = collectHook.getHook(); - const signingPromises: Promise[] = []; let index = 0; @@ -563,7 +562,6 @@ async function addTransactionBatchWithHook( ); hookTransactions.push(hookTransaction); - signingPromises.push(signingComplete); index += 1; const signingFailure = new Promise((_resolve, reject) => { From faee6cf28fa53adb90fa2606779d74f5cd52b857 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Thu, 23 Apr 2026 14:09:51 +0200 Subject: [PATCH 36/48] Fix comments --- .../src/utils/batch.test.ts | 76 +++++++++---------- .../transaction-controller/src/utils/batch.ts | 4 + .../src/utils/quotes.ts | 6 +- 3 files changed, 44 insertions(+), 42 deletions(-) diff --git a/packages/transaction-controller/src/utils/batch.test.ts b/packages/transaction-controller/src/utils/batch.test.ts index 473d94a84cf..3e57e4751b7 100644 --- a/packages/transaction-controller/src/utils/batch.test.ts +++ b/packages/transaction-controller/src/utils/batch.test.ts @@ -357,13 +357,11 @@ describe('Batch Utils', () => { addTransactionMock.mockImplementationOnce((_params, options) => { const signature = SIGNATURES_BY_INDEX[index] ?? SIGNATURES_BY_INDEX[0]; - Promise.resolve().then(() => { - options - .publishHook?.(returnValue.transactionMeta, signature) - .catch(() => { - // Swallow - handled by batch orchestrator. - }); - }); + void options + .publishHook?.(returnValue.transactionMeta, signature) + .catch(() => { + // Intentionally empty + }); return Promise.resolve(returnValue); }); callIndex += 1; @@ -1537,28 +1535,28 @@ describe('Batch Utils', () => { id: TRANSACTION_ID_2_MOCK, }; - let callIndex = 0; - for (const [meta, sig] of [ - [txMeta1, TRANSACTION_SIGNATURE_MOCK], - [txMeta2, TRANSACTION_SIGNATURE_2_MOCK], - ] as const) { - const idx = callIndex; + const metas = [txMeta1, txMeta2] as const; + const signatures = [ + TRANSACTION_SIGNATURE_MOCK, + TRANSACTION_SIGNATURE_2_MOCK, + ] as const; + + for (let idx = 0; idx < metas.length; idx += 1) { + const capturedIdx = idx; addTransactionMock.mockImplementationOnce((_params, options) => { - Promise.resolve().then(() => { - publishHookPromises[idx] = options.publishHook?.( - meta as TransactionMeta, - sig, - ); - publishHookPromises[idx]?.catch(() => { - // Intentionally empty - }); + const hookPromise = options.publishHook?.( + metas[capturedIdx] as TransactionMeta, + signatures[capturedIdx], + ); + publishHookPromises[capturedIdx] = hookPromise; + hookPromise?.catch(() => { + // Intentionally empty }); return Promise.resolve({ - transactionMeta: meta as TransactionMeta, + transactionMeta: metas[capturedIdx] as TransactionMeta, result: Promise.resolve(''), }); }); - callIndex += 1; } publishBatchHook.mockResolvedValue({ @@ -1943,28 +1941,28 @@ describe('Batch Utils', () => { id: TRANSACTION_ID_2_MOCK, }; - let callIndex = 0; - for (const [meta, sig] of [ - [txMeta1, TRANSACTION_SIGNATURE_MOCK], - [txMeta2, TRANSACTION_SIGNATURE_2_MOCK], - ] as const) { - const idx = callIndex; + const metas = [txMeta1, txMeta2] as const; + const signatures = [ + TRANSACTION_SIGNATURE_MOCK, + TRANSACTION_SIGNATURE_2_MOCK, + ] as const; + + for (let idx = 0; idx < metas.length; idx += 1) { + const capturedIdx = idx; addTransactionMock.mockImplementationOnce((_params, options) => { - Promise.resolve().then(() => { - publishHookPromises[idx] = options.publishHook?.( - meta as TransactionMeta, - sig, - ); - publishHookPromises[idx]?.catch(() => { - // Intentionally empty - }); + const hookPromise = options.publishHook?.( + metas[capturedIdx] as TransactionMeta, + signatures[capturedIdx], + ); + publishHookPromises[capturedIdx] = hookPromise; + hookPromise?.catch(() => { + // Intentionally empty }); return Promise.resolve({ - transactionMeta: meta as TransactionMeta, + transactionMeta: metas[capturedIdx] as TransactionMeta, result: Promise.resolve(''), }); }); - callIndex += 1; } publishBatchHook.mockImplementationOnce(() => { diff --git a/packages/transaction-controller/src/utils/batch.ts b/packages/transaction-controller/src/utils/batch.ts index 269e9af84b4..867bcaa6855 100644 --- a/packages/transaction-controller/src/utils/batch.ts +++ b/packages/transaction-controller/src/utils/batch.ts @@ -572,6 +572,10 @@ async function addTransactionBatchWithHook( collectHook.waitForSignedCount(index), signingFailure, ]); + + signingFailure.catch(() => { + // Swallow – error is handled by the batch orchestrator. + }); } const { signedTransactions } = await collectHook.ready(); diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index b639fb5266d..b45853949bb 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -479,7 +479,7 @@ async function refreshPaymentTokenBalance({ * * @param transaction - Transaction metadata. * @param requests - Quote requests. - * @param accountSupports7702 - Whether the account supports EIP-7702. + * @param isAccountEIP7702Compatible - Whether the account supports EIP-7702. * @param getStrategies - Callback to get ordered strategy names for a transaction. * @param messenger - Controller messenger. * @param fiatPaymentMethod - Selected fiat payment method ID, if applicable. @@ -488,7 +488,7 @@ async function refreshPaymentTokenBalance({ async function getQuotes( transaction: TransactionMeta, requests: QuoteRequest[], - accountSupports7702: boolean, + isAccountEIP7702Compatible: boolean, getStrategies: (transaction: TransactionMeta) => TransactionPayStrategy[], messenger: TransactionPayControllerMessenger, fiatPaymentMethod?: string, @@ -515,7 +515,7 @@ async function getQuotes( } const request = { - accountSupports7702, + accountSupports7702: isAccountEIP7702Compatible, fiatPaymentMethod, messenger, requests, From 515c19335914724f693412817810322a361308b9 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Thu, 23 Apr 2026 14:15:42 +0200 Subject: [PATCH 37/48] Fix final lint --- packages/transaction-controller/src/utils/batch.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/utils/batch.test.ts b/packages/transaction-controller/src/utils/batch.test.ts index 3e57e4751b7..09666888a4c 100644 --- a/packages/transaction-controller/src/utils/batch.test.ts +++ b/packages/transaction-controller/src/utils/batch.test.ts @@ -357,7 +357,7 @@ describe('Batch Utils', () => { addTransactionMock.mockImplementationOnce((_params, options) => { const signature = SIGNATURES_BY_INDEX[index] ?? SIGNATURES_BY_INDEX[0]; - void options + options .publishHook?.(returnValue.transactionMeta, signature) .catch(() => { // Intentionally empty From bbaa272f2b0701ef1b09350024151fdd15656f56 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Mon, 27 Apr 2026 11:31:43 +0200 Subject: [PATCH 38/48] Update --- packages/keyring-controller/src/KeyringController.ts | 1 - packages/transaction-pay-controller/CHANGELOG.md | 11 ++--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 3fde1f3ef60..f989e277586 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -77,7 +77,6 @@ const MESSENGER_EXPOSED_METHODS = [ 'createNewVaultAndRestore', 'removeAccount', 'isUnlocked', - 'accountSupports7702', ] as const; /** diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index a77136a6629..c1ba84013d3 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fall back from Across to later pay strategies when Across quotes would require a first-time EIP-7702 authorization list ([#8577](https://github.com/MetaMask/core/pull/8577)) - Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) +- **BREAKING:** Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) + - The `TransactionPayControllerMessenger` now requires `KeyringController:getState` permission. ## [19.3.0] @@ -25,12 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/bridge-controller` from `^70.1.1` to `^70.2.0` ([#8571](https://github.com/MetaMask/core/pull/8571)) - Bump `@metamask/bridge-status-controller` from `^70.0.5` to `^71.0.0` ([#8571](https://github.com/MetaMask/core/pull/8571)) -### Fixed - -- **BREAKING:** Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) - - `AccountSupports7702Callback` type export has been removed. Use the `accountSupports7702` util from `utils/7702` instead. - - The `TransactionPayControllerMessenger` now requires `KeyringController:getState` permission (previously only needed in the publish hook). - ## [19.2.2] ### Changed @@ -41,9 +37,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ignore synthetic gas legs when determining Across support for perps direct deposits ([#8527](https://github.com/MetaMask/core/pull/8527)) - Route Across status polling through the configured Across API base and support `depositTxnRef`/`fillTxnRef` for Across status responses ([#8512](https://github.com/MetaMask/core/pull/8512)) -- **BREAKING:** Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) - - `AccountSupports7702Callback` type export has been removed. Use the `accountSupports7702` util from `utils/7702` instead. - - The `TransactionPayControllerMessenger` now requires `KeyringController:getState` permission (previously only needed in the publish hook). ## [19.2.1] From c56abbee1f3777f286ba5584eb4c99d1c0cfe2d5 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Mon, 27 Apr 2026 11:32:28 +0200 Subject: [PATCH 39/48] Update changelog --- packages/transaction-pay-controller/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index c1ba84013d3..ff363802e6a 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -10,7 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fall back from Across to later pay strategies when Across quotes would require a first-time EIP-7702 authorization list ([#8577](https://github.com/MetaMask/core/pull/8577)) -- Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) - **BREAKING:** Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) - The `TransactionPayControllerMessenger` now requires `KeyringController:getState` permission. From d6a4345bd9181c324acf44409b41033ee0e4d9c7 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Mon, 27 Apr 2026 11:39:19 +0200 Subject: [PATCH 40/48] Update --- .../src/strategy/across/across-quotes.test.ts | 33 +++++++++++++++++++ .../src/strategy/across/across-quotes.ts | 9 ++--- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts index f03bbc73726..eae892a5859 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts @@ -1303,6 +1303,39 @@ describe('Across Quotes', () => { ); }); + it('omits requiresAuthorizationList when batch estimation does not include it', async () => { + estimateGasBatchMock.mockResolvedValue({ + totalGasLimit: 51000, + gasLimits: [51000], + }); + + successfulFetchMock.mockResolvedValue({ + json: async () => ({ + ...QUOTE_MOCK, + approvalTxns: [ + { + chainId: 1, + data: '0xaaaa' as Hex, + to: '0xapprove1' as Hex, + value: '0x1' as Hex, + }, + ], + }), + } as Response); + + const result = await getAcrossQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(result[0].original.metamask.is7702).toBe(true); + expect( + result[0].original.metamask.requiresAuthorizationList, + ).toBeUndefined(); + }); + it('re-estimates individually when batch returns 7702 but account does not support it', async () => { getKeyringControllerStateMock.mockReturnValue({ isUnlocked: true, diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts index deac72d753d..4f4f7778658 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -200,11 +200,8 @@ async function normalizeQuote( const dustUsd = calculateDustUsd(quote, request, targetFiatRate); const dust = getFiatValueFromUsd(dustUsd, usdToFiatRate); - const { gasLimits, is7702, sourceNetwork } = await calculateSourceNetworkCost( - quote, - messenger, - request, - ); + const { gasLimits, is7702, requiresAuthorizationList, sourceNetwork } = + await calculateSourceNetworkCost(quote, messenger, request); const targetNetwork = getFiatValueFromUsd(new BigNumber(0), usdToFiatRate); @@ -411,7 +408,7 @@ async function calculateSourceNetworkCost( value: transaction.value ?? '0x0', })), }); - const { batchGasLimit, is7702 } = gasEstimates; + const { batchGasLimit, is7702, requiresAuthorizationList } = gasEstimates; if (is7702) { if (!batchGasLimit) { From ab2f7c2bcde151324bfecb231ba750bf612a358b Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Mon, 27 Apr 2026 15:19:37 +0200 Subject: [PATCH 41/48] Address PR review: use .some() for 7702 account check, move account support check into estimateGasBatch --- .../src/utils/eip7702.test.ts | 4 +- .../src/utils/eip7702.ts | 11 +-- .../src/utils/gas.test.ts | 72 +++++++++++++++++++ .../transaction-controller/src/utils/gas.ts | 13 +++- .../helpers/TransactionPayPublishHook.test.ts | 4 +- .../src/strategy/across/across-quotes.test.ts | 12 +--- .../src/utils/7702.ts | 13 ++-- .../src/utils/quote-gas.test.ts | 48 ------------- .../src/utils/quote-gas.ts | 44 +----------- .../src/utils/quotes.test.ts | 2 +- 10 files changed, 104 insertions(+), 119 deletions(-) diff --git a/packages/transaction-controller/src/utils/eip7702.test.ts b/packages/transaction-controller/src/utils/eip7702.test.ts index 94992c56804..b23f9ac2179 100644 --- a/packages/transaction-controller/src/utils/eip7702.test.ts +++ b/packages/transaction-controller/src/utils/eip7702.test.ts @@ -343,7 +343,7 @@ describe('EIP-7702 Utils', () => { ); }); - it('returns true when account is not found in any keyring', () => { + it('returns false when account is not found in any keyring', () => { getKeyringStateMock.mockReturnValue({ isUnlocked: true, keyrings: [ @@ -356,7 +356,7 @@ describe('EIP-7702 Utils', () => { }); expect(doesAccountSupportEIP7702(controllerMessenger, ADDRESS_MOCK)).toBe( - true, + false, ); }); diff --git a/packages/transaction-controller/src/utils/eip7702.ts b/packages/transaction-controller/src/utils/eip7702.ts index 072dffb88ee..36e5eb1ce1d 100644 --- a/packages/transaction-controller/src/utils/eip7702.ts +++ b/packages/transaction-controller/src/utils/eip7702.ts @@ -48,7 +48,7 @@ const KEYRING_TYPES_SUPPORTING_7702 = ['HD Key Tree', 'Simple Key Pair']; * * Looks up the account's keyring via `KeyringController:getState` and returns * `true` only when the keyring type is in the supported list. - * Falls back to `true` when the keyring cannot be resolved. + * Returns `false` when the keyring cannot be resolved. * * @param messenger - Controller messenger. * @param account - The account address to check. @@ -59,11 +59,12 @@ export function doesAccountSupportEIP7702( account: string, ): boolean { const { keyrings } = messenger.call('KeyringController:getState'); - const keyring = keyrings.find((k: { type: string; accounts: string[] }) => - k.accounts.some((a: string) => a.toLowerCase() === account.toLowerCase()), - ); - return keyring ? KEYRING_TYPES_SUPPORTING_7702.includes(keyring.type) : true; + return keyrings.some( + (k: { type: string; accounts: string[] }) => + KEYRING_TYPES_SUPPORTING_7702.includes(k.type) && + k.accounts.some((a: string) => a.toLowerCase() === account.toLowerCase()), + ); } /** diff --git a/packages/transaction-controller/src/utils/gas.test.ts b/packages/transaction-controller/src/utils/gas.test.ts index 184441bd039..b453dd0976c 100644 --- a/packages/transaction-controller/src/utils/gas.test.ts +++ b/packages/transaction-controller/src/utils/gas.test.ts @@ -77,6 +77,17 @@ const MESSENGER_MOCK = { }; } + if (action === 'KeyringController:getState') { + return { + keyrings: [ + { + type: 'HD Key Tree', + accounts: [FROM_MOCK], + }, + ], + }; + } + return { remoteFeatureFlags: {}, }; @@ -94,6 +105,17 @@ function mockMessengerCall(): void { }; } + if (action === 'KeyringController:getState') { + return { + keyrings: [ + { + type: 'HD Key Tree', + accounts: [FROM_MOCK], + }, + ], + }; + } + return { remoteFeatureFlags: {}, }; @@ -1335,6 +1357,56 @@ describe('gas', () => { expect(result.gasLimits[1]).toBe(500000); expect(result.totalGasLimit).toBe(600000); }); + + it('skips EIP-7702 path when account does not support it', async () => { + const isAtomicBatchSupportedMock = jest.fn().mockResolvedValue([ + { + chainId: CHAIN_ID_MOCK, + isSupported: true, + upgradeContractAddress: UPGRADE_CONTRACT_ADDRESS_MOCK, + }, + ]); + + jest.mocked(MESSENGER_MOCK.call).mockImplementation((action: string) => { + if (action === 'NetworkController:getNetworkClientById') { + return { + configuration: { chainId: CHAIN_ID_MOCK }, + provider: {}, + }; + } + + if (action === 'KeyringController:getState') { + return { + keyrings: [ + { + type: 'Ledger Hardware', + accounts: [FROM_MOCK], + }, + ], + }; + } + + return { + remoteFeatureFlags: {}, + }; + }); + + simulateTransactionsMock.mockResolvedValue( + SIMULATED_TRANSACTIONS_RESPONSE_MOCK, + ); + + const result = await estimateGasBatch({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + from: FROM_MOCK, + getSimulationConfig: GET_SIMULATION_CONFIG_MOCK, + isAtomicBatchSupported: isAtomicBatchSupportedMock, + messenger: MESSENGER_MOCK, + transactions: BATCH_TX_PARAMS_MOCK, + }); + + expect(generateEIP7702BatchTransactionMock).not.toHaveBeenCalled(); + expect(result.gasLimits).toHaveLength(2); + }); }); describe('simulateGasBatch', () => { diff --git a/packages/transaction-controller/src/utils/gas.ts b/packages/transaction-controller/src/utils/gas.ts index dd4a952a5ff..1650bcb26f7 100644 --- a/packages/transaction-controller/src/utils/gas.ts +++ b/packages/transaction-controller/src/utils/gas.ts @@ -23,7 +23,11 @@ import type { TransactionMeta, TransactionParams, } from '../types'; -import { DELEGATION_PREFIX, generateEIP7702BatchTransaction } from './eip7702'; +import { + DELEGATION_PREFIX, + doesAccountSupportEIP7702, + generateEIP7702BatchTransaction, +} from './eip7702'; import { getGasEstimateBuffer, getGasEstimateFallback } from './feature-flags'; import { getChainId, rpcRequest } from './provider'; @@ -216,7 +220,12 @@ export async function estimateGasBatch({ chainIds: [chainId], }); - const chainResult = is7702Result.find((result) => result.chainId === chainId); + const accountSupports7702 = doesAccountSupportEIP7702(messenger, from); + + const chainResult = accountSupports7702 + ? is7702Result.find((result) => result.chainId === chainId) + : undefined; + const isUpgradeRequired = Boolean(chainResult && !chainResult.isSupported); if (isUpgradeRequired && !chainResult?.upgradeContractAddress) { diff --git a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts index 0f940caac53..cc114686191 100644 --- a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts +++ b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts @@ -154,7 +154,7 @@ describe('TransactionPayPublishHook', () => { expect(updateTransactionMock).not.toHaveBeenCalled(); }); - it('defaults to accountSupports7702 true when keyring not found', async () => { + it('defaults to accountSupports7702 false when keyring not found', async () => { getKeyringControllerStateMock.mockReturnValue({ isUnlocked: true, keyrings: [], @@ -164,7 +164,7 @@ describe('TransactionPayPublishHook', () => { expect(executeMock).toHaveBeenCalledWith( expect.objectContaining({ - accountSupports7702: true, + accountSupports7702: false, }), ); }); diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts index eae892a5859..bb3353061aa 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts @@ -1336,7 +1336,7 @@ describe('Across Quotes', () => { ).toBeUndefined(); }); - it('re-estimates individually when batch returns 7702 but account does not support it', async () => { + it('returns per-transaction gas limits when account does not support 7702', async () => { getKeyringControllerStateMock.mockReturnValue({ isUnlocked: true, keyrings: [ @@ -1349,13 +1349,8 @@ describe('Across Quotes', () => { }); estimateGasBatchMock.mockResolvedValue({ - totalGasLimit: 51000, - gasLimits: [51000], - }); - - estimateGasMock.mockResolvedValue({ - gas: '0x5208', - simulationFails: undefined, + totalGasLimit: 42000, + gasLimits: [21000, 21000], }); successfulFetchMock.mockResolvedValue({ @@ -1381,7 +1376,6 @@ describe('Across Quotes', () => { expect(result[0].original.metamask.is7702).toBe(false); expect(result[0].original.metamask.gasLimits).toHaveLength(2); - expect(estimateGasMock).toHaveBeenCalledTimes(2); }); it('throws when the shared gas estimator marks a quote as 7702 without a combined gas limit', async () => { diff --git a/packages/transaction-pay-controller/src/utils/7702.ts b/packages/transaction-pay-controller/src/utils/7702.ts index 277bda810bf..00354371d23 100644 --- a/packages/transaction-pay-controller/src/utils/7702.ts +++ b/packages/transaction-pay-controller/src/utils/7702.ts @@ -7,7 +7,7 @@ import { KEYRING_TYPES_SUPPORTING_7702 } from '../types'; * Looks up the account's keyring via `KeyringController:getState` and returns * `true` only when the keyring type is in the supported list (HD Key Tree, * Simple Key Pair). Hardware wallets, snap keyrings, and other types return - * `false`. Falls back to `true` when the keyring cannot be resolved. + * `false`. Returns `false` when the keyring cannot be resolved. * * @param messenger - Controller messenger used to call KeyringController. * @param account - The account address to check. @@ -18,11 +18,10 @@ export function accountSupports7702( account: string, ): boolean { const { keyrings } = messenger.call('KeyringController:getState'); - const keyring = keyrings.find((k) => - k.accounts.some((a) => a.toLowerCase() === account.toLowerCase()), - ); - return keyring - ? (KEYRING_TYPES_SUPPORTING_7702 as string[]).includes(keyring.type) - : true; + return keyrings.some( + (k) => + (KEYRING_TYPES_SUPPORTING_7702 as string[]).includes(k.type) && + k.accounts.some((a) => a.toLowerCase() === account.toLowerCase()), + ); } diff --git a/packages/transaction-pay-controller/src/utils/quote-gas.test.ts b/packages/transaction-pay-controller/src/utils/quote-gas.test.ts index b0678edf8a3..ad684d5685e 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.test.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.test.ts @@ -256,54 +256,6 @@ describe('quote gas estimation', () => { }); }); - it('re-estimates individually when account does not support 7702', async () => { - getKeyringControllerStateMock.mockReturnValue({ - isUnlocked: true, - keyrings: [ - { - type: 'Ledger Hardware', - accounts: ['0x1234567890123456789012345678901234567891'], - metadata: { id: 'ledger-keyring', name: 'Ledger Hardware' }, - }, - ], - }); - - getGasBufferMock.mockReturnValue(1.5); - - estimateGasBatchMock.mockResolvedValue({ - totalGasLimit: 50000, - gasLimits: [50000], - }); - - estimateGasLimitMock.mockResolvedValueOnce({ - estimate: 21000, - max: 21000, - usedFallback: false, - }); - - const result = await estimateQuoteGasLimits({ - messenger, - transactions: TRANSACTIONS_MOCK, - }); - - expect(estimateGasLimitMock).toHaveBeenCalledTimes(1); - expect(estimateGasLimitMock).toHaveBeenCalledWith( - expect.objectContaining({ - fallbackOnSimulationFailure: false, - }), - ); - expect(result).toStrictEqual({ - gasLimits: [ - { estimate: 21000, max: 21000 }, - { estimate: 30000, max: 30000 }, - ], - is7702: false, - totalGasEstimate: 51000, - totalGasLimit: 51000, - usedBatch: true, - }); - }); - it('throws when batch estimation fails', async () => { estimateGasBatchMock.mockRejectedValue( new Error('Batch estimation failed'), diff --git a/packages/transaction-pay-controller/src/utils/quote-gas.ts b/packages/transaction-pay-controller/src/utils/quote-gas.ts index 6c122a89b63..25c2823c1d3 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.ts @@ -6,7 +6,6 @@ import { BigNumber } from 'bignumber.js'; import type { TransactionPayControllerMessenger } from '..'; import { projectLogger } from '../logger'; -import { accountSupports7702 } from './7702'; import { getGasBuffer } from './feature-flags'; import { estimateGasLimit } from './gas'; @@ -55,15 +54,8 @@ export async function estimateQuoteGasLimits({ const useBatch = transactions.length > 1; if (useBatch) { - const result = await estimateQuoteGasLimitsBatch( - transactions, - messenger, - fallbackOnSimulationFailure, - fallbackGas, - ); - return { - ...result, + ...(await estimateQuoteGasLimitsBatch(transactions, messenger)), usedBatch: true, }; } @@ -83,8 +75,6 @@ export async function estimateQuoteGasLimits({ async function estimateQuoteGasLimitsBatch( transactions: QuoteGasTransaction[], messenger: TransactionPayControllerMessenger, - fallbackOnSimulationFailure: boolean, - fallbackGas?: { estimate: number; max: number }, ): Promise<{ batchGasLimit?: QuoteGasLimit; gasLimits: QuoteGasLimit[]; @@ -137,38 +127,6 @@ async function estimateQuoteGasLimitsBatch( 0, ); const is7702 = bufferedGasLimits.length === 1; - - // If the batch returned a combined 7702 gas limit but the account cannot - // sign EIP-7702 authorizations (e.g. hardware wallet), re-estimate each - // transaction individually so callers never see is7702=true. - const supports7702 = accountSupports7702(messenger, firstTransaction.from); - - if (is7702 && !supports7702) { - const individualResults = await Promise.all( - transactions.map((transaction) => - estimateQuoteGasLimitSingle({ - fallbackGas, - fallbackOnSimulationFailure, - messenger, - transaction, - }), - ), - ); - - return { - gasLimits: individualResults.map((res) => res.gasLimits[0]), - is7702: false, - totalGasEstimate: individualResults.reduce( - (acc, res) => acc + res.totalGasEstimate, - 0, - ), - totalGasLimit: individualResults.reduce( - (acc, res) => acc + res.totalGasLimit, - 0, - ), - }; - } - const batchGasLimit = is7702 ? bufferedGasLimits[0] : undefined; return { diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index ae0db92e1dc..67aff2e6a8b 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -143,7 +143,7 @@ describe('Quotes Utils', () => { keyrings: [ { type: 'HD Key Tree', - accounts: ['0x1234567890123456789012345678901234567891'], + accounts: ['0xabc'], metadata: { id: 'hd-keyring', name: 'HD Key Tree' }, }, ], From 3f1abbe9c5dc60eb16cc6e689c006aefe6459ec2 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Mon, 27 Apr 2026 15:27:28 +0200 Subject: [PATCH 42/48] Add changelog entry for estimateGasBatch account support check --- packages/transaction-controller/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index a0a8b674ff2..c41a2fc087e 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- `estimateGasBatch` now skips the EIP-7702 path when the account's keyring does not support it, falling back to per-transaction gas estimation ([#8388](https://github.com/MetaMask/core/pull/8388)) +- `doesAccountSupportEIP7702` now returns `false` instead of `true` when the account is not found in any keyring ([#8388](https://github.com/MetaMask/core/pull/8388)) + ## [64.4.0] ### Changed From a05de38566ce9a89ba6039d376895f39b6b9c77b Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 28 Apr 2026 10:31:44 +0200 Subject: [PATCH 43/48] Update --- .../src/TransactionController.ts | 1 - .../src/hooks/CollectPublishHook.ts | 24 ---- .../src/utils/batch.test.ts | 66 +++++++-- .../transaction-controller/src/utils/batch.ts | 131 ++++++++++++------ 4 files changed, 149 insertions(+), 73 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 2ae1426b1f4..646842e68f2 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -1132,7 +1132,6 @@ export class TransactionController extends BaseController< ); return await addTransactionBatch({ - abortTransactionSigning: this.abortTransactionSigning.bind(this), addTransaction: this.addTransaction.bind(this), estimateGas: this.estimateGas.bind(this), getGasFeeEstimates: this.#getGasFeeEstimates, diff --git a/packages/transaction-controller/src/hooks/CollectPublishHook.ts b/packages/transaction-controller/src/hooks/CollectPublishHook.ts index 48956d1336d..4fcd31602a7 100644 --- a/packages/transaction-controller/src/hooks/CollectPublishHook.ts +++ b/packages/transaction-controller/src/hooks/CollectPublishHook.ts @@ -26,8 +26,6 @@ export class CollectPublishHook { readonly #readyPromise: DeferredPromise; - #signedCountWaiters: { count: number; resolve: () => void }[] = []; - constructor(transactionCount: number) { this.#readyPromise = createDeferredPromise(); this.#results = []; @@ -48,20 +46,6 @@ export class CollectPublishHook { return this.#readyPromise.promise; } - /** - * @param count - The number of signed transactions to wait for. - * @returns A promise that resolves when the given number of transactions have been signed. - */ - waitForSignedCount(count: number): Promise { - if (this.#results.length >= count) { - return Promise.resolve(); - } - - return new Promise((resolve) => { - this.#signedCountWaiters.push({ count, resolve }); - }); - } - /** * Resolve all publish promises with the provided transaction hashes. * @@ -113,14 +97,6 @@ export class CollectPublishHook { this.#results = sortBy(this.#results, (result) => result.nonce); - this.#signedCountWaiters = this.#signedCountWaiters.filter((waiter) => { - if (this.#results.length >= waiter.count) { - waiter.resolve(); - return false; - } - return true; - }); - if (this.#results.length === this.#transactionCount) { log('All transactions signed'); diff --git a/packages/transaction-controller/src/utils/batch.test.ts b/packages/transaction-controller/src/utils/batch.test.ts index 09666888a4c..88d11a7389d 100644 --- a/packages/transaction-controller/src/utils/batch.test.ts +++ b/packages/transaction-controller/src/utils/batch.test.ts @@ -69,9 +69,42 @@ const GAS_TOTAL_MOCK = '0x100000'; const VALUE_MOCK = '0x1234'; const MAX_FEE_PER_GAS_MOCK = '0x2'; const MAX_PRIORITY_FEE_PER_GAS_MOCK = '0x1'; +type StateChangeEntry = { + handler: (value: unknown) => void; + selector?: (state: TransactionControllerState) => unknown; +}; + +const stateChangeEntries: StateChangeEntry[] = []; + const MESSENGER_MOCK = { call: jest.fn(), + subscribe: jest.fn( + ( + _event: string, + handler: (value: unknown) => void, + selector?: (state: TransactionControllerState) => unknown, + ) => { + stateChangeEntries.push({ handler, selector }); + }, + ), + unsubscribe: jest.fn( + (_event: string, handler: (value: unknown) => void) => { + const index = stateChangeEntries.findIndex( + (entry) => entry.handler === handler, + ); + if (index !== -1) { + stateChangeEntries.splice(index, 1); + } + }, + ), } as unknown as TransactionControllerMessenger; + +function emitStateChange(state: TransactionControllerState): void { + for (const entry of [...stateChangeEntries]) { + const value = entry.selector ? entry.selector(state) : state; + entry.handler(value); + } +} const NETWORK_CLIENT_ID_MOCK = 'testNetworkClientId'; const PUBLIC_KEY_MOCK = '0x112233'; const BATCH_ID_MOCK = '0x654321'; @@ -332,10 +365,6 @@ describe('Batch Utils', () => { AddBatchTransactionOptions['signTransaction'] >; - let abortTransactionSigningMock: jest.MockedFn< - AddBatchTransactionOptions['abortTransactionSigning'] - >; - let request: AddBatchTransactionOptions; const estimateGasMock = jest.fn(); @@ -357,11 +386,19 @@ describe('Batch Utils', () => { addTransactionMock.mockImplementationOnce((_params, options) => { const signature = SIGNATURES_BY_INDEX[index] ?? SIGNATURES_BY_INDEX[0]; + const { transactionMeta } = returnValue; + options - .publishHook?.(returnValue.transactionMeta, signature) + .publishHook?.(transactionMeta, signature) .catch(() => { // Intentionally empty }); + + getTransactionMock.mockReturnValueOnce({ + ...transactionMeta, + status: TransactionStatus.signed, + }); + return Promise.resolve(returnValue); }); callIndex += 1; @@ -370,11 +407,18 @@ describe('Batch Utils', () => { beforeEach(() => { jest.resetAllMocks(); + stateChangeEntries.length = 0; mockMessengerNetworkCalls(); addTransactionMock = jest.fn(); - abortTransactionSigningMock = jest.fn(); - getTransactionMock = jest.fn(); + getTransactionMock = jest.fn().mockImplementation( + (id: string) => + ({ + id, + status: TransactionStatus.signed, + txParams: {}, + }) as unknown as TransactionMeta, + ); updateTransactionMock = jest.fn(); publishTransactionMock = jest.fn(); getPendingTransactionTrackerMock = jest.fn(); @@ -422,7 +466,6 @@ describe('Batch Utils', () => { signTransactionMock.mockResolvedValue(TRANSACTION_SIGNATURE_3_MOCK); request = { - abortTransactionSigning: abortTransactionSigningMock, addTransaction: addTransactionMock, estimateGas: estimateGasMock, getGasFeeEstimates: getGasFeeEstimatesMock, @@ -1813,6 +1856,13 @@ describe('Batch Utils', () => { }); getTransactionMock + .mockReturnValueOnce({ + id: TRANSACTION_ID_MOCK, + status: TransactionStatus.signed, + txParams: { + nonce: NONCE_MOCK, + }, + } as unknown as TransactionMeta) .mockReturnValueOnce({ txParams: { nonce: NONCE_MOCK, diff --git a/packages/transaction-controller/src/utils/batch.ts b/packages/transaction-controller/src/utils/batch.ts index 867bcaa6855..2b57d9f9711 100644 --- a/packages/transaction-controller/src/utils/batch.ts +++ b/packages/transaction-controller/src/utils/batch.ts @@ -73,7 +73,6 @@ type UpdateStateCallback = ( ) => void; type AddTransactionBatchRequest = { - abortTransactionSigning: TransactionController['abortTransactionSigning']; addTransaction: TransactionController['addTransaction']; estimateGas: TransactionController['estimateGas']; getGasFeeEstimates: ( @@ -459,6 +458,77 @@ async function addTransactionBatchWith7702( }; } +/** + * Wait for a transaction to reach a specific status. + * Checks the current state first to avoid race conditions, then subscribes to + * state changes for ongoing updates. + * + * @param transactionId - The ID of the transaction to monitor. + * @param targetStatus - The status to wait for. + * @param request - The batch request containing messenger and getTransaction. + */ +function waitForTransactionStatus( + transactionId: string, + targetStatus: TransactionStatus, + request: Pick, +): Promise { + const { getTransaction, messenger } = request; + const failureStatuses = [TransactionStatus.failed, TransactionStatus.rejected]; + + return new Promise((resolve, reject) => { + const checkStatus = ( + tx?: TransactionMeta, + unsubscribe?: () => void, + ): boolean => { + if (tx?.status === targetStatus) { + unsubscribe?.(); + resolve(); + return true; + } + + if (failureStatuses.includes(tx?.status as TransactionStatus)) { + unsubscribe?.(); + reject( + new Error( + tx?.error?.message ?? + `Transaction ${transactionId} ${tx?.status}`, + ), + ); + return true; + } + + return false; + }; + + try { + const initialTx = getTransaction(transactionId); + + if (checkStatus(initialTx)) { + return; + } + } catch { + // Transaction may not exist yet in state. + } + + const handler = (tx?: TransactionMeta): void => { + const unsubscribe = (): void => + messenger.unsubscribe( + 'TransactionController:stateChange', + handler, + ); + + checkStatus(tx, unsubscribe); + }; + + messenger.subscribe( + 'TransactionController:stateChange', + handler, + (state: TransactionControllerState) => + state.transactions.find((tx) => tx.id === transactionId), + ); + }); +} + /** * Process a batch transaction using a publish batch hook. * @@ -533,8 +603,6 @@ async function addTransactionBatchWithHook( const transactionCount = nestedTransactions.length; const collectHook = new CollectPublishHook(transactionCount); - const hookTransactions: Omit[] = []; - try { if (requireApproval) { txBatchMeta = await prepareApprovalData({ @@ -547,11 +615,13 @@ async function addTransactionBatchWithHook( } const publishHook = collectHook.getHook(); + const hookTransactions: Omit[] = + []; let index = 0; for (const nestedTransaction of nestedTransactions) { - const { hookTransaction, signingComplete } = + const { hookTransaction, alreadySigned } = await processTransactionWithHook( batchId, nestedTransaction, @@ -564,18 +634,13 @@ async function addTransactionBatchWithHook( hookTransactions.push(hookTransaction); index += 1; - const signingFailure = new Promise((_resolve, reject) => { - signingComplete.catch(reject); - }); - - await Promise.race([ - collectHook.waitForSignedCount(index), - signingFailure, - ]); - - signingFailure.catch(() => { - // Swallow – error is handled by the batch orchestrator. - }); + if (!alreadySigned) { + await waitForTransactionStatus( + String(hookTransaction.id), + TransactionStatus.signed, + request, + ); + } } const { signedTransactions } = await collectHook.ready(); @@ -622,14 +687,6 @@ async function addTransactionBatchWithHook( } catch (error) { log('Publish batch hook failed', error); - for (const hookTransaction of hookTransactions) { - try { - request.abortTransactionSigning(String(hookTransaction.id)); - } catch { - // Transaction may not be signing or may already be complete. - } - } - collectHook.error(error); resultCallbacks?.error(error as Error); @@ -662,7 +719,7 @@ async function processTransactionWithHook( hookTransaction: Omit & { type?: TransactionType; }; - signingComplete: Promise; + alreadySigned: boolean; }> { const { assetsFiatValues, existingTransaction, params, type } = nestedTransaction; @@ -709,26 +766,20 @@ async function processTransactionWithHook( transactionMeta = signResult.transactionMeta; } - const publishResult = publishHook(transactionMeta, signedTransaction).then( - (hookResult) => { + publishHook(transactionMeta, signedTransaction) + .then((hookResult) => { onPublish?.(hookResult); - return ''; - }, - ); - - publishResult.catch(() => { - // Swallow – error is handled by the batch orchestrator. - }); + }) + .catch(() => { + // Swallow – error is handled by the batch orchestrator. + }); log('Processed existing transaction with hook', { id, params, }); - return { - hookTransaction: { id, params }, - signingComplete: publishResult, - }; + return { hookTransaction: { id, params }, alreadySigned: true }; } const transactionMetaForGasEstimates = { @@ -743,7 +794,7 @@ async function processTransactionWithHook( }); } - const { transactionMeta, result: signingComplete } = await addTransaction( + const { transactionMeta } = await addTransaction( transactionMetaForGasEstimates.txParams, { assetsFiatValues, @@ -782,7 +833,7 @@ async function processTransactionWithHook( return { hookTransaction: { id, params: newParams, type }, - signingComplete, + alreadySigned: false, }; } From 7de81fefb3fbb05f953439641364b95aa7ce505f Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 28 Apr 2026 10:36:58 +0200 Subject: [PATCH 44/48] Fix lint --- .../src/utils/batch.test.ts | 26 ++++++++----------- .../transaction-controller/src/utils/batch.ts | 13 +++++----- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/packages/transaction-controller/src/utils/batch.test.ts b/packages/transaction-controller/src/utils/batch.test.ts index 88d11a7389d..a9faac29735 100644 --- a/packages/transaction-controller/src/utils/batch.test.ts +++ b/packages/transaction-controller/src/utils/batch.test.ts @@ -87,16 +87,14 @@ const MESSENGER_MOCK = { stateChangeEntries.push({ handler, selector }); }, ), - unsubscribe: jest.fn( - (_event: string, handler: (value: unknown) => void) => { - const index = stateChangeEntries.findIndex( - (entry) => entry.handler === handler, - ); - if (index !== -1) { - stateChangeEntries.splice(index, 1); - } - }, - ), + unsubscribe: jest.fn((_event: string, handler: (value: unknown) => void) => { + const index = stateChangeEntries.findIndex( + (entry) => entry.handler === handler, + ); + if (index !== -1) { + stateChangeEntries.splice(index, 1); + } + }), } as unknown as TransactionControllerMessenger; function emitStateChange(state: TransactionControllerState): void { @@ -388,11 +386,9 @@ describe('Batch Utils', () => { SIGNATURES_BY_INDEX[index] ?? SIGNATURES_BY_INDEX[0]; const { transactionMeta } = returnValue; - options - .publishHook?.(transactionMeta, signature) - .catch(() => { - // Intentionally empty - }); + options.publishHook?.(transactionMeta, signature).catch(() => { + // Intentionally empty + }); getTransactionMock.mockReturnValueOnce({ ...transactionMeta, diff --git a/packages/transaction-controller/src/utils/batch.ts b/packages/transaction-controller/src/utils/batch.ts index 2b57d9f9711..8cd2e969c9e 100644 --- a/packages/transaction-controller/src/utils/batch.ts +++ b/packages/transaction-controller/src/utils/batch.ts @@ -473,7 +473,10 @@ function waitForTransactionStatus( request: Pick, ): Promise { const { getTransaction, messenger } = request; - const failureStatuses = [TransactionStatus.failed, TransactionStatus.rejected]; + const failureStatuses = [ + TransactionStatus.failed, + TransactionStatus.rejected, + ]; return new Promise((resolve, reject) => { const checkStatus = ( @@ -490,8 +493,7 @@ function waitForTransactionStatus( unsubscribe?.(); reject( new Error( - tx?.error?.message ?? - `Transaction ${transactionId} ${tx?.status}`, + tx?.error?.message ?? `Transaction ${transactionId} ${tx?.status}`, ), ); return true; @@ -512,10 +514,7 @@ function waitForTransactionStatus( const handler = (tx?: TransactionMeta): void => { const unsubscribe = (): void => - messenger.unsubscribe( - 'TransactionController:stateChange', - handler, - ); + messenger.unsubscribe('TransactionController:stateChange', handler); checkStatus(tx, unsubscribe); }; From 829f9efd13d2a2dfd7ec816a1fbfda9d26a257ee Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 28 Apr 2026 10:45:27 +0200 Subject: [PATCH 45/48] Update lint --- .../src/utils/batch.test.ts | 23 +++++-------------- .../transaction-controller/src/utils/batch.ts | 5 ++-- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/packages/transaction-controller/src/utils/batch.test.ts b/packages/transaction-controller/src/utils/batch.test.ts index a9faac29735..6ddf40e2b04 100644 --- a/packages/transaction-controller/src/utils/batch.test.ts +++ b/packages/transaction-controller/src/utils/batch.test.ts @@ -97,12 +97,7 @@ const MESSENGER_MOCK = { }), } as unknown as TransactionControllerMessenger; -function emitStateChange(state: TransactionControllerState): void { - for (const entry of [...stateChangeEntries]) { - const value = entry.selector ? entry.selector(state) : state; - entry.handler(value); - } -} + const NETWORK_CLIENT_ID_MOCK = 'testNetworkClientId'; const PUBLIC_KEY_MOCK = '0x112233'; const BATCH_ID_MOCK = '0x654321'; @@ -384,17 +379,11 @@ describe('Batch Utils', () => { addTransactionMock.mockImplementationOnce((_params, options) => { const signature = SIGNATURES_BY_INDEX[index] ?? SIGNATURES_BY_INDEX[0]; - const { transactionMeta } = returnValue; - - options.publishHook?.(transactionMeta, signature).catch(() => { - // Intentionally empty - }); - - getTransactionMock.mockReturnValueOnce({ - ...transactionMeta, - status: TransactionStatus.signed, - }); - + options + .publishHook?.(returnValue.transactionMeta, signature) + .catch(() => { + // Intentionally empty + }); return Promise.resolve(returnValue); }); callIndex += 1; diff --git a/packages/transaction-controller/src/utils/batch.ts b/packages/transaction-controller/src/utils/batch.ts index 8cd2e969c9e..926c1f1f372 100644 --- a/packages/transaction-controller/src/utils/batch.ts +++ b/packages/transaction-controller/src/utils/batch.ts @@ -514,13 +514,13 @@ function waitForTransactionStatus( const handler = (tx?: TransactionMeta): void => { const unsubscribe = (): void => - messenger.unsubscribe('TransactionController:stateChange', handler); + messenger.unsubscribe('TransactionController:stateChanged', handler); checkStatus(tx, unsubscribe); }; messenger.subscribe( - 'TransactionController:stateChange', + 'TransactionController:stateChanged', handler, (state: TransactionControllerState) => state.transactions.find((tx) => tx.id === transactionId), @@ -768,6 +768,7 @@ async function processTransactionWithHook( publishHook(transactionMeta, signedTransaction) .then((hookResult) => { onPublish?.(hookResult); + return undefined; }) .catch(() => { // Swallow – error is handled by the batch orchestrator. From 9048c720dcf0f9d9e24c9a9481ac5d06efa5db0e Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 28 Apr 2026 10:49:34 +0200 Subject: [PATCH 46/48] Fix lint and remove unncessary changes --- .../src/utils/batch.test.ts | 1 - .../src/strategy/across/across-quotes.test.ts | 23 ------------------- .../src/utils/quote-gas.test.ts | 14 +---------- 3 files changed, 1 insertion(+), 37 deletions(-) diff --git a/packages/transaction-controller/src/utils/batch.test.ts b/packages/transaction-controller/src/utils/batch.test.ts index 6ddf40e2b04..e6527ffd736 100644 --- a/packages/transaction-controller/src/utils/batch.test.ts +++ b/packages/transaction-controller/src/utils/batch.test.ts @@ -97,7 +97,6 @@ const MESSENGER_MOCK = { }), } as unknown as TransactionControllerMessenger; - const NETWORK_CLIENT_ID_MOCK = 'testNetworkClientId'; const PUBLIC_KEY_MOCK = '0x112233'; const BATCH_ID_MOCK = '0x654321'; diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts index bb3353061aa..53d281b2f39 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts @@ -168,24 +168,12 @@ describe('Across Quotes', () => { estimateGasMock, estimateGasBatchMock, findNetworkClientIdByChainIdMock, - getKeyringControllerStateMock, getRemoteFeatureFlagControllerStateMock, } = getMessengerMock(); beforeEach(() => { jest.resetAllMocks(); - getKeyringControllerStateMock.mockReturnValue({ - isUnlocked: true, - keyrings: [ - { - type: 'HD Key Tree', - accounts: ['0x1234567890123456789012345678901234567891'], - metadata: { id: 'hd-keyring', name: 'HD Key Tree' }, - }, - ], - }); - getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { @@ -1337,17 +1325,6 @@ describe('Across Quotes', () => { }); it('returns per-transaction gas limits when account does not support 7702', async () => { - getKeyringControllerStateMock.mockReturnValue({ - isUnlocked: true, - keyrings: [ - { - type: 'Ledger Hardware', - accounts: ['0x1234567890123456789012345678901234567891'], - metadata: { id: 'ledger', name: 'Ledger' }, - }, - ], - }); - estimateGasBatchMock.mockResolvedValue({ totalGasLimit: 42000, gasLimits: [21000, 21000], diff --git a/packages/transaction-pay-controller/src/utils/quote-gas.test.ts b/packages/transaction-pay-controller/src/utils/quote-gas.test.ts index ad684d5685e..64cef9bccae 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.test.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.test.ts @@ -19,8 +19,7 @@ describe('quote gas estimation', () => { const getGasBufferMock = jest.mocked(getGasBuffer); const estimateGasLimitMock = jest.mocked(estimateGasLimit); - const { estimateGasBatchMock, getKeyringControllerStateMock, messenger } = - getMessengerMock(); + const { estimateGasBatchMock, messenger } = getMessengerMock(); const TRANSACTIONS_MOCK = [ { @@ -43,17 +42,6 @@ describe('quote gas estimation', () => { beforeEach(() => { jest.resetAllMocks(); - getKeyringControllerStateMock.mockReturnValue({ - isUnlocked: true, - keyrings: [ - { - type: 'HD Key Tree', - accounts: ['0x1234567890123456789012345678901234567891'], - metadata: { id: 'hd-keyring', name: 'HD Key Tree' }, - }, - ], - }); - getGasBufferMock.mockReturnValue(1); }); From 6fad0802499b190c51e669dd831c59379c5f88e3 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 28 Apr 2026 10:59:37 +0200 Subject: [PATCH 47/48] Address comments --- .../transaction-controller/src/utils/batch.ts | 69 ++++++++----------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/packages/transaction-controller/src/utils/batch.ts b/packages/transaction-controller/src/utils/batch.ts index 926c1f1f372..2817a200e37 100644 --- a/packages/transaction-controller/src/utils/batch.ts +++ b/packages/transaction-controller/src/utils/batch.ts @@ -459,17 +459,17 @@ async function addTransactionBatchWith7702( } /** - * Wait for a transaction to reach a specific status. + * Wait for a transaction to reach one of the specified statuses. * Checks the current state first to avoid race conditions, then subscribes to * state changes for ongoing updates. * * @param transactionId - The ID of the transaction to monitor. - * @param targetStatus - The status to wait for. + * @param targetStatuses - The statuses that indicate success. * @param request - The batch request containing messenger and getTransaction. */ function waitForTransactionStatus( transactionId: string, - targetStatus: TransactionStatus, + targetStatuses: TransactionStatus[], request: Pick, ): Promise { const { getTransaction, messenger } = request; @@ -483,7 +483,7 @@ function waitForTransactionStatus( tx?: TransactionMeta, unsubscribe?: () => void, ): boolean => { - if (tx?.status === targetStatus) { + if (targetStatuses.includes(tx?.status as TransactionStatus)) { unsubscribe?.(); resolve(); return true; @@ -502,25 +502,21 @@ function waitForTransactionStatus( return false; }; - try { - const initialTx = getTransaction(transactionId); + const initialTx = getTransaction(transactionId); - if (checkStatus(initialTx)) { - return; - } - } catch { - // Transaction may not exist yet in state. + if (checkStatus(initialTx)) { + return; } const handler = (tx?: TransactionMeta): void => { const unsubscribe = (): void => - messenger.unsubscribe('TransactionController:stateChanged', handler); + messenger.unsubscribe('TransactionController:stateChange', handler); checkStatus(tx, unsubscribe); }; messenger.subscribe( - 'TransactionController:stateChanged', + 'TransactionController:stateChange', // eslint-disable-line no-restricted-syntax handler, (state: TransactionControllerState) => state.transactions.find((tx) => tx.id === transactionId), @@ -620,26 +616,27 @@ async function addTransactionBatchWithHook( let index = 0; for (const nestedTransaction of nestedTransactions) { - const { hookTransaction, alreadySigned } = - await processTransactionWithHook( - batchId, - nestedTransaction, - publishHook, - request, - txBatchMeta, - index, - ); + const hookTransaction = await processTransactionWithHook( + batchId, + nestedTransaction, + publishHook, + request, + txBatchMeta, + index, + ); hookTransactions.push(hookTransaction); index += 1; - if (!alreadySigned) { - await waitForTransactionStatus( - String(hookTransaction.id), + await waitForTransactionStatus( + String(hookTransaction.id), + [ TransactionStatus.signed, - request, - ); - } + TransactionStatus.submitted, + TransactionStatus.confirmed, + ], + request, + ); } const { signedTransactions } = await collectHook.ready(); @@ -714,12 +711,9 @@ async function processTransactionWithHook( request: AddTransactionBatchRequest, txBatchMeta: TransactionBatchMeta | undefined, index: number, -): Promise<{ - hookTransaction: Omit & { - type?: TransactionType; - }; - alreadySigned: boolean; -}> { +): Promise< + Omit & { type?: TransactionType } +> { const { assetsFiatValues, existingTransaction, params, type } = nestedTransaction; @@ -779,7 +773,7 @@ async function processTransactionWithHook( params, }); - return { hookTransaction: { id, params }, alreadySigned: true }; + return { id, params }; } const transactionMetaForGasEstimates = { @@ -831,10 +825,7 @@ async function processTransactionWithHook( type, }); - return { - hookTransaction: { id, params: newParams, type }, - alreadySigned: false, - }; + return { id, params: newParams, type }; } /** From 4fa203c20fb6901c8373dc67babf011638ae1bb4 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 28 Apr 2026 11:06:40 +0200 Subject: [PATCH 48/48] Remove unnecessary diffs --- packages/transaction-controller/src/utils/batch.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/transaction-controller/src/utils/batch.ts b/packages/transaction-controller/src/utils/batch.ts index 2817a200e37..625fc6108c0 100644 --- a/packages/transaction-controller/src/utils/batch.ts +++ b/packages/transaction-controller/src/utils/batch.ts @@ -765,7 +765,7 @@ async function processTransactionWithHook( return undefined; }) .catch(() => { - // Swallow – error is handled by the batch orchestrator. + // Intentionally empty }); log('Processed existing transaction with hook', { @@ -773,7 +773,10 @@ async function processTransactionWithHook( params, }); - return { id, params }; + return { + id, + params, + }; } const transactionMetaForGasEstimates = { @@ -825,7 +828,11 @@ async function processTransactionWithHook( type, }); - return { id, params: newParams, type }; + return { + id, + params: newParams, + type, + }; } /**