When you're building modern dApps, user onboarding and ongoing interaction are critical. Let's be honest, the traditional Web3 experience – juggling seed phrases, managing gas tokens – can scare off even enthusiastic users. That's where passwordless wallets come in, and you're about to learn how to build one that's both secure and refreshingly simple for your users.
This guide, inspired by Openfort's slick demonstration of EIP-7702 with ERC-4337, will walk you through creating a wallet experience where users interact with the blockchain using familiar WebAuthn credentials (like Passkeys). Think of it: no more "write this down and keep it safe" lectures! But before you jump into the code and start wiring things up, it’s essential to grasp the core components. Getting these concepts clear from the outset will make your development journey smoother and your final product much more robust.
If you want to check the code directly here is the repo.
Key Concepts & Glossary: The Tech You Need to Know
Before you start building, let's break down the essential tech powering this passwordless revolution. Understanding these terms isn't just academic; it's about knowing the tools that will let you deliver a top-tier user experience.
- Externally Owned Account (EOA): This is your starting block – the standard Ethereum account controlled by a private key. While it's the most common account type, we're about to give it some serious upgrades.
- EIP-7702: Think of this as the EOA "empowerment" proposal. It allows an EOA to temporarily grant specific permissions to a smart contract. This is key because it lets your EOA gain smart contract-like abilities without the immediate overhead of deploying a full smart contract wallet for every user.
- Delegation Contract: This is the trusted smart contract your EOA (via EIP-7702) authorizes to act on its behalf. It’s the intermediary that will execute actions based on the EOA’s approved instructions, enabling more complex operations.
- ERC-4337 (Account Abstraction): This is the standard that brings true "Account Abstraction" to Ethereum without needing deep protocol changes. For you, this means unlocking powerful features like gas sponsorship and improved transaction flows, making your dApp far more accessible.
- UserOperation (UserOp): With ERC-4337, users (or their accounts) don't just send plain old transactions. Instead, they submit
UserOperation
objects to a special, alternative mempool. This is how we tap into the advanced features of Account Abstraction. - Bundler: These are the workhorses of the ERC-4337 ecosystem. Bundlers pick up UserOps, package them efficiently into a single on-chain transaction, and get them executed. They handle the complexity so your users don't have to.
- WebAuthn / Passkeys: This is the heart of the "passwordless" experience. A web standard that lets users authenticate using biometrics (Face ID, Touch ID) or physical security keys. For your users, this means maximum security with minimal friction – a huge win.
- RIP-7212 P256 Precompile: For WebAuthn to be useful on-chain, signatures need to be verifiable. This precompile makes verifying the P256 signatures (common in WebAuthn) efficient and standard on Ethereum, securely linking Passkeys to blockchain accounts.
- Paymaster: An ERC-4337 smart contract that can pay gas fees on behalf of your users. This enables Sponsored Transactions, a game-changer for onboarding.
- Session Keys: To avoid "signature fatigue" (users constantly having to approve transactions), you can implement session keys. These are temporary, limited-permission keys that can be authorized for a certain number of actions or time, allowing for smoother, uninterrupted interactions within your dApp.
- Batch Transactions: Another win for user experience, enabled by Account Abstraction. This allows multiple distinct operations (e.g., approve a token and then swap it) to be combined and executed as a single, atomic transaction.
- IndexedDB: A browser-based database. In our context, it's a practical example of where you might securely store non-extractable session keys locally in the user's browser, keeping them handy but safe.
How It All Clicks: Your Passwordless Architecture Blueprint
So, how do these pieces – EOAs, EIP-7702, ERC-4337, WebAuthn – actually combine to deliver that seamless passwordless flow? It's not just about individual components; it's their smart integration:
Traditional wallets expose private keys to the application layer, creating multiple attack vectors. Traditional wallets expose private keys to the application layer, creating multiple attack vectors. Then, using EIP-7702, you'll empower this EOA by allowing it to designate a Delegation Contract. This contract becomes a trusted agent, ready to execute operations based on your EOA’s authority.
Next, you'll bring in WebAuthn (Passkeys) for authentication. Users will use their familiar device biometrics or security keys, and thanks to the RIP-7212 P256 Precompile, these credentials can securely control the EOA via the Delegation Contract.
The ERC-4337 standard then kicks things into high gear. User intentions become UserOperations, processed by Bundlers. This is your gateway to features that truly differentiate your dApp:
- Sponsored Transactions (via Paymasters): Onboard users without them ever touching gas.
- Session Keys: Let users perform multiple actions within a session without constant signature prompts.
- Batch Transactions: Simplify complex multi-step processes into single clicks.
By thoughtfully combining these technologies, you’re not just building a wallet; you’re crafting a user experience that’s years ahead of the curve. You're setting up a secure, trustworthy, and incredibly intuitive way for users to engage with the decentralized web.
With these foundational concepts in place, you’re well-prepared to tackle the code and bring your passwordless wallet to life. Let's get building!
Setting Up WebAuthn Authentication
Creating Your First Passkey
The journey begins by creating a WebAuthn credential that will serve as the primary authentication method for your smart account. This credential is tied to your device and protected by biometric authentication or a PIN.
_17import { Bytes, WebAuthnP256 } from "ox";_17_17// Create a WebAuthn credential using the account address as the user ID_17const credential = await WebAuthnP256.createCredential({_17 authenticatorSelection: {_17 requireResidentKey: false,_17 residentKey: "preferred", // Store credential on device when possible_17 userVerification: "required", // Require biometric/PIN verification_17 },_17 user: {_17 id: Bytes.from(account.address),_17 name: `${account.address.slice(0, 6)}...${account.address.slice(-4)}`,_17 },_17});_17_17// Store the credential ID for future authentication_17const credentialId = credential.id;
This process prompts the user to authenticate (via Face ID, Touch ID, Windows Hello, etc.) and creates a unique key pair bound to their device. The private key remains in secure hardware, while the public key is returned for smart account initialization.
Initializing the Smart Account
With the WebAuthn credential created, we can now initialize a smart account that recognizes this passkey as its primary signer:
_30// Prepare the initialization call data_30const callData = encodeFunctionData({_30 abi: accountABI,_30 functionName: "initialize",_30 args: [_30 { _30 pubKey: { _30 x: toHex(x), // The WebAuthn public key x coordinate_30 y: toHex(y), // The WebAuthn public key y coordinate_30 },_30 eoaAddress: zeroAddress, // address(0)_30 keyType: KEY_TYPE_WEBAUTHN, // WEBAUTHN key type identifier_30 },_30 spendTokenInfo, // Token spending configuration_30 [ // Function selectors this key can call_30 '0xa9059cbb', // transfer(address,uint256)_30 '0x40c10f19' // mint(address,uint256)_30 ],_30 messageHash, // Verification message hash_30 signatureInit, // Initial signature for verification_30 validUntil, // Key expiration timestamp_30 nonce + 1n, // Account nonce_30 ],_30});_30_30// Create the user operation with EIP-7702 authorization_30const userOperation = await smartAccountClient.prepareUserOperation({_30 callData,_30 authorization: signedAuthorization, // EIP-7702 authorization signature_30});
The EIP-7702 authorization allows an EOA to temporarily act as a smart contract, enabling the initialization process. Once this UserOperation is executed, your smart account is ready for WebAuthn-based transactions.
Executing Transactions with WebAuthn
Preparing the Transaction
Let's demonstrate by minting some ERC-20 tokens. The process involves preparing a UserOperation with a placeholder signature, then replacing it with the actual WebAuthn signature:
_20// Encode the mint function call_20const data = encodeFunctionData({_20 abi: erc20ABI,_20 functionName: "mint",_20 args: [_20 smartAccountClient.account.address,_20 parseEther("10"), // Mint 10 tokens_20 ],_20});_20_20// Prepare UserOperation with stub signature_20const userOperation = await smartAccountClient.prepareUserOperation({_20 calls: [_20 {_20 to: erc20Address,_20 data,_20 },_20 ],_20 signature: webAuthnStubSignature, // Placeholder signature_20});
Signing with WebAuthn
Now we generate the actual signature using the WebAuthn credential:
_16// Get the UserOperation hash that needs to be signed_16const userOperationHash = getUserOperationHash(userOperation);_16_16// Sign with WebAuthn - this triggers biometric authentication_16const webauthnData = await WebAuthnP256.sign({_16 challenge: userOperationHash,_16 credentialId, // The credential we created earlier_16 rpId: window.location.hostname, // Relying party identifier_16 userVerification: "required", // Require user verification_16});_16_16// Replace stub signature with the actual WebAuthn signature_16userOperation.signature = encodeWebAuthnSignature(webauthnData);_16_16// Send to bundler for execution_16await bundlerClient.sendUserOperation(userOperation);
The user experiences a familiar biometric prompt (Face ID, fingerprint, etc.), and once authenticated, the transaction is signed and submitted.
Hint: The stub signature has to be different for P256 and WebAuthn. The actual webauthn signature is significantly different from the P256 signature, so the stub signature has to be different as well.
Implementing Session Keys for Seamless UX
While WebAuthn provides excellent security, requiring biometric authentication for every transaction can impact user experience. Session keys solve this by creating temporary, limited-privilege keys for routine operations.
Creating a Session Key
Session keys are also non-extractable P256 keys, but they're created using the WebCrypto API and stored locally:
_10import { WebCryptoP256 } from "ox";_10_10// Generate a new P256 key pair for the session_10const keyPair = await WebCryptoP256.createKeyPair();_10const publicKey = await WebCryptoP256.getPublicKey({ _10 publicKey: keyPair.publicKey _10});_10_10// Store the key pair securely (e.g., in IndexedDB)_10await storeSessionKey(keyPair);
Registering the Session Key
The session key must be registered with the smart account, defining its permissions and validity:
_39const callData = encodeFunctionData({_39 abi: accountABI,_39 functionName: 'registerSessionKey',_39 args: [_39 {_39 pubKey: {_39 x: toHex(publicKey.x), // The P256 public key x coordinate_39 y: toHex(publicKey.y), // The P256 public key y coordinate_39 },_39 eoaAddress: zeroAddress, // address(0)_39 keyType: KEY_TYPE_P256, // P256 key type identifier_39 },_39 validUntil, // Session expiration_39 0, // Nonce_39 limits, // Spending/call limits_39 true, // Authorized flag_39 erc20Address, // Contract this key can interact with_39 spendTokenInfo, // Token spending configuration_39 [ // Function selectors this key can call_39 '0xa9059cbb', // transfer(address,uint256)_39 '0x40c10f19' // mint(address,uint256)_39 ],_39 ethLimit // ETH spending limit_39 ]_39});_39_39// This registration requires WebAuthn approval_39const userOperation = await smartAccountClient.prepareUserOperation({_39 callData,_39 signature: webAuthnStubSignature,_39});_39_39// Sign with WebAuthn (one-time approval for session key)_39const webauthnData = await WebAuthnP256.sign({_39 challenge: getUserOperationHash(userOperation),_39 credentialId,_39 rpId: window.location.hostname,_39 userVerification: "required",_39});
Hint:
Webauthn rpId
is crucial. It binds credentials to a specific domain for strong phishing protection. If you don't provide it, the browser will throw an error.
Using Session Keys for Transactions
Once registered, session keys can sign transactions without requiring biometric authentication:
_26// Prepare the transaction as before_26const userOperation = await smartAccountClient.prepareUserOperation({_26 calls: [_26 {_26 to: erc20Address,_26 data: encodeFunctionData({_26 abi: erc20ABI,_26 functionName: "mint",_26 args: [smartAccountClient.account.address, parseEther("10")],_26 }),_26 },_26 ],_26 signature: P256StubSignature, // Different stub for P256 keys_26});_26_26// Sign with the session key (no user interaction required)_26const { r, s } = await WebCryptoP256.sign({_26 privateKey: sessionKey.privateKey,_26 payload: getUserOperationHash(userOperation),_26});_26_26// Format and attach the signature_26userOperation.signature = encodeP256Signature({ r, s });_26_26// Submit the transaction_26await bundlerClient.sendUserOperation(userOperation);
Conclusion
WebAuthn with P256 signatures represents a significant leap forward in Web3 user experience and security. By eliminating private key exposure and leveraging familiar authentication methods, we can build smart accounts that are both more secure and more user-friendly than traditional wallets.