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 lightAccountClientlightAccountClient } 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 lightAccountClientlightAccountClient,
{ account: SmartContractAccount<string, keyof EntryPointRegistryBase<unknown>>account: import lightAccountClientlightAccountClient.anyaccount },
);
const const hash: anyhash = await import lightAccountClientlightAccountClient.anyupgradeAccount({
upgradeTo: {
implAddress: Address;
initializationData: Hex;
}upgradeTo: const upgradeToData: {
implAddress: Address;
initializationData: Hex;
}upgradeToData,
waitForTx: booleanwaitForTx: 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: 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 { 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(): HexgeneratePrivateKey } 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: 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" }),
chain: {
blockExplorers?: {
[key: string]: ChainBlockExplorer;
default: ChainBlockExplorer;
} | undefined;
... 7 more ...;
testnet?: boolean | undefined;
} & ChainConfig<...>Chain for the client.
chain: const sepolia: Chainsepolia,
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()),
});
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): 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 { 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 upgradedAccountupgradedAccount } 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: 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" }),
chain?: anyChain for the client.
chain: anylightAccountClient.anychain,
account?: anyaccount: import upgradedAccountupgradedAccount,
}).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 lightAccountClientlightAccountClient } 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 lightAccountClientlightAccountClient,
{ account: SmartContractAccount<string, keyof EntryPointRegistryBase<unknown>>account: import lightAccountClientlightAccountClient.anyaccount },
);
const const hash: anyhash = await import lightAccountClientlightAccountClient.anyupgradeAccount({
upgradeTo: {
implAddress: Address;
initializationData: Hex;
}upgradeTo: const upgradeToData: {
implAddress: Address;
initializationData: Hex;
}upgradeToData,
waitForTx: booleanwaitForTx: 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: 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 { 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(): HexgeneratePrivateKey } 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: 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" }),
chain: {
blockExplorers?: {
[key: string]: ChainBlockExplorer;
default: ChainBlockExplorer;
} | undefined;
... 7 more ...;
testnet?: boolean | undefined;
} & ChainConfig<...>Chain for the client.
chain: const sepolia: Chainsepolia,
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()),
});