Modular Account V2 • Getting started

It is easy to get started with Modular Account v2! Below, you will create a new Modular Account v2 client that will be used to send user operations. Your MAv2 smart account will be deployed on-chain when you send the first User Operation from a unique signer.

Install packages

Prerequisites

  • minimum Typescript version of 5

Installation

First, install the @account-kit/smart-contracts package.

$yarn add @account-kit/smart-contracts
>yarn add @account-kit/infra
Address calculation

For Modular Account V2, the address of the smart account will be calculated as a combination of the owner and the salt. You will get the same smart account address each time you supply the same owner, the signer(s) used to create the account for the first time. You can also optionally supply salt if you want a different address for the same owner param (the default salt is 0n).

If you want to use a signer to connect to an account whose address does not map to the contract-generated address, you can supply the accountAddress to connect with the account of interest. In that case, the signer address is not used for address calculation, but only for signing the operation.

Creating a Modular Account V2 client

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: 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 {
function generatePrivateKey(): Hex
generatePrivateKey
} from "viem/accounts";
const
const accountClient: { [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<...>; }
accountClient
= 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" | undefined
mode
: "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: Chain
sepolia
,
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" }), // 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(): Hex
generatePrivateKey
()),
});

:::tip[Choosing which mode to use] We currently offer two variants of Modular Account v2: default and 7702.

  • (Recommended) default provides you with the cheapest, most flexible and advanced Smart Account
  • 7702 if you are looking for 7702 support, learn about how to set up and take adavantage of our EIP-7702 compliant account here :::

Want to enable social login methods? Set up your Alchemy Signer.

Alternatively, you can bring a 3rd party signer as the owner of your new account.

Not sure what signer to use? Learn more.

Sending a user operation

Now that you have a client, you can send a User Operation. The first User Operation will also deploy the new Modular Account v2.

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: 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 {
function generatePrivateKey(): Hex
generatePrivateKey
} from "viem/accounts";
import {
function parseEther(ether: string, unit?: "wei" | "gwei"): bigint

Converts a string representation of ether to numerical wei.

Docs: https://viem.sh/docs/utilities/parseEther

parseEther
} from "viem";
const
const accountClient: { [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<...>; }
accountClient
= 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
({
chain: { blockExplorers?: { [key: string]: ChainBlockExplorer; default: ChainBlockExplorer; } | undefined; ... 7 more ...; testnet?: boolean | undefined; } & ChainConfig<...>

Chain for the client.

chain
:
const sepolia: Chain
sepolia
,
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" }),
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
()),
}); const
const operation: SendUserOperationResult<keyof EntryPointRegistryBase<unknown>>
operation
= await
const accountClient: { [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<...>; }
accountClient
.
sendUserOperation: (args: SendUserOperationParameters<ModularAccountV2<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>>, UserOperationContext | undefined, keyof EntryPointRegistryBase<...>>) => Promise<...>
sendUserOperation
({
// simple UO sending no data or value to vitalik's address
uo: UserOperationCallData | BatchUserOperationCallData
uo
: {
target: `0x${string}`
target
: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", // The address to call in the UO
data: `0x${string}`
data
: "0x", // The calldata to send in the UO
value?: bigint | undefined
value
:
function parseEther(ether: string, unit?: "wei" | "gwei"): bigint

Converts a string representation of ether to numerical wei.

Docs: https://viem.sh/docs/utilities/parseEther

parseEther
("0"), // The value to send in the UO
}, });
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream. * A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:


const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```

Example using the `Console` class:

```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);

myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console
.
Console.log(message?: any, ...optionalParams: any[]): void

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout

See util.format() for more information.

log
(
"User operation sent! \nUO hash: ",
const operation: SendUserOperationResult<keyof EntryPointRegistryBase<unknown>>
operation
.
hash: `0x${string}`
hash
,
"\nModular Account v2 Address: ",
const operation: SendUserOperationResult<keyof EntryPointRegistryBase<unknown>>
operation
.
request: UserOperationRequest<keyof EntryPointRegistryBase<unknown>>
request
.
sender: `0x${string}`
sender
,
);