Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d1d3ebc
add account upgrade and check status RPC methods
MoMannn Sep 30, 2025
0511ad1
Merge branch 'main' of https://github.com/MetaMask/metamask-extension…
MoMannn Sep 30, 2025
b173723
cast chainId to hex
MoMannn Sep 30, 2025
3f7117f
optimize tests
MoMannn Sep 30, 2025
7c7c089
Merge branch 'main' of https://github.com/MetaMask/metamask-extension…
MoMannn Sep 30, 2025
0890df6
get config via isAtomicBatchSupported
MoMannn Sep 30, 2025
e8ccc03
Merge branch 'main' into feat/account-upgrade-rpc
MoMannn Sep 30, 2025
21dc9b4
use txController
MoMannn Sep 30, 2025
244a65d
Merge branch 'feat/account-upgrade-rpc' of https://github.com/MetaMas…
MoMannn Sep 30, 2025
6f9f1a0
Fix supported chain check
MoMannn Oct 1, 2025
6a001c7
cursor configuration check fix
MoMannn Oct 1, 2025
3a2437c
Merge branch 'main' of https://github.com/MetaMask/metamask-extension…
MoMannn Oct 1, 2025
9e936a5
wait for transaction hash
MoMannn Oct 1, 2025
ab1fdbf
Merge branch 'main' into feat/account-upgrade-rpc
MoMannn Oct 1, 2025
9e4b674
test fix
MoMannn Oct 1, 2025
2703ec4
Merge branch 'feat/account-upgrade-rpc' of https://github.com/MetaMas…
MoMannn Oct 1, 2025
32ca828
Merge branch 'main' into feat/account-upgrade-rpc
MoMannn Oct 2, 2025
928c2c7
update import path
MoMannn Oct 2, 2025
b52fe35
Merge branch 'feat/account-upgrade-rpc' of https://github.com/MetaMas…
MoMannn Oct 2, 2025
4688eff
add null check to getCurrentChainId
MoMannn Oct 2, 2025
60717f2
use getCurrentChainIdForDomain
MoMannn Oct 3, 2025
f7711ca
Move rpc methods to external core package
MoMannn Oct 7, 2025
5ab0a9e
Merge branch 'main' into feat/account-upgrade-rpc
MoMannn Oct 7, 2025
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
4 changes: 3 additions & 1 deletion app/scripts/controller-init/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,9 @@ function getControllerOrThrow<Name extends ControllerName>(
const controller = controllersByName[name];

if (!controller) {
throw new Error(`Controller requested before it was initialized: ${name}`);
throw new Error(
`Controller requested before it was initialized: ${String(name)}`,
);
}

return controller;
Expand Down
101 changes: 100 additions & 1 deletion app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
ERC1155,
ERC20,
ERC721,
toHex,
} from '@metamask/controller-utils';

import { AccountsController } from '@metamask/accounts-controller';
Expand All @@ -69,7 +70,6 @@ import {
TransactionStatus,
TransactionType,
} from '@metamask/transaction-controller';

import { Interface } from '@ethersproject/abi';
import { abiERC1155, abiERC721 } from '@metamask/metamask-eth-abis';
import {
Expand Down Expand Up @@ -113,6 +113,11 @@ import {
walletSendCalls,
} from '@metamask/eip-5792-middleware';

import {
walletUpgradeAccount,
walletGetAccountUpgradeStatus,
} from '@metamask/eip-7702-internal-rpc-middleware';

import {
Caip25CaveatMutators,
Caip25CaveatType,
Expand All @@ -137,6 +142,7 @@ import {
SecretType,
RecoveryError,
} from '@metamask/seedless-onboarding-controller';
import { createEIP7702UpgradeTransaction } from '../../shared/lib/eip7702-utils';
import {
FEATURE_VERSION_2,
isMultichainAccountsFeatureEnabled,
Expand Down Expand Up @@ -218,6 +224,8 @@ import {
isGatorPermissionsFeatureEnabled,
} from '../../shared/modules/environment';
import { isSnapPreinstalled } from '../../shared/lib/snaps/snaps';
// eslint-disable-next-line import/no-restricted-paths
import { isFlask } from '../../ui/helpers/utils/build-types';
import { getShieldGatewayConfig } from '../../shared/modules/shield';
import {
HYPERLIQUID_ORIGIN,
Expand Down Expand Up @@ -1276,6 +1284,35 @@ export default class MetamaskController extends EventEmitter {
),
});

this.eip7702Middleware = createScaffoldMiddleware({
wallet_upgradeAccount: createAsyncMiddleware(async (req, res) => {
await walletUpgradeAccount(req, res, {
upgradeAccount: this.upgradeAccount.bind(this),
getCurrentChainIdForDomain:
this.getCurrentChainIdForDomain.bind(this),
isAtomicBatchSupported: this.txController.isAtomicBatchSupported.bind(
this.txController,
),
getAccounts: this.getPermittedAccounts.bind(this, req.origin),
});
}),
wallet_getAccountUpgradeStatus: createAsyncMiddleware(
async (req, res) => {
await walletGetAccountUpgradeStatus(req, res, {
getCurrentChainIdForDomain:
this.getCurrentChainIdForDomain.bind(this),
getCode: this.getCode.bind(this),
getNetworkConfigurationByChainId:
this.controllerMessenger.call.bind(
this.controllerMessenger,
'NetworkController:getNetworkConfigurationByChainId',
),
getAccounts: this.getPermittedAccounts.bind(this, req.origin),
});
},
),
});

this.metamaskMiddleware = createMetamaskMiddleware({
static: {
eth_syncing: false,
Expand Down Expand Up @@ -1539,6 +1576,22 @@ export default class MetamaskController extends EventEmitter {
}
}

/**
* Returns the current chainId (hex string) for a given domain/origin.
* Used by EIP-7702 middleware hooks.
* @param {string} domain
* @returns {string | undefined}
*/
getCurrentChainIdForDomain(domain) {
const networkClientId =
this.selectedNetworkController.getNetworkClientIdForDomain(domain);
const { chainId } =
this.networkController.getNetworkConfigurationByNetworkClientId(
networkClientId,
);
return chainId;
}

// Provides a method for getting feature flags for the multichain
// initial rollout, such that we can remotely modify polling interval
getInfuraFeatureFlags() {
Expand Down Expand Up @@ -6990,6 +7043,13 @@ export default class MetamaskController extends EventEmitter {
}),
);

if (
subjectType === SubjectType.Snap &&
(isFlask() || isSnapPreinstalled(origin))
) {
engine.push(this.eip7702Middleware);
}

if (subjectType !== SubjectType.Internal) {
engine.push(
this.permissionController.createPermissionMiddleware({
Expand Down Expand Up @@ -7445,6 +7505,7 @@ export default class MetamaskController extends EventEmitter {
...this.setupCommonMiddlewareHooks(origin),
}),
);

engine.push(this.metamaskMiddleware);

engine.push(this.eip5792Middleware);
Expand Down Expand Up @@ -8869,4 +8930,42 @@ export default class MetamaskController extends EventEmitter {
initRequest,
});
}

/**
* Upgrades an account to support EIP-7702 delegation.
* Uses shared EIP-7702 utility to avoid code duplication.
*
* @param {string} address - The account address to upgrade
* @param {string} upgradeContractAddress - The contract address to delegate to
* @param {number} chainId - The chain ID for the upgrade
* @returns {Promise<{transactionHash: string, delegatedTo: string}>}
*/
async upgradeAccount(address, upgradeContractAddress, chainId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth decomposing these hooks out to avoid bloating metamask-controller.js

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know this is the pattern used for all other implementations I would go with the same way.

// Get the network client for the specified chain
const networkClientId = this.networkController.findNetworkClientIdByChainId(
toHex(chainId),
);

return createEIP7702UpgradeTransaction(
{
address,
upgradeContractAddress,
networkClientId,
},
async (transactionParams, options) => {
const transactionMeta = await addTransaction(
this.getAddTransactionRequest({
transactionParams,
transactionOptions: {
...options,
origin: 'metamask',
requireApproval: true,
},
waitForSubmit: true,
}),
);
return transactionMeta;
},
);
}
}
8 changes: 8 additions & 0 deletions lavamoat/browserify/beta/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,14 @@
"uuid": true
}
},
"@metamask/eip-7702-internal-rpc-middleware": {
"packages": {
"@metamask/controller-utils": true,
"@metamask/rpc-errors": true,
"@metamask/superstruct": true,
"@metamask/utils": true
}
},
"@metamask/ens-controller": {
"packages": {
"@ethersproject/providers": true,
Expand Down
8 changes: 8 additions & 0 deletions lavamoat/browserify/experimental/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,14 @@
"uuid": true
}
},
"@metamask/eip-7702-internal-rpc-middleware": {
"packages": {
"@metamask/controller-utils": true,
"@metamask/rpc-errors": true,
"@metamask/superstruct": true,
"@metamask/utils": true
}
},
"@metamask/ens-controller": {
"packages": {
"@ethersproject/providers": true,
Expand Down
8 changes: 8 additions & 0 deletions lavamoat/browserify/flask/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,14 @@
"uuid": true
}
},
"@metamask/eip-7702-internal-rpc-middleware": {
"packages": {
"@metamask/controller-utils": true,
"@metamask/rpc-errors": true,
"@metamask/superstruct": true,
"@metamask/utils": true
}
},
"@metamask/ens-controller": {
"packages": {
"@ethersproject/providers": true,
Expand Down
8 changes: 8 additions & 0 deletions lavamoat/browserify/main/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,14 @@
"uuid": true
}
},
"@metamask/eip-7702-internal-rpc-middleware": {
"packages": {
"@metamask/controller-utils": true,
"@metamask/rpc-errors": true,
"@metamask/superstruct": true,
"@metamask/utils": true
}
},
"@metamask/ens-controller": {
"packages": {
"@ethersproject/providers": true,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@
"@metamask/design-system-tailwind-preset": "^0.6.1",
"@metamask/design-tokens": "^8.1.1",
"@metamask/eip-5792-middleware": "^1.1.0",
"@metamask/eip-7702-internal-rpc-middleware": "1.0.0",
"@metamask/ens-controller": "^17.0.1",
"@metamask/ens-resolver-snap": "^0.1.4",
"@metamask/error-reporting-service": "^2.0.0",
Expand Down
135 changes: 135 additions & 0 deletions shared/lib/eip7702-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {
TransactionEnvelopeType,
TransactionType,
TransactionParams,
} from '@metamask/transaction-controller';
import { Hex } from '@metamask/utils';

export const EIP_7702_REVOKE_ADDRESS =
'0x0000000000000000000000000000000000000000';

export type EIP7702TransactionParams = {
address: Hex;
upgradeContractAddress?: Hex;
networkClientId: string;
};

export type EIP7702TransactionResult = {
transactionHash: string;
delegatedTo?: string;
transactionId?: string;
};

/**
* Creates an EIP-7702 upgrade transaction.
*
* @param params - The transaction parameters
* @param addTransactionAndWaitForPublish - Function to add transaction and wait for publish
* @returns Promise with transaction result
*/
export async function createEIP7702UpgradeTransaction(
params: EIP7702TransactionParams,
addTransactionAndWaitForPublish: (
transactionParams: TransactionParams,
options: {
networkClientId: string;
requireApproval?: boolean;
type?: TransactionType;
},
) => Promise<Record<string, unknown>>,
): Promise<EIP7702TransactionResult> {
const { address, upgradeContractAddress, networkClientId } = params;

if (!upgradeContractAddress) {
throw new Error('Upgrade contract address is required');
}

const transactionParams: TransactionParams = {
authorizationList: [
{
address: upgradeContractAddress,
},
],
from: address,
to: address,
type: TransactionEnvelopeType.setCode,
};

const transactionMeta = await addTransactionAndWaitForPublish(
transactionParams,
{
networkClientId,
requireApproval: true,
type: TransactionType.batch,
},
);

return {
transactionHash: transactionMeta.hash as string,
delegatedTo: upgradeContractAddress,
transactionId: transactionMeta.id as string,
};
}

/**
* Creates an EIP-7702 downgrade transaction.
*
* @param params - The transaction parameters
* @param addTransactionAndWaitForPublish - Function to add transaction and wait for publish
* @returns Promise with transaction result
*/
export async function createEIP7702DowngradeTransaction(
params: EIP7702TransactionParams,
addTransactionAndWaitForPublish: (
transactionParams: TransactionParams,
options: {
networkClientId: string;
requireApproval?: boolean;
type?: TransactionType;
},
) => Promise<Record<string, unknown>>,
): Promise<EIP7702TransactionResult> {
const { address, networkClientId } = params;

const transactionParams: TransactionParams = {
authorizationList: [
{
address: EIP_7702_REVOKE_ADDRESS,
},
],
from: address,
to: address,
type: TransactionEnvelopeType.setCode,
};

const transactionMeta = await addTransactionAndWaitForPublish(
transactionParams,
{
networkClientId,
requireApproval: true,
type: TransactionType.revokeDelegation,
},
);

return {
transactionHash: transactionMeta.hash as string,
transactionId: transactionMeta.id as string,
};
}

/**
* Checks if an account is upgraded by checking if it has code.
*
* @param address - The account address to check
* @param networkClientId - The network client ID
* @param getCode - Function to get account code
* @returns Promise with boolean indicating if account is upgraded
*/
export async function isAccountUpgraded(
address: Hex,
networkClientId: string,
getCode: (address: Hex, networkClientId: string) => Promise<string | null>,
): Promise<boolean> {
const code = await getCode(address, networkClientId);
return Boolean(code && code.length > 2);
}
Loading
Loading