Sending User Operations
The guide below demonstrates how to send User Operations with a Smart Account.
Overview
Here is an end-to-end overview of how to broadcast a User Operation with a Smart Account. We will break it down into Steps below.
import { parseEther } from 'viem'
import { bundlerClient } from './config.js'
const hash = await bundlerClient.sendUserOperation({
account,
calls: [{
to: '0xcb98643b8786950F0461f3B0edf99D88F274574D',
value: parseEther('0.001')
}]
})
const receipt = await bundlerClient.waitForUserOperationReceipt({ hash })
Steps
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 will need to set up a Bundler Client. A Bundler is required to submit User Operations to the Blockchain for the Smart Account.
import { createPublicClient, http } from 'viem'
import { createBundlerClient } from 'viem/account-abstraction' // [!code ++]
import { mainnet } from 'viem/chains'
const client = createPublicClient({
chain: mainnet,
transport: http(),
})
const bundlerClient = createBundlerClient({ // [!code ++]
client, // [!code ++]
transport: http('https://public.pimlico.io/v2/1/rpc'), // [!code ++]
}) // [!code ++]
3. Set up an Owner
We will 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' // [!code ++]
const client = createPublicClient({
chain: mainnet,
transport: http(),
})
const bundlerClient = createBundlerClient({
client,
transport: http('https://public.pimlico.io/v2/1/rpc'),
})
const owner = privateKeyToAccount('0x...') // [!code ++]
4. Create a Smart Account
Next, we will instantiate a Smart Account. For this example, we will use toCoinbaseSmartAccount
(Coinbase Smart Wallet).
import { createPublicClient, http } from 'viem'
import { // [!code ++]
createBundlerClient, // [!code ++]
toCoinbaseSmartAccount // [!code ++]
} from 'viem/account-abstraction' // [!code ++]
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({ // [!code ++]
client, // [!code ++]
owners: [owner] // [!code ++]
}) // [!code ++]
See toCoinbaseSmartAccount
Docs
5. Send User Operation
Next, we will 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({ // [!code ++]
account, // [!code ++]
calls: [{ // [!code ++]
to: '0xcb98643b8786950F0461f3B0edf99D88F274574D', // [!code ++]
value: parseEther('0.001') // [!code ++]
}] // [!code ++]
}) // [!code ++]
const receipt = await bundlerClient.waitForUserOperationReceipt({ hash }) // [!code ++]
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 Wallet 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://public.pimlico.io/v2/11155111/rpc'),
})
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')
}]
})