Guide
Signing
Every authorization in EIP-8130 is an auth blob: an authenticator address followed by its data. The authenticator decides how the data is verified, so one account can be controlled by secp256k1, P-256, passkey, or delegate keys. Viem's signer helpers produce the right data for each.
Auth blobs
A sender_auth takes one of two forms. On the EOA path (from unset) it is a raw 65-byte secp256k1 signature and the sender is recovered from it. On the configured-actor path (from set) it is authenticator || data, where the authenticator validates the data.
| Concept | Meaning |
|---|---|
| ECRECOVER (K1) | privateKeyToAccount(pk). data = r || s || v (65 bytes). The default authenticator. |
| P-256 | toP256Signer({ privateKey }). data = r || s || x || y || preHash (129 bytes). |
| WebAuthn / passkey | toWebAuthnSigner(toWebAuthnAccount({ credential })). data = ABI-encoded (WebAuthnAuth, x, y). |
| Delegate | Another account's signer + canonicalAuthenticators.delegate. Validates a signature produced for the linked account. |
secp256k1 owner
A K1 signer is just a Viem LocalAccount; its type is detected automatically and ECRECOVER is the default authenticator.
import { privateKeyToAccount } from 'viem/accounts'import { newSmartAccount } from 'viem/eip8130'
// secp256k1: the signer is a LocalAccount; ECRECOVER is the default authenticator.const account = newSmartAccount({ signer: privateKeyToAccount('0x…'),})P-256 key
toP256Signer signs over the digest directly (no re-hash) and carries the P-256 authenticator, so it drops straight into an account.
import { newSmartAccount, toP256Signer } from 'viem/eip8130'
const signer = toP256Signer({ privateKey: '0x…' })// signer.sign returns r || s || x || y || preHash (129 bytes)// signer.authenticator = canonical P-256 authenticator
const account = newSmartAccount({ signer }) // P-256 detected automaticallyWebAuthn passkey
toWebAuthnSigner wraps Viem's toWebAuthnAccount so the assertion is ABI-encoded into the passkey authenticator data. Each signature is a fresh user gesture.
import { createWebAuthnCredential, toWebAuthnAccount,} from 'viem/account-abstraction'import { newSmartAccount, toWebAuthnSigner } from 'viem/eip8130'
const credential = await createWebAuthnCredential({ name: 'vibes' })const signer = toWebAuthnSigner(toWebAuthnAccount({ credential }))
const account = newSmartAccount({ signer }) // WebAuthn detected automaticallyOne signer, everything
The same signer produces sender_auth for transactions and the auth for config changes: nothing else changes between the two.
import { actorScope, authorizeActor, key, sendTransaction,} from 'viem/eip8130'
// The same signer signs transactions…const hash = await sendTransaction(client, { account, calls, gas })
// …and config changes (authorize / revoke).const change = await account.change( [authorizeActor(key.p256({ x, y }), { scope: actorScope.operator })], { chainId: client.chain.id, sequence },)Signing messages (ERC-1271)
An EIP-8130 account signs messages and typed data too: account.signMessage and account.signTypedData return an ERC-1271 SignedMessageEnvelope signature. By default the envelope is multichain (sigType 0x02, bound to chainId 0), so it verifies for the account on every chain. Verify with core client.verifyMessage / client.verifyTypedData, which route through the account's isValidSignature.
import { privateKeyToAccount } from 'viem/accounts'import { newSmartAccount } from 'viem/eip8130'
const account = newSmartAccount({ signer: privateKeyToAccount('0x…') })
// Multichain ERC-1271 signature by default (sigType 0x02, binds chainId 0).const signature = await account.signMessage({ message: 'gm' })const typedSig = await account.signTypedData(typedData)
// Verify via core ERC-1271 (calls the account's isValidSignature):const valid = await client.verifyMessage({ address: account.address, message: 'gm', signature,})The wire form is sigType(1) || authenticator(20) || data. Use the standalone signMessageEnvelope / signTypedDataEnvelope helpers for non-K1 signers or to pick a channel: signatureType.multichain (0x02, the default) binds chainId 0; signatureType.local (0x01) binds block.chainid.
import { signMessageEnvelope, signatureType } from 'viem/eip8130'
// Standalone helper: P-256 / WebAuthn / delegate signers sign the SAME// envelope via their authenticator. Default sigType is 'multichain'.const multichain = await signMessageEnvelope({ signer, account: account.address, message: 'gm', authenticator, // defaults to the signer's authenticator})
// Chain-bound (local) signature: sigType 0x01 binds block.chainid.const local = await signMessageEnvelope({ signer, account: account.address, message: 'gm', sigType: 'local', chainId: client.chain.id,})
// Envelope layout: sigType(1) || authenticator(20) || datasignatureType.local // 0x01 (this chain only)signatureType.multichain // 0x02 (valid on all chains; the default)For canonical Keystore validation that also tells you which actor signed, use validateSignature, which returns { valid, actorId, scope }. For a not-yet-deployed account, wrap the signature with wrapCounterfactualSignature (ERC-6492) so verifiers can validate it before deployment.
import { validateSignature, wrapCounterfactualSignature,} from 'viem/eip8130'
// Canonical Keystore validation: returns WHICH actor/scope signed.const { valid, actorId, scope } = await validateSignature(client, { account: account.address, message: 'gm', signature,})
// Not-yet-deployed account? Wrap as ERC-6492 so verifiers can// validate it before deployment.const wrapped = wrapCounterfactualSignature({ signature, userSalt, code, initialActors,})Pricing non-K1 signatures
Authentication gas depends on the authenticator's blob shape, so gas estimates must know which authenticator will sign. Pass senderAuthAuthenticator (and payerAuthAuthenticator for a payer) matching the signing authenticator.
import { canonicalAuthenticators, estimateGas } from 'viem/eip8130'
const gas = await estimateGas(client, { sender: account.address, calls: [[{ to, data }]], // Price the sender auth from the RIGHT authenticator: senderAuthAuthenticator: canonicalAuthenticators.passkey,})