diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 52ca3f75e4c..8d033da6934 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `accountSupports7702` method and `KeyringController:accountSupports7702` messenger action to check whether an account's keyring supports EIP-7702 authorization signing ([#8388](https://github.com/MetaMask/core/pull/8388)) + ### Changed - Bump `@metamask/messenger` from `^1.0.0` to `^1.1.1` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373)) diff --git a/packages/keyring-controller/src/KeyringController-method-action-types.ts b/packages/keyring-controller/src/KeyringController-method-action-types.ts index 4a25c520bc0..1383bd2ebe2 100644 --- a/packages/keyring-controller/src/KeyringController-method-action-types.ts +++ b/packages/keyring-controller/src/KeyringController-method-action-types.ts @@ -297,6 +297,11 @@ export type KeyringControllerWithKeyringUnsafeAction = { handler: KeyringController['withKeyringUnsafe']; }; +export type KeyringControllerAccountSupports7702Action = { + type: `KeyringController:accountSupports7702`; + handler: KeyringController['accountSupports7702']; +}; + /** * Union of all KeyringController action types. */ @@ -320,4 +325,5 @@ export type KeyringControllerMethodActions = | KeyringControllerPatchUserOperationAction | KeyringControllerSignUserOperationAction | KeyringControllerWithKeyringAction - | KeyringControllerWithKeyringUnsafeAction; + | KeyringControllerWithKeyringUnsafeAction + | KeyringControllerAccountSupports7702Action; diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index 5ead0613292..8e9393c73da 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -1036,6 +1036,53 @@ 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 6c12c6e9485..7f682e5a021 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -68,6 +68,7 @@ const MESSENGER_EXPOSED_METHODS = [ 'createNewVaultAndKeychain', 'createNewVaultAndRestore', 'removeAccount', + 'accountSupports7702', ] as const; /** @@ -1826,10 +1827,14 @@ 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, diff --git a/packages/keyring-controller/src/index.ts b/packages/keyring-controller/src/index.ts index 320131ba586..68373d71a1b 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, KeyringControllerSignTypedMessageAction, diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index dae634f91df..268b6057969 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add optional `submissionMethod` property and `TransactionSubmissionMethod` enum to `TransactionMeta` ([#8375](https://github.com/MetaMask/core/pull/8375)) +- Add `KeyringController:accountSupports7702` action into allowed list. ([#8388](https://github.com/MetaMask/core/pull/8388)) ### Changed diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index b0bdd3bf222..49d089c5028 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, @@ -490,6 +493,7 @@ 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 9bc7266d427..741cb3b57a1 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- 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 @@ -20,6 +24,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] 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 766001d26f0..b1e91732f39 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 52fd12ab668..bc40d9cbc55 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 dbf8ab7cdc8..e5dc2c7cb27 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,34 @@ describe('Across Submit', () => { ); }); + it('submits individually when account does not support 7702', async () => { + accountSupports7702Mock.mockResolvedValue(false); + + const nonIs7702Quote = { + ...QUOTE_MOCK, + original: { + ...QUOTE_MOCK.original, + metamask: { + gasLimits: [ + { estimate: 21000, max: 21000 }, + { estimate: 22000, max: 22000 }, + ], + is7702: false, + }, + }, + } as unknown as TransactionPayQuote; + + await submitAcrossQuotes({ + messenger, + quotes: [nonIs7702Quote], + transaction: TRANSACTION_META_MOCK, + isSmartTransaction: jest.fn(), + }); + + expect(addTransactionBatchMock).not.toHaveBeenCalled(); + expect(addTransactionMock).toHaveBeenCalledTimes(2); + }); + 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 b4b73039a52..1d4e09e549d 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, @@ -196,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-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 85ca5bfa33e..e72d04e9122 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 8ad148cbf13..637725564f4 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 0e38d5bcea3..4f242bdd61c 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); @@ -388,6 +390,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], @@ -994,6 +1023,44 @@ describe('Relay Submit Utils', () => { ); }); + 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 = [21000, 22000]; + request.quotes[0].original.metamask.is7702 = true; + + await submitRelayQuotes(request); + + expect(addTransactionBatchMock).not.toHaveBeenCalled(); + 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/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 001973d3ef0..336ecd81567 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -476,6 +476,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, @@ -526,7 +531,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), @@ -536,24 +543,31 @@ async function submitViaTransactionController( const { metamask } = quote.original; const { gasLimits } = metamask; - if (allParams.length === 1) { - const transactionParams = { - ...allParams[0], - authorizationList, - gas: toHex(gasLimits[0]), - }; - - result = await messenger.call( - 'TransactionController:addTransaction', - transactionParams, - { - gasFeeToken, - networkClientId, - origin: ORIGIN_METAMASK, - requireApproval: false, - type: getRelayDepositType(transaction.type), - }, - ); + 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', + transactionParams, + { + ...(i === 0 ? { gasFeeToken } : {}), + networkClientId, + origin: ORIGIN_METAMASK, + requireApproval: false, + type: getTransactionType( + isPostQuote, + i, + transaction.type, + normalizedParams.length, + ), + }, + ); + } } else { const gasLimit7702 = metamask.is7702 ? toHex(metamask.gasLimits[0]) diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index 7ebda275cde..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, @@ -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 diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 7e0953e7cc0..4d5629a64c6 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(); diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index ed5c122effb..bf780e42375 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,