Skip to main content

Calibur Account

Calibur7702Account upgrades an EOA to a smart account via EIP-7702 delegation to the Calibur singleton. It provides batched transactions, passkey authentication, multi-key management with per-key hooks, and full ERC-4337 support through EntryPoint v0.8.

When to Use Calibur Account

  • Multi-key support: Register passkeys, session keys, or secondary signers alongside the EOA root key.
  • Per-key hooks: Attach custom validation logic to individual keys (e.g., spending limits, time locks).
  • Key expiration and admin separation: Set expiration timestamps and distinguish admin keys from non-admin keys.
  • WebAuthn/passkey authentication: Sign transactions with biometrics through the WebAuthn standard.
  • Production-grade contracts: Built by Uniswap, audited by OpenZeppelin and Cantina.

Supported ERCs: ERC-4337 (Account Abstraction), EIP-7702 (EOA delegation).

Supported key types:

TypeDescription
Secp256k1Standard Ethereum EOA key (keyType = 2). Used for EOA private keys and secondary signers.
WebAuthnP256P-256 key signed through the browser's WebAuthn API (keyType = 1). Used for passkeys, Face ID, fingerprint, and security keys. The signature includes WebAuthn metadata (authenticatorData, clientDataJSON) that the contract parses before verifying the underlying P-256 signature.
P256Raw P-256 / secp256r1 key (keyType = 0). Same elliptic curve as WebAuthnP256, but with a raw (r, s) signature format. Used when you have direct access to a P-256 key (e.g., from a secure enclave or HSM) without the WebAuthn wrapper.

Smart Contracts and Audits

The contracts were developed by Uniswap and audited by OpenZeppelin and Cantina.

How to Use

Prerequisites

Before using Calibur7702Account, you must have:

  • Node.js: Version 18.0 or higher.
  • EIP-7702 Compatible Network: Ethereum mainnet, Sepolia, or other EIP-7702 enabled chains.
  • Private Key Access: Required for signing authorizations and user operations.

Installation

npm install abstractionkit@0.2.39

Calibur support requires abstractionkit v0.2.39 or later. The contracts are audited by OpenZeppelin and Cantina.

Usage

import { Calibur7702Account } from "abstractionkit";

const eoaAddress = "0xBdbc5FBC9cA8C3F514D073eC3de840Ac84FC6D31"; // EOA public address
const smartAccount = new Calibur7702Account(eoaAddress);

Constructor defaults:

  • entrypointAddress: 0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108 (EntryPoint v0.8)
  • delegateeAddress: 0x000000009B1D0aF20D8C6d0A44e162d11F9b8f00 (Uniswap v1.0.0)

Both can be overridden by passing an overrides object as the second constructor argument.

const smartAccount = new Calibur7702Account(eoaAddress, {
entrypointAddress: "0x...", // optional
delegateeAddress: "0x...", // optional
});

Essential Methods

createUserOperation

Creates a UserOperation for EIP-7702 accounts that can be sent to bundlers for execution. Handles nonce fetching, gas estimation, and optional EIP-7702 delegation authorization.

example.ts
import { Calibur7702Account } from "abstractionkit";

const eoaAddress = "0xBdbc5FBC9cA8C3F514D073eC3de840Ac84FC6D31";
const smartAccount = new Calibur7702Account(eoaAddress);

const transactions = [
{
to: "0x9a7af758aE5d7B6aAE84fe4C5Ba67c041dFE5336",
value: 0n,
data: "0x...",
},
];

const userOperation = await smartAccount.createUserOperation(
transactions,
"https://ethereum-sepolia-rpc.publicnode.com", // provider RPC
"https://api.candide.dev/public/v3/sepolia", // bundler RPC
{
eip7702Auth: {
chainId: 11155111n, // required for the first UserOperation
},
}
);

Source code

createUserOperation

signUserOperation

Signs a UserOperation with a private key. Computes the UserOperation hash and wraps the ECDSA signature in Calibur's format. By default signs with the root key. To sign with a registered secondary key, pass its key hash via overrides.keyHash.

example.ts
// Sign with the root key (EOA private key)
userOperation.signature = smartAccount.signUserOperation(
userOperation,
"0x...private-key",
11155111n // chain ID
);

// Sign with a registered secondary key
userOperation.signature = smartAccount.signUserOperation(
userOperation,
"0x...secondary-private-key",
11155111n,
{ keyHash: "0x...registered-key-hash" }
);

Source code

signUserOperation

signUserOperationWithSigner

Signs a UserOperation with an external signer (viem, ethers Signer, hardware wallet, MPC signer, etc.). Computes the UserOperation hash and wraps the returned signature in Calibur's format. By default signs with the root key.

example.ts
import { privateKeyToAccount } from "viem/accounts";

const viemAccount = privateKeyToAccount("0x...private-key");

// Sign with a viem account
userOperation.signature = await smartAccount.signUserOperationWithSigner(
userOperation,
async (hash) => viemAccount.sign({ hash: hash as `0x${string}` }),
11155111n // chain ID
);

// Sign with a secondary key using a viem account
userOperation.signature = await smartAccount.signUserOperationWithSigner(
userOperation,
async (hash) => viemAccount.sign({ hash: hash as `0x${string}` }),
11155111n,
{ keyHash: "0x...registered-key-hash" }
);

Source code

signUserOperationWithSigner

formatWebAuthnSignature

Formats a WebAuthn (passkey) assertion into Calibur's signature format. Use this after getting an assertion from the browser's navigator.credentials.get() API. The challenge for the assertion should be the UserOperation hash.

example.ts
import { Calibur7702Account, WebAuthnSignatureData } from "abstractionkit";

// After getting a WebAuthn assertion from the browser
const webAuthnSignatureData: WebAuthnSignatureData = {
authenticatorData: "0x...",
clientDataJSON: '{"type":"webauthn.get","challenge":"...","origin":"https://example.com"}',
challengeIndex: 36n,
typeIndex: 8n,
r: 0x...n,
s: 0x...n,
};

const keyHash = Calibur7702Account.getKeyHash(webAuthnKey);

// Format the WebAuthn signature for the UserOperation
userOperation.signature = smartAccount.formatWebAuthnSignature(
keyHash,
webAuthnSignatureData,
);

Source code

formatWebAuthnSignature

sendUserOperation

Sends a signed UserOperation to the bundler for execution on-chain.

example.ts
const response = await smartAccount.sendUserOperation(
userOperation,
"https://api.candide.dev/public/v3/sepolia" // bundler URL
);

console.log("UserOperation hash:", response.userOperationHash);

// Wait for the transaction to be included
const receipt = await response.included();
console.log("Transaction receipt:", receipt);

Source code

sendUserOperation

Key Management Methods

createSecp256k1Key

Static method. Creates a secp256k1 key descriptor from an Ethereum address.

example.ts
import { Calibur7702Account } from "abstractionkit";

const secondaryAddress = "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18";
const key = Calibur7702Account.createSecp256k1Key(secondaryAddress);

console.log(key);
// { keyType: 2, publicKey: "0x000...742d35Cc6634C0532925a3b844Bc9e7595f2bD18" }

Source code

createSecp256k1Key

createWebAuthnP256Key

Static method. Creates a WebAuthn P-256 key descriptor from public key coordinates. Use this when registering a passkey on the account.

example.ts
import { Calibur7702Account } from "abstractionkit";

// Extract x, y coordinates from a WebAuthn credential
const x = 0x1234...n; // P-256 public key x coordinate
const y = 0x5678...n; // P-256 public key y coordinate

const webAuthnKey = Calibur7702Account.createWebAuthnP256Key(x, y);
const keyHash = Calibur7702Account.getKeyHash(webAuthnKey);
console.log("Key hash:", keyHash);

Source code

createWebAuthnP256Key

createP256Key

Static method. Creates a raw P-256 (secp256r1) key descriptor from public key coordinates. Use this for non-WebAuthn P-256 keys.

example.ts
import { Calibur7702Account } from "abstractionkit";

const x = 0x1234...n; // P-256 public key x coordinate
const y = 0x5678...n; // P-256 public key y coordinate

const p256Key = Calibur7702Account.createP256Key(x, y);
const keyHash = Calibur7702Account.getKeyHash(p256Key);
console.log("Key hash:", keyHash);

Source code

createP256Key

getKeyHash

Static method. Computes the key hash for a Calibur key. Uses double hashing: keccak256(abi.encode(uint8 keyType, bytes32 keccak256(publicKey))).

example.ts
import { Calibur7702Account } from "abstractionkit";

const key = Calibur7702Account.createSecp256k1Key("0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18");
const keyHash = Calibur7702Account.getKeyHash(key);

console.log("Key hash:", keyHash);
// bytes32 hex string, e.g. "0xabc123..."

Source code

getKeyHash

createRegisterKeyMetaTransactions

Static method. Creates meta-transactions to register a new key on the Calibur account. Returns two transactions ([register, update]) that must both be included in the same UserOperation. For safety, isAdmin is always forced to false.

example.ts
import { Calibur7702Account } from "abstractionkit";

// Create the key to register
const newKey = Calibur7702Account.createSecp256k1Key("0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18");

// Build register + update transactions (both required in the same UserOp)
const registerTxs = Calibur7702Account.createRegisterKeyMetaTransactions(
newKey,
{
expiration: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, // 30 days
}
);

// Include both transactions in the UserOperation
const userOperation = await smartAccount.createUserOperation(
registerTxs, // array of two SimpleMetaTransactions
nodeRpc,
bundlerRpc,
);

Source code

createRegisterKeyMetaTransactions

createRevokeKeyMetaTransaction

Static method. Creates a meta-transaction to revoke a key from the Calibur account. Only admin keys can execute revoke operations.

example.ts
import { Calibur7702Account } from "abstractionkit";

const keyHash = "0x...key-hash-to-revoke";
const revokeTx = Calibur7702Account.createRevokeKeyMetaTransaction(keyHash);

// Include in a UserOperation signed by an admin key
const userOperation = await smartAccount.createUserOperation(
[revokeTx],
nodeRpc,
bundlerRpc,
);

Source code

createRevokeKeyMetaTransaction

createUpdateKeySettingsMetaTransaction

Static method. Creates a meta-transaction to update settings for a registered key. Only admin keys can execute update operations. Throws if isAdmin is set to true in the settings.

example.ts
import { Calibur7702Account } from "abstractionkit";

const keyHash = "0x...registered-key-hash";

// Extend key expiration to 1 year from now
const updateTx = Calibur7702Account.createUpdateKeySettingsMetaTransaction(
keyHash,
{
expiration: Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60,
}
);

const userOperation = await smartAccount.createUserOperation(
[updateTx],
nodeRpc,
bundlerRpc,
);

Source code

createUpdateKeySettingsMetaTransaction

packKeySettings

Static method. Packs key settings into a single uint256 value. Layout: (isAdmin << 200) | (expiration << 160) | hook.

example.ts
import { Calibur7702Account } from "abstractionkit";

const packed = Calibur7702Account.packKeySettings({
hook: "0x0000000000000000000000000000000000000000",
expiration: Math.floor(Date.now() / 1000) + 86400, // 1 day
isAdmin: false,
});

console.log("Packed settings:", packed);

Source code

packKeySettings

unpackKeySettings

Static method. Unpacks a uint256 settings value into a CaliburKeySettingsResult object with all fields populated.

example.ts
import { Calibur7702Account } from "abstractionkit";

const packed = 0x...n; // packed settings from the contract

const settings = Calibur7702Account.unpackKeySettings(packed);
console.log("Admin:", settings.isAdmin);
console.log("Expiration:", settings.expiration);
console.log("Hook:", settings.hook);

Source code

unpackKeySettings

Read Methods

isKeyRegistered

Checks if a key is registered on this account by querying the on-chain isRegistered(bytes32) function.

example.ts
const keyHash = Calibur7702Account.getKeyHash(key);
const isRegistered = await smartAccount.isKeyRegistered(
"https://ethereum-sepolia-rpc.publicnode.com",
keyHash,
);

console.log("Key registered:", isRegistered);

Source code

isKeyRegistered

getKeySettings

Gets the settings for a registered key. Returns a CaliburKeySettingsResult with hook address, expiration timestamp, and admin flag.

example.ts
const keyHash = Calibur7702Account.getKeyHash(key);
const settings = await smartAccount.getKeySettings(
"https://ethereum-sepolia-rpc.publicnode.com",
keyHash,
);

console.log("Admin:", settings.isAdmin);
console.log("Expiration:", settings.expiration === 0 ? "never" : new Date(settings.expiration * 1000).toISOString());
console.log("Hook:", settings.hook);

Source code

getKeySettings

getKey

Gets the full key data for a registered key, including the key type and ABI-encoded public key bytes.

example.ts
const keyHash = "0x...registered-key-hash";
const key = await smartAccount.getKey(
"https://ethereum-sepolia-rpc.publicnode.com",
keyHash,
);

console.log("Key type:", key.keyType); // 0 = P256, 1 = WebAuthnP256, 2 = Secp256k1
console.log("Public key:", key.publicKey);

Source code

getKey

listKeys

Lists all keys registered on this account. Iterates keyCount() and keyAt(i) to enumerate all keys.

example.ts
const keys = await smartAccount.listKeys(
"https://ethereum-sepolia-rpc.publicnode.com",
);

for (const key of keys) {
const keyHash = Calibur7702Account.getKeyHash(key);
const settings = await smartAccount.getKeySettings(nodeRpc, keyHash);
console.log("Key:", keyHash.slice(0, 18) + "...");
console.log(" Type:", key.keyType);
console.log(" Admin:", settings.isAdmin);
}

Source code

listKeys

isDelegated

Checks if the EOA is delegated to this account's Calibur singleton. Returns true only when delegated to the account's delegateeAddress, false if not delegated or delegated to a different singleton.

example.ts
const isDelegated = await smartAccount.isDelegated(
"https://ethereum-sepolia-rpc.publicnode.com"
);

if (isDelegated) {
console.log("EOA is delegated to the Calibur singleton");
} else {
console.log("EOA is not delegated. Include eip7702Auth in createUserOperation.");
}

Source code

isDelegated

getNonce

Gets the account nonce from the EntryPoint. Supports parallel nonce channels via the optional sequenceKey parameter.

example.ts
// Default nonce (sequence key = 0)
const nonce = await smartAccount.getNonce(
"https://ethereum-sepolia-rpc.publicnode.com"
);
console.log("Nonce:", nonce);

// Parallel nonce channel
const parallelNonce = await smartAccount.getNonce(
"https://ethereum-sepolia-rpc.publicnode.com",
1 // sequence key
);

Source code

getNonce

Utility Methods

createAccountCallData

Static method. Encodes calldata for executeUserOp with BatchedCall format. All transactions (even single ones) go through the same BatchedCall path.

example.ts
import { Calibur7702Account } from "abstractionkit";

const transactions = [
{ to: "0x...", value: 0n, data: "0x..." },
{ to: "0x...", value: 1000000000000000000n, data: "0x" },
];

const callData = Calibur7702Account.createAccountCallData(transactions);

// With revertOnFailure disabled (individual calls can fail without reverting the batch)
const callDataNoRevert = Calibur7702Account.createAccountCallData(transactions, false);

Source code

createAccountCallData

wrapSignature

Static method. Wraps a raw ECDSA signature in Calibur's signature format: abi.encode(bytes32 keyHash, bytes signature, bytes hookData). Use this when signing externally (e.g., with viem, hardware wallet, MPC) to avoid manually ABI-encoding the wrapped signature.

example.ts
import { Calibur7702Account } from "abstractionkit";

// Root key hash (bytes32 zero) for the EOA's own key
const ROOT_KEY_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000";

const rawEcdsaSignature = "0x..."; // 65-byte ECDSA signature

// Wrap for root key
const wrappedSignature = Calibur7702Account.wrapSignature(
ROOT_KEY_HASH,
rawEcdsaSignature,
);

// Wrap for a secondary key with hook data
const wrappedWithHook = Calibur7702Account.wrapSignature(
"0x...secondary-key-hash",
rawEcdsaSignature,
"0x...hook-data",
);

Source code

wrapSignature

createDummyWebAuthnSignature

Static method. Creates a dummy WebAuthn signature for gas estimation when signing with a passkey. The key hash must correspond to an actually registered key on the account, otherwise the contract's validateUserOp will revert.

example.ts
import { Calibur7702Account } from "abstractionkit";

const keyHash = Calibur7702Account.getKeyHash(webAuthnKey);

const dummySignature = Calibur7702Account.createDummyWebAuthnSignature(keyHash);

// Pass as override during UserOperation creation for accurate gas estimation
const userOperation = await smartAccount.createUserOperation(
transactions,
nodeRpc,
bundlerRpc,
{ dummySignature },
);

Source code

createDummyWebAuthnSignature

getUserOperationHash

Computes the UserOperation hash for this account's EntryPoint. Convenience wrapper that automatically uses this account's EntryPoint address.

example.ts
const userOpHash = smartAccount.getUserOperationHash(
userOperation,
11155111n // chain ID
);

console.log("UserOperation hash:", userOpHash);
// Use this hash as the challenge for WebAuthn assertions

Source code

getUserOperationHash

prependTokenPaymasterApproveToCallData

Prepends a token approve call to existing calldata for a token paymaster. Decodes the existing BatchedCall, prepends an ERC-20 approve transaction, and re-encodes.

example.ts
const callDataWithApproval = smartAccount.prependTokenPaymasterApproveToCallData(
userOperation.callData,
"0xa0b86a33e6b3e96bb24b8e4b28e80e0fb3a4f4b6", // USDC token address
"0x...", // paymaster address
1000000n // approve amount (1 USDC)
);

userOperation.callData = callDataWithApproval;

Source code

prependTokenPaymasterApproveToCallData

createInvalidateNonceMetaTransaction

Static method. Creates a meta-transaction to invalidate nonces up to a given value. All nonces below the specified value become invalid, preventing replay of previously signed but unsubmitted UserOperations.

example.ts
import { Calibur7702Account } from "abstractionkit";

// Invalidate all nonces below 100
const invalidateTx = Calibur7702Account.createInvalidateNonceMetaTransaction(100n);

const userOperation = await smartAccount.createUserOperation(
[invalidateTx],
nodeRpc,
bundlerRpc,
);

Source code

createInvalidateNonceMetaTransaction