Getting Started with Account Abstraction
Account Abstraction (ERC-4337) is a proposal within the Ethereum ecosystem which aims to standardize Smart Contract Accounts (SCA) and their operations without the need to modify or upgrade the protocol.
Smart Contract Accounts can send calls on the Network via a "meta-transaction" called a User Operation. Users can send User Operations to Bundlers which aggregate User Operations into single transactions and submit them to the Network via an EntryPoint contract.
Key features that Account Abstraction enables are:
- Batching: Group multiple calls into a single transaction.
- Fee Sponsorship: Allow third parties to pay for gas fees, or pay for gas via ERC20 tokens.
- Arbitrary Signature Verification: Smart Contract Accounts can contain arbitrary signature verification logic (e.g. WebAuthn, secp256r1, secp256k1, ed25519, etc).
- Multi-Owner Wallets: Enable multiple owners to control a single account, and set rules for the owners.
- Recovery Mechanisms: A Smart Contract Account can assign multiple entities or services as trusted recovery agents for the Account.
- and many more...
Sending your first User Operation
1. Set up a Client
A Smart Account needs access to the Network to query for information about its state (e.g. nonce, address, etc). Let's set up a Client that we can use for the Smart Account:
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const client = createPublicClient({
chain: mainnet,
transport: http(),
})
2. Set up a Bundler Client
Next, we need to set up a Bundler Client. A Bundler is required to submit User Operations to the Network for the Smart Account.
import { createPublicClient, http } from 'viem'
import { createBundlerClient } from 'viem/account-abstraction'
import { mainnet } from 'viem/chains'
const client = createPublicClient({
chain: mainnet,
transport: http(),
})
const bundlerClient = createBundlerClient({
client,
transport: http('https://public.pimlico.io/v2/1/rpc'),
})
3. Set up an Owner
We also need to set up an Owner for the Smart Account which will be used to sign User Operations (transactions) for the Smart Account.
import { createPublicClient, http } from 'viem'
import { createBundlerClient } from 'viem/account-abstraction'
import { mainnet } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
const client = createPublicClient({
chain: mainnet,
transport: http(),
})
const bundlerClient = createBundlerClient({
client,
transport: http('https://public.pimlico.io/v2/1/rpc'),
})
const owner = privateKeyToAccount('0x...')
4. Create a Smart Account
Next, we instantiate a Smart Account. For this example, we will use toCoinbaseSmartAccount
(Coinbase Smart Wallet).
import { createPublicClient, http } from 'viem'
import {
createBundlerClient,
toCoinbaseSmartAccount
} from 'viem/account-abstraction'
import { mainnet } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
const client = createPublicClient({
chain: mainnet,
transport: http(),
})
const bundlerClient = createBundlerClient({
client,
transport: http('https://public.pimlico.io/v2/1/rpc'),
})
const owner = privateKeyToAccount('0x...')
const account = await toCoinbaseSmartAccount({
client,
owners: [owner]
})
See toCoinbaseSmartAccount
Docs
5. Send User Operation
Next, we send a User Operation to the Bundler. For the example below, we will send 0.001 ETH to a random address.
import { createPublicClient, http, parseEther } from 'viem'
import {
createBundlerClient,
toCoinbaseSmartAccount
} from 'viem/account-abstraction'
import { mainnet } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
const client = createPublicClient({
chain: mainnet,
transport: http(),
})
const bundlerClient = createBundlerClient({
client,
transport: http('https://public.pimlico.io/v2/1/rpc'),
})
const owner = privateKeyToAccount('0x...')
const account = await toCoinbaseSmartAccount({
client,
owners: [owner]
})
const hash = await bundlerClient.sendUserOperation({
account,
calls: [{
to: '0xcb98643b8786950F0461f3B0edf99D88F274574D',
value: parseEther('0.001')
}]
})
const receipt = await bundlerClient.waitForUserOperationReceipt({ hash })
6. Optional: Hoist the Account
If you do not wish to pass an account around to every Action that requires an account
, you can also hoist the account onto a Bundler Client.
import { createPublicClient, http, parseEther } from 'viem'
import { createBundlerClient, toCoinbaseSmartAccount } from 'viem/account-abstraction'
import { mainnet } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
const client = createPublicClient({
chain: mainnet,
transport: http(),
})
const owner = privateKeyToAccount('0x...')
const account = await toCoinbaseSmartAccount({
client,
owners: [owner]
})
const bundlerClient = createBundlerClient({
account,
client,
transport: http('https://public.pimlico.io/v2/1/rpc'),
})
const hash = await bundlerClient.sendUserOperation({
account,
calls: [{
to: '0xcb98643b8786950F0461f3B0edf99D88F274574D',
value: parseEther('0.001')
}]
})
7. Optional: Sponsor User Operation
By using a Paymaster, we can add sponsorship of User Operation fees.
Viem exposes a paymaster
property on both the Bundler Client ("on Client" tab) and User Operation Action ("on Action" tab) to add User Operation sponsorship capabilities.
The paymaster
property accepts a Paymaster Client (among others), which is used to fetch the necessary data for User Operation sponsorship.
import { http } from 'viem'
import {
createBundlerClient,
createPaymasterClient,
} from 'viem/account-abstraction'
import { account, client } from './config.ts'
const paymasterClient = createPaymasterClient({
transport: http('https://api.pimlico.io/v2/1/rpc?apikey={API_KEY}'),
})
const bundlerClient = createBundlerClient({
account,
client,
paymaster: paymasterClient,
transport: http('https://public.pimlico.io/v2/1/rpc'),
})
const hash = await bundlerClient.sendUserOperation({
calls: [{
to: '0xcb98643b8786950F0461f3B0edf99D88F274574D',
value: parseEther('0.001')
}]
})