Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **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]

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -491,6 +494,7 @@ export type AllowedActions =
| AccountsControllerGetSelectedAccountAction
| AccountsControllerGetStateAction
| ApprovalControllerAddRequestAction
| KeyringControllerGetStateAction
| KeyringControllerSignEip7702AuthorizationAction
| NetworkControllerFindNetworkClientIdByChainIdAction
| NetworkControllerGetNetworkClientByIdAction
Expand Down
8 changes: 7 additions & 1 deletion packages/transaction-controller/src/utils/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import type {
import type { TransactionBatchResult, TransactionParams } from '../types';
import {
ERROR_MESSGE_PUBLIC_KEY,
doesAccountSupportEIP7702,
doesChainSupportEIP7702,
generateEIP7702BatchTransaction,
isAccountUpgradedToEIP7702,
Expand Down Expand Up @@ -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) {
Expand Down
31 changes: 31 additions & 0 deletions packages/transaction-controller/src/utils/eip7702.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
6 changes: 6 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Bump `@metamask/assets-controllers` from `^104.0.0` to `^104.1.0` ([#8509](https://github.com/MetaMask/core/pull/8509))

### Fixed

- **BREAKING:** Fix mUSD conversion for hardware wallets on EIP-7702 chains by gating relay and Across 7702 paths on the account keyring type via `KeyringController:getState` ([#8388](https://github.com/MetaMask/core/pull/8388))
- `AccountSupports7702Callback` type export has been removed. Use the `accountSupports7702` util from `utils/7702` instead.
- The `TransactionPayControllerMessenger` now requires `KeyringController:getState` permission (previously only needed in the publish hook).

## [19.2.1]

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -53,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);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('TransactionPayPublishHook', () => {
const {
messenger,
getControllerStateMock,
getKeyringControllerStateMock,
getTransactionControllerStateMock,
updateTransactionMock,
} = getMessengerMock();
Expand All @@ -51,6 +52,17 @@ describe('TransactionPayPublishHook', () => {
beforeEach(() => {
jest.resetAllMocks();

getKeyringControllerStateMock.mockReturnValue({
isUnlocked: true,
keyrings: [
{
type: 'HD Key Tree',
accounts: ['0xabc'],
metadata: { id: 'hd-keyring', name: 'HD Key Tree' },
},
],
});

hook = new TransactionPayPublishHook({
isSmartTransaction: isSmartTransactionMock,
messenger,
Expand Down Expand Up @@ -81,6 +93,7 @@ describe('TransactionPayPublishHook', () => {

expect(executeMock).toHaveBeenCalledWith(
expect.objectContaining({
accountSupports7702: true,
quotes: [QUOTE_MOCK, QUOTE_MOCK],
}),
);
Expand Down Expand Up @@ -141,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'));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
TransactionPayControllerMessenger,
TransactionPayQuote,
} from '../types';
import { accountSupports7702 } from '../utils/7702';
import { getStrategyByName } from '../utils/strategy';
import { updateTransaction } from '../utils/transaction';

Expand Down Expand Up @@ -81,8 +82,10 @@ export class TransactionPayPublishHook {
);

const strategy = getStrategyByName(quotes[0].strategy);
const from = transactionMeta.txParams.from as Hex;

return await strategy.execute({
accountSupports7702: accountSupports7702(this.#messenger, from),
isSmartTransaction: this.#isSmartTransaction,
quotes,
messenger: this.#messenger,
Expand Down
Loading
Loading