Modular Account • Upgrading to a Modular Account using Account Kit

Upgrading a SmartContractAccount can be done easily using Account Kit. It just involves a simple call to a single function on the SmartAccountClient, namely upgradeAccount, along with the necessary call data, UpgradeToData, for the account targeted for the upgrade. For upgrading to a Modular Account, you can use the utility function getMSCAUpgradeToData provided by the @account-kit/smart-contracts package to retrieve the call data for the upgrade. This process applies to any account with upgrade capabilities.

Using the Light Account as an example, here is an overview of how the upgrade can be executed using a Smart Account Client:

import { 
import lightAccountClient
lightAccountClient
} from "./lightAccountClient";
import {
function getMSCAUpgradeToData<TTransport extends Transport = Transport, TChain extends Chain | undefined = Chain | undefined, TSigner extends SmartAccountSigner = SmartAccountSigner<...>, TAccount extends SmartContractAccountWithSigner<string, TSigner> | undefined = SmartContractAccountWithSigner<...> | undefined>(client: SmartAccountClient<TTransport, TChain, TAccount>, args: GetMSCAUpgradeToData<TSigner, TAccount>): Promise<UpgradeToData & { createMAAccount: () => Promise<MultiOwnerModularAccount<TSigner>>; }>

Retrieves the data necessary to upgrade to a Multi-Signature Contract Account (MSCA) and provides a method to create a Multi-Owner Modular Account.

getMSCAUpgradeToData
} from "@account-kit/smart-contracts";
const {
const createMAAccount: () => Promise<MultiOwnerModularAccount<SmartAccountSigner<any>>>
createMAAccount
, ...
const upgradeToData: { implAddress: Address; initializationData: Hex; }
upgradeToData
} = await
getMSCAUpgradeToData<Transport, Chain | undefined, SmartAccountSigner<any>, SmartContractAccountWithSigner<string, SmartAccountSigner<any>> | undefined>(client: { ...; }, args: GetMSCAUpgradeToData<...>): Promise<...>

Retrieves the data necessary to upgrade to a Multi-Signature Contract Account (MSCA) and provides a method to create a Multi-Owner Modular Account.

getMSCAUpgradeToData
(
import lightAccountClient
lightAccountClient
,
{
account: SmartContractAccount<string, keyof EntryPointRegistryBase<unknown>>
account
:
import lightAccountClient
lightAccountClient
.
any
account
},
); const
const hash: any
hash
= await
import lightAccountClient
lightAccountClient
.
any
upgradeAccount
({
upgradeTo: { implAddress: Address; initializationData: Hex; }
upgradeTo
:
const upgradeToData: { implAddress: Address; initializationData: Hex; }
upgradeToData
,
waitForTx: boolean
waitForTx
: true,
}); const
const upgradedAccount: MultiOwnerModularAccount<SmartAccountSigner<any>>
upgradedAccount
= await
const createMAAccount: () => Promise<MultiOwnerModularAccount<SmartAccountSigner<any>>>
createMAAccount
();
import { 
function createLightAccountAlchemyClient<TSigner extends SmartAccountSigner = SmartAccountSigner<any>>(params: AlchemyLightAccountClientConfig<TSigner>): Promise<AlchemySmartAccountClient<Chain | undefined, LightAccount<TSigner>, LightAccountClientActions<TSigner>>>
createLightAccountAlchemyClient
} from "@account-kit/smart-contracts";
import {
const sepolia: Chain
sepolia
,
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates 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 {
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 {
function generatePrivateKey(): Hex
generatePrivateKey
} from "viem/accounts";
export const
const lightAccountClient: { account: LightAccount<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>>; ... 86 more ...; extend: <const client extends { ...; } & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>; }
lightAccountClient
= await
createLightAccountAlchemyClient<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>>(params: AlchemyLightAccountClientConfig<...>): Promise<...>
createLightAccountAlchemyClient
({
transport: AlchemyTransport

The RPC transport

transport
:
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates 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: string
apiKey
: "YOUR_API_KEY" }),
chain: { blockExplorers?: { [key: string]: ChainBlockExplorer; default: ChainBlockExplorer; } | undefined; ... 7 more ...; testnet?: boolean | undefined; } & ChainConfig<...>

Chain for the client.

chain
:
const sepolia: Chain
sepolia
,
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(): Hex
generatePrivateKey
()),
});

That is all! Now, you can create a smart account client to connect with the upgraded account as a Modular Account.

import { 
function createAlchemySmartAccountClient<TChain extends Chain = Chain, TAccount extends SmartContractAccount | undefined = SmartContractAccount | undefined, TContext extends UserOperationContext | undefined = UserOperationContext | undefined>(params: AlchemySmartAccountClientConfig<TChain, TAccount, TContext>): AlchemySmartAccountClient<TChain, TAccount, Record<string, never>, TContext>
createAlchemySmartAccountClient
,
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates 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 {
const multiOwnerPluginActions: <TTransport extends Transport = Transport, TChain extends Chain | undefined = Chain | undefined, TAccount extends SmartContractAccount | undefined = SmartContractAccount | undefined>(client: Client<TTransport, TChain, TAccount>) => MultiOwnerPluginActions<TAccount>

Creates actions for the MultiOwner plugin, including reading owners and checking ownership. NOTE: this is already added to the client returned from createMultiOwnerModularAccountClient

multiOwnerPluginActions
} from "@account-kit/smart-contracts";
import {
import upgradedAccount
upgradedAccount
} from "./upgradedAccount";
const
const upgradedAccountClient: Client<AlchemyTransport, any, any, [{ Method: "eth_sendUserOperation"; Parameters: [UserOperationRequest, Address]; ReturnType: Hash; }, ... 50 more ..., { ...; }], { ...; } & ... 5 more ... & AlchemySmartAccountClientActions<...>>
upgradedAccountClient
= await
createAlchemySmartAccountClient<any, any, UserOperationContext | undefined>(params: AlchemySmartAccountClientConfig<any, any, UserOperationContext | undefined>): { ...; }
createAlchemySmartAccountClient
({
transport: AlchemyTransport

The RPC transport

transport
:
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates 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: string
apiKey
: "YOUR_API_KEY" }),
chain?: any

Chain for the client.

chain
:
any
lightAccountClient
.
any
chain
,
account?: any
account
:
import upgradedAccount
upgradedAccount
,
}).
extend: <MultiOwnerPluginActions<any>>(fn: (client: Client<AlchemyTransport, any, any, [{ Method: "eth_sendUserOperation"; Parameters: [UserOperationRequest, Address]; ReturnType: Hash; }, ... 50 more ..., { ...; }], Record<...> & ... 4 more ... & AlchemySmartAccountClientActions<...>>) => MultiOwnerPluginActions<...>) => Client<...>
extend
(
const multiOwnerPluginActions: <TTransport extends Transport = Transport, TChain extends Chain | undefined = Chain | undefined, TAccount extends SmartContractAccount | undefined = SmartContractAccount | undefined>(client: Client<TTransport, TChain, TAccount>) => MultiOwnerPluginActions<TAccount>

Creates actions for the MultiOwner plugin, including reading owners and checking ownership. NOTE: this is already added to the client returned from createMultiOwnerModularAccountClient

multiOwnerPluginActions
);
const
const owners: readonly `0x${string}`[]
owners
= await
const upgradedAccountClient: Client<AlchemyTransport, any, any, [{ Method: "eth_sendUserOperation"; Parameters: [UserOperationRequest, Address]; ReturnType: Hash; }, ... 50 more ..., { ...; }], { ...; } & ... 5 more ... & AlchemySmartAccountClientActions<...>>
upgradedAccountClient
.
readOwners: (params: GetPluginAddressParameter & { account: SmartContractAccount<string, keyof EntryPointRegistryBase<unknown>>; }) => Promise<ReadonlyArray<Address>>
readOwners
();
import { 
import lightAccountClient
lightAccountClient
} from "./lightAccountClient";
import {
function getMSCAUpgradeToData<TTransport extends Transport = Transport, TChain extends Chain | undefined = Chain | undefined, TSigner extends SmartAccountSigner = SmartAccountSigner<...>, TAccount extends SmartContractAccountWithSigner<string, TSigner> | undefined = SmartContractAccountWithSigner<...> | undefined>(client: SmartAccountClient<TTransport, TChain, TAccount>, args: GetMSCAUpgradeToData<TSigner, TAccount>): Promise<UpgradeToData & { createMAAccount: () => Promise<MultiOwnerModularAccount<TSigner>>; }>

Retrieves the data necessary to upgrade to a Multi-Signature Contract Account (MSCA) and provides a method to create a Multi-Owner Modular Account.

getMSCAUpgradeToData
} from "@account-kit/smart-contracts";
const {
const createMAAccount: () => Promise<MultiOwnerModularAccount<SmartAccountSigner<any>>>
createMAAccount
, ...
const upgradeToData: { implAddress: Address; initializationData: Hex; }
upgradeToData
} = await
getMSCAUpgradeToData<Transport, Chain | undefined, SmartAccountSigner<any>, SmartContractAccountWithSigner<string, SmartAccountSigner<any>> | undefined>(client: { ...; }, args: GetMSCAUpgradeToData<...>): Promise<...>

Retrieves the data necessary to upgrade to a Multi-Signature Contract Account (MSCA) and provides a method to create a Multi-Owner Modular Account.

getMSCAUpgradeToData
(
import lightAccountClient
lightAccountClient
,
{
account: SmartContractAccount<string, keyof EntryPointRegistryBase<unknown>>
account
:
import lightAccountClient
lightAccountClient
.
any
account
},
); const
const hash: any
hash
= await
import lightAccountClient
lightAccountClient
.
any
upgradeAccount
({
upgradeTo: { implAddress: Address; initializationData: Hex; }
upgradeTo
:
const upgradeToData: { implAddress: Address; initializationData: Hex; }
upgradeToData
,
waitForTx: boolean
waitForTx
: true,
}); export const
const upgradedAccount: MultiOwnerModularAccount<SmartAccountSigner<any>>
upgradedAccount
= await
const createMAAccount: () => Promise<MultiOwnerModularAccount<SmartAccountSigner<any>>>
createMAAccount
();
import { 
function createLightAccountAlchemyClient<TSigner extends SmartAccountSigner = SmartAccountSigner<any>>(params: AlchemyLightAccountClientConfig<TSigner>): Promise<AlchemySmartAccountClient<Chain | undefined, LightAccount<TSigner>, LightAccountClientActions<TSigner>>>
createLightAccountAlchemyClient
} from "@account-kit/smart-contracts";
import {
const sepolia: Chain
sepolia
,
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates 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 {
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 {
function generatePrivateKey(): Hex
generatePrivateKey
} from "viem/accounts";
export const
const lightAccountClient: { account: LightAccount<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>>; ... 86 more ...; extend: <const client extends { ...; } & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>; }
lightAccountClient
= await
createLightAccountAlchemyClient<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>>(params: AlchemyLightAccountClientConfig<...>): Promise<...>
createLightAccountAlchemyClient
({
transport: AlchemyTransport

The RPC transport

transport
:
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates 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: string
apiKey
: "YOUR_API_KEY" }),
chain: { blockExplorers?: { [key: string]: ChainBlockExplorer; default: ChainBlockExplorer; } | undefined; ... 7 more ...; testnet?: boolean | undefined; } & ChainConfig<...>

Chain for the client.

chain
:
const sepolia: Chain
sepolia
,
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(): Hex
generatePrivateKey
()),
});