Examples
Basic authorization
example.ts
import { walletClient } from './client'
const ACCOUNT_IMPLEMENTATION_ADDRESS = '0x…';
const authorization = await walletClient.signAuthorization({
contractAddress: ACCOUNT_IMPLEMENTATION_ADDRESS,
})
const hash = await walletClient.sendTransaction({
authorizationList: [authorization],
data: '0x',
to: walletClient.account.address,
})
Smart account initialization
Set code to a ZeroDev Kernel V3 implementation and initialize the smart account to make the EOA the owner of the account.
example.ts
import { concat, encodeFunctionData } from 'viem'
import kernelV3Abi from './abi'
import { walletClient } from './client'
const ZERODEV_KERNEL_V3_IMPLEMENTATION_ADDRESS = '0x…'
const ZERODEV_ECDSA_VALIDATOR_ADDRESS = '0x…'
const ROOT_VALIDATOR_ID = concat(['0x01', ZERODEV_ECDSA_VALIDATOR_ADDRESS])
const authorization = await walletClient.signAuthorization({
contractAddress: ZERODEV_KERNEL_V3_IMPLEMENTATION_ADDRESS,
})
const hash = await walletClient.sendTransaction({
authorizationList: [authorization],
data: encodeFunctionData({
abi: kernelV3Abi,
functionName: 'initialize',
args: [
ROOT_VALIDATOR_ID,
zeroAddress,
walletClient.account.address,
'0x',
[]
],
}),
to: walletClient.account.address,
})
Sending a user operation
Signs and sends a paymaster-sponsored UserOp using the ERC-4337 flow. Assumes the account is already initialized, and the EOA is the owner of the account.
Note that the process is exactly the same as when using a regular smart account. To minimize code surface, you can use a convenience library like permissionless.js.
example.ts
import { concat, encodeFunctionData, encodeAbiParameters, keccak256, padHex } from 'viem'
import { readContract } from 'viem/actions';
import { sendUserOperation, entryPoint07Abi, entryPoint07Address } from 'viem/account-abstraction';
import { mainnet } from 'viem/chains'
import kernelV3ImplementationAbi from './abi'
import { publicClient, walletClient, bundlerClient, paymasterClient } from './clients'
const executions = [
{
target: "0x0000000000000000000000000000000000000001",
value: 0n,
callData: "0xdeadbeef",
},
{
target: "0x0000000000000000000000000000000000000002",
value: 0n,
callData: "0xbeefdead",
},
]
const execMode =
"0x0100000000000000000000000000000000000000000000000000000000000000";
const executionCalldata = encodeAbiParameters(
[
{
type: "tuple[]",
components: [
{
type: "address",
name: "target",
},
{
type: "uint256",
name: "value",
},
{
type: "bytes",
name: "callData",
},
],
},
],
[executions]
);
const callData = encodeFunctionData({
abi: kernelV3ImplementationAbi,
functionName: "execute",
args: [execMode, executionCalldata],
});
const nonce = await readContract(publicClient, {
address: entryPoint07Address,
abi: entryPoint07Abi,
functionName: "getNonce",
args: [walletClient.account.address, nonceKey],
});
const { maxFeePerGas, maxPriorityFeePerGas } =
await publicClient.estimateFeesPerGas();
const userOperationGasResult = await bundlerClient.estimateUserOperationGas({
entryPointAddress: entryPoint07Address,
callData,
maxFeePerGas,
maxPriorityFeePerGas,
nonce,
sender: ownerAddress,
signature: stubSignature,
});
const callGasLimit = userOperationGasResult.callGasLimit;
const verificationGasLimit = userOperationGasResult.verificationGasLimit;
const preVerificationGas = userOperationGasResult.preVerificationGas;
const { paymasterPostOpGasLimit, paymasterVerificationGasLimit } =
await paymasterClient.getPaymasterStubData({
chainId: odysseyTestnet.id,
entryPointAddress: entryPoint07Address,
callData,
callGasLimit,
verificationGasLimit,
preVerificationGas,
maxFeePerGas,
maxPriorityFeePerGas,
nonce: actualNonce,
sender: ownerAddress,
});
const { paymaster, paymasterData } = await paymasterClient.getPaymasterData({
chainId: odysseyTestnet.id,
entryPointAddress: entryPoint07Address,
callData,
callGasLimit,
verificationGasLimit,
preVerificationGas,
maxFeePerGas,
maxPriorityFeePerGas,
paymasterPostOpGasLimit,
paymasterVerificationGasLimit,
nonce: actualNonce,
sender: ownerAddress,
});
const sender = walletClient.account.address;
const initCode = '0x';
const accountGasLimits = concat([
padHex(verificationGasLimit.toString(16), { size: 16 }),
padHex(callGasLimit.toString(16), { size: 16 }),
]);
const gasFees = concat([
padHex(maxPriorityFeePerGas.toString(16), { size: 16 }),
padHex(maxFeePerGas.toString(16), { size: 16 }),
]);
const paymasterAndData = concat([
padHex(paymaster.toLowerCase(), { size: 20 }),
padHex(paymasterVerificationGasLimit.toString(16), {
size: 16,
}),
padHex(paymasterPostOpGasLimit.toString(16), { size: 16 }),
paymasterData,
]);
const hashedInitCode = keccak256(initCode);
const hashedCallData = keccak256(callData);
const hashedPaymasterAndData = keccak256(paymasterAndData);
const packedUserOp = encodeAbiParameters(
[
{ type: "address" },
{ type: "uint256" },
{ type: "bytes32" },
{ type: "bytes32" },
{ type: "bytes32" },
{ type: "uint256" },
{ type: "bytes32" },
{ type: "bytes32" },
],
[
sender,
nonce,
hashedInitCode,
hashedCallData,
accountGasLimits,
preVerificationGas,
gasFees,
hashedPaymasterAndData,
]
);
const opHash = encodeAbiParameters(
[{ type: "bytes32" }, { type: "address" }, { type: "uint256" }],
[keccak256(packedUserOp), entryPoint07Address, BigInt(mainnet.id)]
);
const signature = await walletClient.signMessage({
message: {
raw: opHash,
}
});
await sendUserOperation(bundlerClient, {
entryPointAddress: entryPoint07Address,
sender,
nonce,
callData,
callGasLimit,
verificationGasLimit,
preVerificationGas,
maxPriorityFeePerGas,
maxFeePerGas,
paymaster,
paymasterData,
paymasterPostOpGasLimit,
paymasterVerificationGasLimit,
signature,
});