From e6afc6c3249eddeb7f1f1c6a2e2573a312e87a54 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 7 Apr 2026 14:33:32 +0200 Subject: [PATCH 01/30] 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 57cd39afe3f..c52678cbcf9 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 415a912b9d6..90f7d8643a8 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -2076,15 +2076,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 962302cf373..eb276b7991a 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 4a50e9b832b..ec5b5ba274a 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 8e640262811..f8cf4a323b1 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 8c307a923dc..cf18f90c1d3 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -482,6 +482,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, @@ -532,7 +537,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), @@ -561,9 +568,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 320f07327dc..49e93eed8b4 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 3c89e22bf64e8eb79fc43d0a5e488e25f91faaff Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 7 Apr 2026 14:37:51 +0200 Subject: [PATCH 02/30] 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 b5faa90c035..2b3f737a959 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -63,6 +63,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 bfd33847e618248dfe9f68fe615e09db3269b7e0 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 7 Apr 2026 19:41:45 +0200 Subject: [PATCH 03/30] Update --- .../src/KeyringController.test.ts | 5 ++- .../src/strategy/across/across-quotes.test.ts | 40 +++++++++++++++++++ .../src/strategy/across/across-quotes.ts | 40 ++++++++++++++++++- .../src/strategy/across/across-submit.test.ts | 6 +-- .../src/tests/messenger-mock.ts | 2 +- 5 files changed, 85 insertions(+), 8 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index c52678cbcf9..d85af31d538 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 2b5e411625f..0a42ff6fd47 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', @@ -1253,6 +1255,44 @@ 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], + }); + + estimateGasMock.mockResolvedValue({ + gas: '0x5208', + simulationFails: undefined, + }); + + 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({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + 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 () => { const estimateQuoteGasLimitsSpy = jest.spyOn( quoteGasUtils, 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 ee84439f0f2..a5b800dd078 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); - const gasEstimates = await estimateQuoteGasLimits({ + let gasEstimates = await estimateQuoteGasLimits({ fallbackGas: acrossFallbackGas, messenger, transactions: orderedTransactions.map((transaction) => ({ @@ -413,7 +413,43 @@ async function calculateSourceNetworkCost( value: transaction.value ?? '0x0', })), }); - const { batchGasLimit, is7702 } = 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 eb276b7991a..043a13473f6 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 f61f594f7e0d5a0c9cedbf97130ad136d9e6f045 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 10:25:35 +0200 Subject: [PATCH 04/30] 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 043a13473f6..5973ce530c4 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 ec5b5ba274a..1b09c523295 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 f8cf4a323b1..a58317ea9ce 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 cf18f90c1d3..1d294b8ccce 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -549,12 +549,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', @@ -568,10 +569,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 e0be8776f0a3a02f73e91c7729a0d2d977ceef21 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 12:50:08 +0200 Subject: [PATCH 05/30] 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 e90c6dfe8a4..1ea4d1b9c3c 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -122,8 +122,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 1c7999b570c18b868a0c4d098ca7e35fc42d2cc7 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 13:10:01 +0200 Subject: [PATCH 06/30] 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 a58317ea9ce..b8fb8d5f4dc 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 04d55c153e7..a08912d8cad 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -96,7 +96,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); @@ -158,6 +159,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({ @@ -560,6 +562,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 3fb157811934a2c0a855efd5b1085d6e80201bfc Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 13:25:14 +0200 Subject: [PATCH 07/30] 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 90f7d8643a8..a981e063c9c 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -75,6 +75,7 @@ const MESSENGER_EXPOSED_METHODS = [ 'createNewVaultAndKeychain', 'createNewVaultAndRestore', 'removeAccount', + 'accountSupports7702', ] as const; /** @@ -2089,11 +2090,6 @@ export class KeyringController< this, MESSENGER_EXPOSED_METHODS, ); - - this.messenger.registerActionHandler( - `${name}:accountSupports7702`, - this.accountSupports7702.bind(this), - ); } /** From b409ea9dfead7ddbaca1b0b5e4bb56634b64f4c1 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 13:32:40 +0200 Subject: [PATCH 08/30] 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 6fb608c050d..7eaec81d58a 100644 --- a/packages/keyring-controller/src/KeyringController-method-action-types.ts +++ b/packages/keyring-controller/src/KeyringController-method-action-types.ts @@ -311,25 +311,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`; @@ -337,38 +319,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 823bd73bef8ba48282be69c1c544dcaf3f0de722 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 13:36:52 +0200 Subject: [PATCH 09/30] 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 9ac85f95897..ea349fdfff5 100644 --- a/packages/keyring-controller/src/index.ts +++ b/packages/keyring-controller/src/index.ts @@ -12,6 +12,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 b78422231e7..9185df50214 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, @@ -491,6 +494,7 @@ export type AllowedActions = | AccountsControllerGetSelectedAccountAction | AccountsControllerGetStateAction | ApprovalControllerAddRequestAction + | KeyringControllerAccountSupports7702Action | KeyringControllerSignEip7702AuthorizationAction | NetworkControllerFindNetworkClientIdByChainIdAction | NetworkControllerGetNetworkClientByIdAction From 78d9279ab9c279b4579741a9f01373d2a8357e61 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 8 Apr 2026 14:05:19 +0200 Subject: [PATCH 10/30] 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 2b3f737a959..c471f9b0e6f 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -50,6 +50,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 2ce4f121a0e6b0f9d4976b8757915209c4d2e6cb Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Thu, 9 Apr 2026 13:20:56 +0200 Subject: [PATCH 11/30] 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 b8fb8d5f4dc..584ea3bfb9c 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 1d294b8ccce..345e373dd25 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -549,6 +549,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 = { @@ -615,6 +617,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 91e66586df7a75ef877ece58b063134d62a61256 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 12:31:53 +0200 Subject: [PATCH 12/30] Address comments --- .../src/KeyringController.test.ts | 47 - .../src/KeyringController.ts | 13 +- 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 | 39 +- .../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, 1383 insertions(+), 1684 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index d85af31d538..57cd39afe3f 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 a981e063c9c..bb759b42205 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -75,7 +75,6 @@ const MESSENGER_EXPOSED_METHODS = [ 'createNewVaultAndKeychain', 'createNewVaultAndRestore', 'removeAccount', - 'accountSupports7702', ] as const; /** @@ -2077,14 +2076,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 ea349fdfff5..9ac85f95897 100644 --- a/packages/keyring-controller/src/index.ts +++ b/packages/keyring-controller/src/index.ts @@ -12,7 +12,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 9185df50214..b78422231e7 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, @@ -494,7 +491,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 c471f9b0e6f..9016cc83d4e 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -52,7 +52,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] @@ -67,7 +67,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 7f669956e50..7f6b50befa0 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, }); @@ -238,6 +239,7 @@ describe('TransactionPayController', () => { .mockResolvedValue(resultMock); new TransactionPayController({ + accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: getDelegationTransactionMock, messenger, }); @@ -268,6 +270,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, @@ -283,6 +286,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, @@ -306,6 +310,7 @@ describe('TransactionPayController', () => { getStrategyOrderMock.mockReturnValue([TransactionPayStrategy.Test]); new TransactionPayController({ + accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: jest.fn(), getStrategies: (): TransactionPayStrategy[] => [], messenger, @@ -323,6 +328,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 a1e48b3a2ca..aa48f66a1f9 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), @@ -251,6 +257,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 0a42ff6fd47..7bc656050ee 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( @@ -1206,11 +1134,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', @@ -1256,8 +1182,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], @@ -1283,6 +1207,7 @@ describe('Across Quotes', () => { } as Response); const result = await getAcrossQuotes({ + accountSupports7702: false, messenger, requests: [QUOTE_REQUEST_MOCK], transaction: TRANSACTION_META_MOCK, @@ -1321,11 +1246,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', ); @@ -1353,11 +1276,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', ); @@ -1377,11 +1298,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([ @@ -1409,11 +1328,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([ @@ -1476,11 +1393,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, @@ -1510,11 +1425,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([ { @@ -1551,11 +1464,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); }); @@ -1573,13 +1484,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'); @@ -1598,11 +1507,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'); @@ -1617,15 +1524,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 () => { @@ -1643,13 +1546,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 () => { @@ -1657,11 +1558,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; @@ -1677,21 +1576,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); }); @@ -1701,18 +1598,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); }); @@ -1723,11 +1618,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; @@ -1743,11 +1636,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); }); @@ -1763,11 +1654,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); }); @@ -1780,21 +1669,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); }); @@ -1805,18 +1692,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; @@ -1830,21 +1715,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 a5b800dd078..88e5301f9d5 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -193,7 +193,7 @@ async function normalizeQuote( request: QuoteRequest, fullRequest: PayStrategyGetQuotesRequest, ): Promise> { - const { messenger } = fullRequest; + const { accountSupports7702, messenger } = fullRequest; const { quote } = original; const { usdToFiatRate, sourceFiatRate, targetFiatRate } = getFiatRates( @@ -208,6 +208,7 @@ async function normalizeQuote( quote, messenger, request, + accountSupports7702, ); const targetNetwork = getFiatValueFromUsd(new BigNumber(0), usdToFiatRate); @@ -390,6 +391,7 @@ async function calculateSourceNetworkCost( quote: AcrossSwapApprovalResponse, messenger: TransactionPayControllerMessenger, request: QuoteRequest, + accountSupports7702: boolean, ): Promise<{ sourceNetwork: TransactionPayQuote['fees']['sourceNetwork']; gasLimits: AcrossGasLimits; @@ -412,43 +414,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 5973ce530c4..a17bdd40dc2 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?'), @@ -718,12 +696,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'); }); @@ -741,12 +717,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; @@ -767,12 +741,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'); }); @@ -786,12 +758,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'); }); @@ -804,12 +774,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'); }); @@ -834,12 +802,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; @@ -868,12 +834,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; @@ -900,12 +864,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 }; @@ -929,12 +891,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(); @@ -952,12 +912,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', ); @@ -978,12 +936,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; @@ -1011,12 +967,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; @@ -1045,12 +999,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(); }); @@ -1076,12 +1028,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 }; @@ -1144,12 +1094,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); }); @@ -1170,12 +1118,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 1b09c523295..1f2adf0dc0a 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 584ea3bfb9c..2aacbffb5f8 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 345e373dd25..8936f4e08a6 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); @@ -105,7 +108,7 @@ async function executeSingleQuote( const from = transaction.txParams.from as Hex; await submitHyperliquidWithdraw(quote, from, messenger); } else { - await submitTransactions(quote, transaction, messenger); + await submitTransactions(quote, transaction, messenger, accountSupports7702); } const targetHash = await waitForRelayCompletion( @@ -315,6 +318,7 @@ async function submitTransactions( quote: TransactionPayQuote, transaction: TransactionMeta, messenger: TransactionPayControllerMessenger, + accountSupports7702: boolean, ): Promise { const { steps } = quote.original; const txSteps = steps.filter( @@ -372,6 +376,7 @@ async function submitTransactions( quote, transaction, messenger, + accountSupports7702, normalizedParams, allParams, ); @@ -475,6 +480,7 @@ async function submitViaTransactionController( quote: TransactionPayQuote, transaction: TransactionMeta, messenger: TransactionPayControllerMessenger, + accountSupports7702: boolean, normalizedParams: TransactionParams[], allParams: TransactionParams[], ): Promise { @@ -482,11 +488,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, @@ -549,15 +550,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', @@ -571,9 +571,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]; @@ -601,13 +599,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, }); @@ -617,14 +615,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 49e93eed8b4..1504fef6a08 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 @@ -133,8 +129,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; @@ -428,6 +432,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; @@ -443,6 +450,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 128154ca037..793417f6518 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; @@ -53,8 +55,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 a08912d8cad..14b35039c5e 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, @@ -96,8 +97,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); @@ -118,6 +121,7 @@ describe('Quotes Utils', () => { */ async function run(params?: Partial): Promise { return await updateQuotes({ + accountSupports7702: accountSupports7702Mock, getStrategies: getStrategiesMock, messenger, transactionData: cloneDeep(TRANSACTION_DATA_MOCK), @@ -482,6 +486,7 @@ describe('Quotes Utils', () => { await run(); expect(getQuotesMock).toHaveBeenCalledWith({ + accountSupports7702: true, messenger, requests: [ { @@ -524,6 +529,7 @@ describe('Quotes Utils', () => { }); expect(getQuotesMock).toHaveBeenCalledWith({ + accountSupports7702: true, messenger, requests: [ expect.objectContaining({ @@ -759,6 +765,7 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, + accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(4); @@ -788,6 +795,7 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, + accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(4); @@ -818,6 +826,7 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, + accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(0); @@ -839,6 +848,7 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, + accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(0); @@ -891,6 +901,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 1ea4d1b9c3c..4942d41feae 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, @@ -31,6 +32,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; @@ -48,6 +50,7 @@ export async function updateQuotes( request: UpdateQuotesRequest, ): Promise { const { + accountSupports7702, getStrategies, messenger, transactionData, @@ -104,9 +107,12 @@ export async function updateQuotes( transactionId, }); + const supports7702 = await accountSupports7702(from); + const { batchTransactions, quotes } = await getQuotes( transaction, requests, + supports7702, getStrategies, messenger, transactionData.fiatPayment?.selectedPaymentMethodId, @@ -122,13 +128,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, @@ -169,7 +170,7 @@ function syncTransaction({ totals, transactionId, }: { - batchTransactions: BatchTransaction[]; + batchTransactions: BatchTransaction[] | undefined; isPostQuote?: boolean; messenger: TransactionPayControllerMessenger; paymentToken: TransactionPaymentToken | undefined; @@ -188,7 +189,9 @@ function syncTransaction({ }, (tx: TransactionMeta) => { tx.batchTransactions = batchTransactions; - tx.batchTransactionsOptions = {}; + tx.batchTransactionsOptions = batchTransactions?.length + ? {} + : undefined; tx.metamaskPay = { bridgeFeeFiat: totals.fees.provider.usd, @@ -209,11 +212,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); @@ -242,6 +247,7 @@ export async function refreshQuotes( } const isUpdated = await updateQuotes({ + accountSupports7702, getStrategies, messenger, transactionData, @@ -474,6 +480,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. @@ -482,6 +489,7 @@ async function refreshPaymentTokenBalance({ async function getQuotes( transaction: TransactionMeta, requests: QuoteRequest[], + accountSupports7702: boolean, getStrategies: (transaction: TransactionMeta) => TransactionPayStrategy[], messenger: TransactionPayControllerMessenger, fiatPaymentMethod?: string, @@ -508,6 +516,7 @@ async function getQuotes( } const request = { + accountSupports7702, fiatPaymentMethod, messenger, requests, From e1c6e6e246d2d7c2ab3a57f547184b75ae61fcda Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 12:33:22 +0200 Subject: [PATCH 13/30] 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 bb759b42205..415a912b9d6 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -2077,7 +2077,7 @@ export class KeyringController< } /** - * Constructor helper for registering this controller's messenger + * Constructor helper for registering this controller's messeger * actions. */ #registerMessageHandlers(): void { From 5078b8e815c8028f90b4446e6e8c9ffd134b86dd Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 12:45:55 +0200 Subject: [PATCH 14/30] 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 ce42d4591224e86d067af39905be8fe1f1785d7e Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:04:38 +0200 Subject: [PATCH 15/30] 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 7eaec81d58a..6fb608c050d 100644 --- a/packages/keyring-controller/src/KeyringController-method-action-types.ts +++ b/packages/keyring-controller/src/KeyringController-method-action-types.ts @@ -311,7 +311,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`; @@ -319,7 +337,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 ccb28a1d83bec48a0abc782f33e6ad236e50884f Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:05:30 +0200 Subject: [PATCH 16/30] 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 65878dd07b8be2e2bf7daf03fee9dfcf337ede92 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:10:12 +0200 Subject: [PATCH 17/30] 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 7bc656050ee..69f22f1fe07 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( @@ -1134,9 +1239,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', @@ -1246,9 +1354,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', ); @@ -1276,9 +1387,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', ); @@ -1298,9 +1412,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([ @@ -1328,9 +1445,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([ @@ -1393,9 +1513,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, @@ -1425,9 +1548,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([ { @@ -1464,9 +1590,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); }); @@ -1484,9 +1613,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'); @@ -1507,9 +1639,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'); @@ -1524,9 +1659,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'); }); @@ -1546,9 +1684,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'); }); @@ -1558,9 +1699,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; @@ -1576,19 +1720,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); }); @@ -1598,16 +1745,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); }); @@ -1618,9 +1768,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; @@ -1636,9 +1789,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); }); @@ -1654,9 +1810,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); }); @@ -1669,19 +1828,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); }); @@ -1692,16 +1854,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; @@ -1715,19 +1880,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 a17bdd40dc2..18dc1dc000d 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?'), @@ -696,10 +734,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'); }); @@ -717,10 +758,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; @@ -741,10 +785,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'); }); @@ -758,10 +805,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'); }); @@ -774,10 +824,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'); }); @@ -802,10 +855,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; @@ -834,10 +890,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; @@ -864,10 +923,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 }; @@ -891,10 +953,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(); @@ -912,10 +977,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', ); @@ -936,10 +1004,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; @@ -967,10 +1038,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; @@ -999,10 +1073,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(); }); @@ -1028,10 +1105,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 }; @@ -1094,10 +1174,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); }); @@ -1118,10 +1201,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 2aacbffb5f8..14bd20fa987 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 8936f4e08a6..4bb85f9c3b8 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -108,7 +108,12 @@ async function executeSingleQuote( const from = transaction.txParams.from as Hex; await submitHyperliquidWithdraw(quote, 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 1504fef6a08..4d536754fe5 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -130,9 +130,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 793417f6518..d0be26a6852 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.ts @@ -55,10 +55,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 14b35039c5e..2926fb79007 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -98,9 +98,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 4942d41feae..862a6125cdb 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -189,9 +189,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 da90b1816e24bf17fe551270f20f33c783612394 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:11:43 +0200 Subject: [PATCH 18/30] 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 028863f148a3981cfa4d1a0abad6ad3a0e5033e4 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:14:37 +0200 Subject: [PATCH 19/30] 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 9016cc83d4e..74758eb1db1 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -11,6 +11,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] @@ -50,10 +51,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 d9c202a786959b8cf35a53e775813be2598207e7 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:18:06 +0200 Subject: [PATCH 20/30] 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 7f6b50befa0..39506c1d841 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -466,6 +466,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 18dc1dc000d..91f47fbc935 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 14bd20fa987..ca6977e4816 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 2926fb79007..332c336ec7c 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -577,8 +577,8 @@ describe('Quotes Utils', () => { expect(transactionMetaMock).toMatchObject( expect.objectContaining({ - batchTransactions: [], - batchTransactionsOptions: {}, + batchTransactions: undefined, + batchTransactionsOptions: undefined, }), ); }); From da54d34c00e68816570532e841d420bb08409f64 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:22:31 +0200 Subject: [PATCH 21/30] 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 88e5301f9d5..cbabf0026a5 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -403,7 +403,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 9b36c4bc48fd02e9f3ec8772a9f2394af0b79686 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:27:29 +0200 Subject: [PATCH 22/30] 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 1f2adf0dc0a..47853c13166 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 4bb85f9c3b8..e87df4d086c 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( @@ -317,6 +318,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( @@ -477,6 +479,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 d0be26a6852..26c2e300f4b 100644 --- a/packages/transaction-pay-controller/src/utils/quote-gas.ts +++ b/packages/transaction-pay-controller/src/utils/quote-gas.ts @@ -73,14 +73,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 10acf7983aded357f70e248610555f93b66e4f2d Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 13:44:37 +0200 Subject: [PATCH 23/30] 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 cbabf0026a5..4e2b969fd1a 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -416,9 +416,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 47853c13166..0c3d48bdc21 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 1a14fc1b53ffc0a5f74afa08e568a9c4acf7372f Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 15:24:02 +0200 Subject: [PATCH 24/30] Update --- packages/transaction-controller/CHANGELOG.md | 4 + .../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, 215 insertions(+), 25 deletions(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 6bc74bdfa14..014f305387c 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- 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)) + ## [64.3.0] ### Added diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index b78422231e7..b2c24ead95d 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, @@ -491,6 +494,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 39506c1d841..5e8a5b31ffc 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); }); @@ -478,6 +492,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 aa48f66a1f9..822269e3caa 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), @@ -257,7 +253,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], @@ -267,6 +263,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 4d536754fe5..2f60df6c0d9 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 @@ -132,11 +137,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 9efcb5b16b51e93821e0917884d2fc54944d153e Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 15:26:37 +0200 Subject: [PATCH 25/30] 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 74758eb1db1..1d3150be6dd 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -11,7 +11,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 6f74bc0c8de7df2de59f8960de8ef106db0fbaa1 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 15:33:02 +0200 Subject: [PATCH 26/30] Update --- packages/transaction-pay-controller/CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 1d3150be6dd..94a1c7df61f 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,9 +9,14 @@ 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 the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388)) + +## [19.2.1] + +### Fixed + - 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] @@ -677,7 +682,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#6820](https://github.com/MetaMask/core/pull/6820)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@19.2.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@19.2.1...HEAD +[19.2.1]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@19.2.0...@metamask/transaction-pay-controller@19.2.1 [19.2.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@19.1.3...@metamask/transaction-pay-controller@19.2.0 [19.1.3]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@19.1.2...@metamask/transaction-pay-controller@19.1.3 [19.1.2]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@19.1.1...@metamask/transaction-pay-controller@19.1.2 From e41adb4e5ae6fed60c158270bd7ca5597334483c Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 15:36:01 +0200 Subject: [PATCH 27/30] 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 e6f5a871182f0cfe6b51d8a460554ace394fe7b2 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Fri, 17 Apr 2026 15:38:09 +0200 Subject: [PATCH 28/30] Update changelogs --- packages/transaction-pay-controller/CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 9a6e6adcba9..94a1c7df61f 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [19.2.1] - ### Fixed - 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)) From c03a7e4a49d5ac3b363711242643ce193775df0a Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Thu, 23 Apr 2026 10:30:28 +0200 Subject: [PATCH 29/30] Temp --- packages/transaction-controller/CHANGELOG.md | 4 +- .../transaction-controller/src/utils/batch.ts | 8 +- .../src/utils/eip7702.ts | 31 +++++++ .../transaction-pay-controller/CHANGELOG.md | 4 +- .../src/TransactionPayController.test.ts | 90 ------------------- .../src/TransactionPayController.ts | 14 --- .../src/helpers/QuoteRefresher.test.ts | 7 -- .../src/helpers/QuoteRefresher.ts | 7 -- .../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 | 11 +-- .../src/strategy/relay/relay-quotes.ts | 11 +-- .../src/strategy/relay/relay-submit.test.ts | 57 ++++++------ .../src/strategy/relay/relay-submit.ts | 24 ++--- .../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, 212 insertions(+), 283 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 014f305387c..e64f163417c 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- 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 ## [64.3.0] 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 7060ddd53e2..4950743854e 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -13,7 +13,9 @@ 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 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)) + - `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 5e8a5b31ffc..4bf9953bb58 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -253,7 +253,6 @@ describe('TransactionPayController', () => { .mockResolvedValue(resultMock); new TransactionPayController({ - accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: getDelegationTransactionMock, messenger, }); @@ -284,7 +283,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, @@ -300,7 +298,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, @@ -324,7 +321,6 @@ describe('TransactionPayController', () => { getStrategyOrderMock.mockReturnValue([TransactionPayStrategy.Test]); new TransactionPayController({ - accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: jest.fn(), getStrategies: (): TransactionPayStrategy[] => [], messenger, @@ -342,7 +338,6 @@ describe('TransactionPayController', () => { getStrategyOrderMock.mockReturnValue([TransactionPayStrategy.Bridge]); new TransactionPayController({ - accountSupports7702: jest.fn().mockResolvedValue(true), getDelegationTransaction: jest.fn(), getStrategies: (): TransactionPayStrategy[] => [undefined] as unknown as TransactionPayStrategy[], @@ -480,7 +475,6 @@ describe('TransactionPayController', () => { ); expect(updateQuotesMock).toHaveBeenCalledWith({ - accountSupports7702: expect.any(Function), getStrategies: expect.any(Function), messenger, transactionData: expect.objectContaining({ @@ -492,90 +486,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 822269e3caa..a1e48b3a2ca 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), @@ -253,7 +251,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], @@ -263,17 +260,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..e7b0b68de1f 100644 --- a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts +++ b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts @@ -5,7 +5,6 @@ import { noop } from 'lodash'; import { TransactionPayStrategy } from '../constants'; import { projectLogger } from '../logger'; import type { - AccountSupports7702Callback, TransactionPayControllerMessenger, TransactionPayControllerState, UpdateTransactionDataCallback, @@ -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 4e2b969fd1a..ee84439f0f2 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -193,7 +193,7 @@ async function normalizeQuote( request: QuoteRequest, fullRequest: PayStrategyGetQuotesRequest, ): Promise> { - const { accountSupports7702, messenger } = fullRequest; + const { messenger } = fullRequest; const { quote } = original; const { usdToFiatRate, sourceFiatRate, targetFiatRate } = getFiatRates( @@ -208,7 +208,6 @@ async function normalizeQuote( quote, messenger, request, - accountSupports7702, ); const targetNetwork = getFiatValueFromUsd(new BigNumber(0), usdToFiatRate); @@ -391,7 +390,6 @@ async function calculateSourceNetworkCost( quote: AcrossSwapApprovalResponse, messenger: TransactionPayControllerMessenger, request: QuoteRequest, - accountSupports7702: boolean, ): Promise<{ sourceNetwork: TransactionPayQuote['fees']['sourceNetwork']; gasLimits: AcrossGasLimits; @@ -414,7 +412,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 91f47fbc935..ccaf01f1981 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 0c3d48bdc21..0ccf7b1b63e 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts @@ -48,7 +48,6 @@ export async function submitAcrossQuotes( log('Executing quotes', request); const { quotes, messenger, transaction } = request; - const { accountSupports7702 } = request; let transactionHash: Hex | undefined; @@ -57,7 +56,6 @@ export async function submitAcrossQuotes( quote, messenger, transaction, - accountSupports7702, )); } @@ -68,7 +66,6 @@ async function executeSingleQuote( quote: TransactionPayQuote, messenger: TransactionPayControllerMessenger, transaction: TransactionMeta, - accountSupports7702: boolean, ): Promise<{ transactionHash?: Hex }> { log('Executing single quote', quote); @@ -89,7 +86,6 @@ async function executeSingleQuote( transaction.id, acrossDepositType, messenger, - accountSupports7702, ); updateTransaction( @@ -113,7 +109,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 +116,11 @@ async function submitTransactions( parentTransactionId: string, acrossDepositType: TransactionType, messenger: TransactionPayControllerMessenger, - accountSupports7702: boolean, ): Promise { const { swapTx } = quote.original.quote; - const { gasLimits: quoteGasLimits, is7702: apiIs7702 } = + 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 +217,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..0dad5a098f2 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, ): 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 ca6977e4816..534bbcd90e1 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 e87df4d086c..f84a8617f22 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); @@ -113,7 +109,6 @@ async function executeSingleQuote( quote, transaction, messenger, - accountSupports7702, ); } @@ -318,14 +313,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( @@ -383,7 +376,6 @@ async function submitTransactions( quote, transaction, messenger, - accountSupports7702, normalizedParams, allParams, ); @@ -479,7 +471,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. @@ -488,7 +479,6 @@ async function submitViaTransactionController( quote: TransactionPayQuote, transaction: TransactionMeta, messenger: TransactionPayControllerMessenger, - accountSupports7702: boolean, normalizedParams: TransactionParams[], allParams: TransactionParams[], ): Promise { @@ -545,8 +535,11 @@ async function submitViaTransactionController( quote.original.details.currencyIn.currency.chainId === quote.original.details.currencyOut.currency.chainId; + const { metamask } = quote.original; + const { gasLimits, is7702 } = metamask; + const authorizationList: AuthorizationList | undefined = - accountSupports7702 && + is7702 && isSameChain && quote.original.request.authorizationList?.length ? quote.original.request.authorizationList.map((a) => ({ @@ -555,11 +548,6 @@ async function submitViaTransactionController( })) : undefined; - const { metamask } = quote.original; - const { gasLimits } = metamask; - - const is7702 = metamask.is7702 && accountSupports7702; - if (allParams.length === 1) { const transactionParams = { ...allParams[0], @@ -607,13 +595,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 2f60df6c0d9..c962d059fb7 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -134,9 +134,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 26c2e300f4b..94e77287b4c 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; @@ -55,37 +54,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, @@ -108,6 +82,8 @@ export async function estimateQuoteGasLimits({ async function estimateQuoteGasLimitsBatch( transactions: QuoteGasTransaction[], messenger: TransactionPayControllerMessenger, + fallbackGas?: { estimate: number; max: number }, + fallbackOnSimulationFailure?: boolean, ): Promise<{ batchGasLimit?: QuoteGasLimit; gasLimits: QuoteGasLimit[]; @@ -159,6 +135,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 332c336ec7c..7a16c0bfe79 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, @@ -98,8 +97,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); @@ -120,7 +117,6 @@ describe('Quotes Utils', () => { */ async function run(params?: Partial): Promise { return await updateQuotes({ - accountSupports7702: accountSupports7702Mock, getStrategies: getStrategiesMock, messenger, transactionData: cloneDeep(TRANSACTION_DATA_MOCK), @@ -162,7 +158,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({ @@ -505,6 +500,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(); @@ -567,9 +587,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; @@ -577,8 +595,8 @@ describe('Quotes Utils', () => { expect(transactionMetaMock).toMatchObject( expect.objectContaining({ - batchTransactions: undefined, - batchTransactionsOptions: undefined, + batchTransactions: [BATCH_TRANSACTION_MOCK], + batchTransactionsOptions: {}, }), ); }); @@ -764,7 +782,6 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, - accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(4); @@ -794,7 +811,6 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, - accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(4); @@ -825,7 +841,6 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, - accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(0); @@ -847,7 +862,6 @@ describe('Quotes Utils', () => { messenger, updateTransactionDataMock, getStrategiesMock, - accountSupports7702Mock, ); expect(updateTransactionDataMock).toHaveBeenCalledTimes(0); @@ -899,25 +913,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 862a6125cdb..f2f54ef38d5 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 { getStrategiesByName, getStrategyByName } from './strategy'; import { computeTokenAmounts, @@ -32,7 +32,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; @@ -50,7 +49,6 @@ export async function updateQuotes( request: UpdateQuotesRequest, ): Promise { const { - accountSupports7702, getStrategies, messenger, transactionData, @@ -107,7 +105,7 @@ export async function updateQuotes( transactionId, }); - const supports7702 = await accountSupports7702(from); + const supports7702 = accountSupports7702(messenger, from); const { batchTransactions, quotes } = await getQuotes( transaction, @@ -129,7 +127,7 @@ export async function updateQuotes( log('Calculated totals', { transactionId, totals }); syncTransaction({ - batchTransactions: supports7702 ? batchTransactions : undefined, + batchTransactions, isPostQuote, messenger: messenger as never, paymentToken, @@ -210,13 +208,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); @@ -245,7 +241,6 @@ export async function refreshQuotes( } const isUpdated = await updateQuotes({ - accountSupports7702, getStrategies, messenger, transactionData, From c372c6d803e858425b31bd96687fb3e1089ac49b Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Thu, 23 Apr 2026 11:03:46 +0200 Subject: [PATCH 30/30] Update --- .../src/helpers/QuoteRefresher.ts | 8 ++++---- .../src/strategy/across/across-submit.ts | 4 +--- .../src/strategy/relay/relay-quotes.ts | 2 +- .../src/strategy/relay/relay-submit.ts | 20 ++++++++----------- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts index e7b0b68de1f..ec93f5c22db 100644 --- a/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts +++ b/packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts @@ -2,13 +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 { 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 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 0ccf7b1b63e..4a50e9b832b 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts @@ -48,7 +48,6 @@ export async function submitAcrossQuotes( log('Executing quotes', request); const { quotes, messenger, transaction } = request; - let transactionHash: Hex | undefined; for (const quote of quotes) { @@ -118,8 +117,7 @@ async function submitTransactions( messenger: TransactionPayControllerMessenger, ): Promise { const { swapTx } = quote.original.quote; - const { gasLimits: quoteGasLimits, is7702 } = - quote.original.metamask; + const { gasLimits: quoteGasLimits, is7702 } = quote.original.metamask; const { from } = quote.request; const chainId = toHex(swapTx.chainId); const orderedTransactions = getAcrossOrderedTransactions({ 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 0dad5a098f2..6b62bf8e506 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -810,7 +810,7 @@ async function calculateSourceNetworkCost( async function calculateSourceNetworkGasLimit( params: RelayTransactionStep['items'][0]['data'][], messenger: TransactionPayControllerMessenger, - fromOverride: Hex | undefined, + fromOverride?: Hex, ): Promise<{ totalGasEstimate: number; totalGasLimit: number; 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 f84a8617f22..8c307a923dc 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -105,11 +105,7 @@ async function executeSingleQuote( const from = transaction.txParams.from as Hex; await submitHyperliquidWithdraw(quote, from, messenger); } else { - await submitTransactions( - quote, - transaction, - messenger, - ); + await submitTransactions(quote, transaction, messenger); } const targetHash = await waitForRelayCompletion( @@ -535,19 +531,17 @@ async function submitViaTransactionController( quote.original.details.currencyIn.currency.chainId === quote.original.details.currencyOut.currency.chainId; - const { metamask } = quote.original; - const { gasLimits, is7702 } = metamask; - const authorizationList: AuthorizationList | undefined = - is7702 && - 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), })) : undefined; + const { metamask } = quote.original; + const { gasLimits } = metamask; + if (allParams.length === 1) { const transactionParams = { ...allParams[0], @@ -567,7 +561,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];