Upgrading to MAv2
0. Construct your clients
First we need to check what kind of account address you have! The way client.account.address
is derived depends on the type of account and the salt. We’ll have to handle different cases depending on whether your client’s
address is a light account address or a modular account address. So we construct two clients:
Our Light Account client:
Our Modular Account client:
1. Check the implementation of your account (using client.account.getImplementationAddress()
):
Depending on the result, take different actions based on the state of the account.
For your Modular Account client:
For your Light Account client:
2. Upgrade Account!
Case A: Implementation Slot Exists and is a LightAccount
If the implementation slot exists and points to LightAccount, it means the account is a legacy LightAccount. Follow this path if the account has been deployed or if the account hasn’t been deployed but has assets at the counterfactual.
Steps:
- Retrieve the upgrade data
upgradeToData
and methodcreateMAV2Account
to create the MAv2 account usinggetMAV2UpgradeToData
. - Call
upgradeAccount
to prepare the upgrade data for MAv2 with the initializer set to give it the same owner as the LA. - Call
createSmartAccountClient
to create the MAv2 client from your newly upgraded account
Case B: Implementation Slot is Empty (No Account or Account Not Deployed With No Assets)
If the implementation slot is empty, this means that the account doesn’t exist. Or, if the account exists but has no assets, we still want to follow this path.
- Construct a MAv2 client with the signer to handle both cases:
- New user with no account.
- New user with an existing but un-deployed account.
import { function createModularAccountV2Client<TChain extends Chain = Chain, TSigner extends SmartAccountSigner = SmartAccountSigner<any>>(args: CreateModularAccountV2AlchemyClientParams<AlchemyTransport, TChain, TSigner>): Promise<ModularAccountV2Client<TSigner, TChain, AlchemyTransport>> (+1 overload)createModularAccountV2Client } from "@account-kit/smart-contracts";
import { class LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>Represents a local account signer and provides methods to sign messages and transactions, as well as static methods to create the signer from mnemonic or private key.
LocalAccountSigner } from "@aa-sdk/core";
import { const sepolia: Chainsepolia, function alchemy(config: AlchemyTransportConfig): AlchemyTransportCreates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.
alchemy } from "@account-kit/infra";
import { function generatePrivateKey(): HexgeneratePrivateKey } from "viem/accounts";
const const maV2Client: {
[x: string]: unknown;
account: ModularAccountV2<LocalAccountSigner<{
address: Address;
nonceManager?: NonceManager | undefined;
sign: (parameters: {
hash: Hash;
}) => Promise<Hex>;
... 6 more ...;
type: "local";
}>>;
... 84 more ...;
extend: <const client extends {
...;
} & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>;
}maV2Client = await createModularAccountV2Client<Chain, LocalAccountSigner<{
address: Address;
nonceManager?: NonceManager | undefined;
sign: (parameters: {
hash: Hash;
}) => Promise<Hex>;
... 6 more ...;
type: "local";
}>>(args: CreateModularAccountV2AlchemyClientParams<...>): Promise<...> (+1 overload)createModularAccountV2Client({
mode?: "default" | "7702" | undefinedmode: "default", // optional param to specify the MAv2 variant (either "default" or "7702")
chain: {
blockExplorers?: {
[key: string]: ChainBlockExplorer;
default: ChainBlockExplorer;
} | undefined;
... 7 more ...;
testnet?: boolean | undefined;
} & ChainConfig<...>Chain for the client.
chain: const sepolia: Chainsepolia,
transport: AlchemyTransportThe RPC transport
transport: function alchemy(config: AlchemyTransportConfig): AlchemyTransportCreates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.
alchemy({ apiKey: stringapiKey: "your-api-key" }), // Get your API key at https://dashboard.alchemy.com/apps or http("RPC_URL") for non-alchemy infra
signer: LocalAccountSigner<{
address: Address;
nonceManager?: NonceManager | undefined;
sign: (parameters: {
hash: Hash;
}) => Promise<Hex>;
... 6 more ...;
type: "local";
}>signer: class LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>Represents a local account signer and provides methods to sign messages and transactions, as well as static methods to create the signer from mnemonic or private key.
LocalAccountSigner.LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>.privateKeyToAccountSigner(key: Hex): LocalAccountSigner<PrivateKeyAccount>Creates a LocalAccountSigner
instance using the provided private key.
privateKeyToAccountSigner(function generatePrivateKey(): HexgeneratePrivateKey()),
});
This ensures that operations are executed on the already upgraded MAv2 account.
3. Send a User Operation!
Send a user operation to deploy your upgraded account!
And now you’ve upgraded your account to MAV2!
You can use a Modular Account v2 client to send user operations or use the React packages directly to simplify integration - this hook defaults to MAv2. Make sure to pass in an accountAddress
param to the client to connect the client to the existing account.