Guide
Keys & scopes
An EIP-8130 account is controlled by a set of actors. You rotate ownership by applying a signed config account-change that authorizes new actors and/or revokes existing ones. Because a config change is just an accountChanges entry, it rides inside a normal transaction.
Actors and keys
Each actor is { actorId, authenticator }. Build them with the key.* helpers:
import { key } from 'viem/experimental/eip8130'
key.k1('0xowner…') // secp256k1 (native ecrecover)key.p256({ x, y }) // P-256 public keykey.passkey({ x, y }) // WebAuthn / FIDO2 passkeykey.delegate('0xotherAccount') // signatures for another account act for this onekey.trustedExecutor('0xmgr…') // driven via executeBatch by msg.sender, not a signatureScopes
A scope is a bitmask of grants. Combine flags with toScope. Scope 0x00 is unrestricted admin: config changes, lock, delegation, and ERC-1271 signing all ride on admin scope (there is no separate signature or config bit).
| Concept | Meaning |
|---|---|
| actorScope.sender (0x01) | May originate transactions with the account as sender. |
| actorScope.policy (0x02) | Actor is gated to its policy manager. Set automatically when you pass a policy; do not set it by hand. |
| actorScope.nonce (0x04) | May use sequenced 2D nonce keys. Without it, the actor is restricted to nonce-free (expiring) transactions. |
| actorScope.selfPayer (0x08) | May pay for its own transactions (payer == sender). |
| actorScope.sponsorPayer (0x10) | May sponsor other accounts (payer != sender). |
| 0x00 (admin) | Unrestricted full owner. Omit scope (or pass 0) for a full-owner key. |
authorizeActor attaches a scope, optional expiry, and optional policy to a key:
import { actorScope, authorizeActor, key, toScope } from 'viem/experimental/eip8130'
authorizeActor(key.p256({ x, y }), { // Combine scope flags; 0 (omitted) = unrestricted admin. scope: toScope(actorScope.sender, actorScope.nonce), // Optional expiry (unix seconds); 0 / omitted = no expiry. expiry: BigInt(Math.floor(Date.now() / 1000) + 86_400),})Read the config sequence
Every config change is signed against the account's next local sequence. Read it first to avoid sequence-mismatch rejections from a stale cache.
import { getConfigSequence8130, getEip8130Deployment } from 'viem/experimental/eip8130'
const { accountConfiguration } = getEip8130Deployment(client.chain.id)!const { local: sequence } = await getConfigSequence8130(client, { accountConfiguration, account: account.address,})The returned local value is the per-chain sequence. The action also returns a multichain sequence for replayable chain_id 0 changes that propagate to every chain, covered in Multichain.
Authorize, revoke, rotate
Sign a change with account.change(...), then include it in a transaction's accountChanges. revokeActor accepts an actor or a raw actorId. Combine authorize + revoke in one change to rotate a key atomically:
import { actorScope, authorizeActor, key, revokeActor, sendCalls8130,} from 'viem/experimental/eip8130'
// Authorize the new key and revoke the old one in one atomic change.const rotate = await account.change( [ authorizeActor(key.k1('0xnewOwner…'), { scope: actorScope.sender }), revokeActor(key.k1('0xoldOwner…')), ], { chainId: client.chain.id, sequence: Number(sequence) },)
const hash = await sendCalls8130(client, { account, accountChanges: [rotate], calls: [], // config-only transaction gas: 200_000n,})Just-in-time vs. immediate
- Immediate: land the change now with a config-only transaction (
calls: []), as above. - Just-in-time: attach the change to a transaction that also does work, so the new key is authorized and used in the same send.
// Add a key and use it in the same transaction.const hash = await sendCalls8130(client, { account, accountChanges: [addKey], calls: [{ to, data }], gas: 300_000n,})Rotate during deployment
For a delegated EOA, you can delegate and install new keys in the very first transaction, no separate account handle required:
import { actorScope, authorizeActor, canonicalEip8130Deployment, key, sendCalls8130, toEoa8130Account,} from 'viem/experimental/eip8130'import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'
const account = toEoa8130Account(privateKeyToAccount(generatePrivateKey()))
const addP256 = await account.change( [authorizeActor(key.p256({ x, y }), { scope: actorScope.sender })], { chainId: client.chain.id, sequence: 0 },)
// Delegate the EOA and install the P-256 key in the very first transaction.const hash = await sendCalls8130(client, { account, accountChanges: [ account.delegate(canonicalEip8130Deployment.accounts.default), addP256, ], calls: [], gas: 300_000n,})