Interacting with ERC-4337 Contracts in TypeScript and Viem V2
A comprehensive guide to sending User Operations and monitoring contract events
Highlights
- Send a User Operation: Use permissionless.js with Viem V2 to create and dispatch a User Operation that calls a contract.
- Event Listening: Use Viem’s event watching capabilities (or alternative libraries) to subscribe to contract events like those from the EntryPoint or Paymaster.
- Step-by-Step Implementation: Detailed instructions for setting up clients, preparing transactions, and handling events, complete with code examples.
Overview
ERC-4337 introduces a new way to manage transactions with smart contract-based accounts using User Operations rather than conventional externally-owned accounts (EOAs). This design allows you to abstract away certain complexities like gas payments and signature management by leveraging bundlers, paymasters, and modular client libraries. In a TypeScript web application that uses Viem V2 and permissionless.js, you can integrate ERC-4337 functionalities for submitting transaction requests and listening to events emitted by contracts such as the EntryPoint and Paymaster.
Sending a User Operation
The process of sending a User Operation includes several key stages: setting up the environment and clients, creating the smart account, preparing the User Operation object, and finally dispatching it to the blockchain. Below are the steps and code examples which walk you through the process:
1. Environment Setup
Installing Dependencies
Before beginning, ensure that you have installed the necessary npm packages: permissionless and viem. Run the following command in your project directory:
# Install the required packages using npm
npm install permissionless viem
2. Configuring Clients and Creating a Smart Account
You’ll need to set up both a paymaster client (if you wish to offer gas sponsorship) and a public client for blockchain interactions. The following code example shows how you can accomplish this:
// Import necessary functions and types from permissionless and viem
import { createSmartAccountClient, toSimpleSmartAccount } from "permissionless";
import { createPaymasterClient } from "permissionless";
import { getPublicClient, http, sepolia } from "viem";
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
// Configure your API key for a bundler or gas service, such as Pimlico
const pimlicoApiKey = "YOUR_PIMLICO_API_KEY";
// Create a paymaster client for offering gas sponsorship (optional)
const paymaster = createPaymasterClient({
transport: http(`https://api.pimlico.io/v2/sepolia/rpc?apikey=${pimlicoApiKey}`)
});
// Set up the public client for interacting with the blockchain
const publicClient = getPublicClient({
transport: http("YOUR_RPC_URL"),
chain: sepolia
});
// Generate an account from a private key; for production, use your secure key management
const owner = privateKeyToAccount(generatePrivateKey());
// Create a simple smart account instance
const account = toSimpleSmartAccount({
client: publicClient,
owner: owner
});
// Create the smart account client considering your account, chain and transport mechanism
const smartAccountClient = createSmartAccountClient({
account: account,
chain: sepolia,
transport: http(`https://api.pimlico.io/v2/sepolia/rpc?apikey=${pimlicoApiKey}`)
});
This setup involves creating two different clients: one for paying the gas if a paymaster is used, and one for the smart account’s blockchain interactions. By combining these elements, you can efficiently manage your User Operation flows.
3. Creating and Sending the User Operation
A User Operation is a structured object that must include the sender’s information, gas limits, and method call data. This ensures that the transaction is constructed according to ERC-4337 requirements. Below is a sample function to send a User Operation:
async function sendUserOperation() {
// Define an example transaction: calling a smart contract method (assuming call data is already encoded)
const transactionData = {
to: "0xRecipientAddress", // Target contract address
data: "0xEncodedCallData", // Encoded function call data
value: 0n // Optional Ether value (in bigint)
};
// Send the User Operation using the smart account client
const txHash = await smartAccountClient.sendTransaction(transactionData);
console.log("User Operation sent successfully, transaction hash:", txHash);
// Wait for the transaction to be confirmed before proceeding further
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
console.log("Transaction confirmed with receipt:", receipt);
}
// Invoke the function to send the User Operation
sendUserOperation().catch((error) => console.error("Error sending User Operation:", error));
In this snippet, the User Operation essentially represents a transaction where a smart contract function is invoked. The use of sendTransaction abstracts away some of the intricacies related to nonce management and signature verification by letting the library handle them in the background.
Listening to Contract Events
Monitoring smart contract events is essential for building responsive dApps that react to on-chain changes. ERC-4337 contracts, such as the EntryPoint and Paymaster, emit events that you can listen to in order to handle successful operations, errors, or state changes. Below, we detail how to subscribe to these event streams.
1. Setting Up a Public Client for Event Listening
Before being able to listen to events, you need to set up a public client. This client connects to the blockchain and provides functions to monitor live event logs.
import { createPublicClient, http, sepolia } from "viem";
// Create a public client which connects to your chosen RPC endpoint
const publicEventClient = createPublicClient({
chain: sepolia,
transport: http("https://rpc.sepolia.org")
});
This client is used primarily for event listening and performs queries on the blockchain directly, independent of your smart account’s state.
2. Listening to EntryPoint Events
User Operation Events
The EntryPoint contract typically emits a UserOperationEvent when a User Operation is processed. You can subscribe to these events with watchContractEvent from Viem. Below is an example:
import { watchContractEvent, parseAbi } from "viem";
// Define the ABI for the EntryPoint contract. Ensure this includes the event you want to listen to.
const entryPointAbi = parseAbi([
"event UserOperationEvent(bytes32 indexed userOpHash, address indexed sender, uint256 nonce, bool success, uint256 actualGasCost)"
]);
// Define the contract address of your EntryPoint
const entryPointAddress = "0xYourEntryPointContractAddress";
// Subscribe to the UserOperationEvent
const unwatchEntryPoint = publicEventClient.watchContractEvent({
address: entryPointAddress,
abi: entryPointAbi,
eventName: "UserOperationEvent",
onLogs: (logs) => {
console.log("Received UserOperationEvent logs:", logs);
// Process the received logs as needed:
// logs can include userOpHash, sender, nonce, success status, and gas metrics.
}
});
// To stop listening, simply call the unwatchEntryPoint() when needed.
// unwatchEntryPoint();
This code sets up a listener on the EntryPoint contract, filtering for events named UserOperationEvent. The onLogs callback allows you to process or display the event details in your web application.
3. Listening to Paymaster Events
Paymaster-Specific Events
Similar to the EntryPoint, paymaster contracts can emit events to signal changes such as gas sponsorship details. Use the same event subscription pattern for paymaster contracts:
import { parseAbi, watchContractEvent as watchPaymasterEvent } from "viem";
// Define your paymaster contract’s ABI including the event you want to monitor
const paymasterAbi = parseAbi([
"event PaymasterUserOperationSponsored(address indexed paymaster, bytes32 indexed userOpHash)"
]);
// Set the address of the paymaster contract
const paymasterAddress = "0xYourPaymasterContractAddress";
// Subscribe to the paymaster event
const unwatchPaymaster = publicEventClient.watchContractEvent({
address: paymasterAddress,
abi: paymasterAbi,
eventName: "PaymasterUserOperationSponsored",
onLogs: (logs) => {
console.log("Received Paymaster event logs:", logs);
// Process event logs based on your application’s needs.
}
});
// To stop listening for paymaster events when no longer needed:
// unwatchPaymaster();
This subscription allows you to receive real-time notifications when the paymaster takes an action or sponsors a User Operation. Adjust the ABI and event name as per your specific smart contract’s definition.
4. Retrieving Past Events
If you need to look up historical events rather than subscribing in real time, the getContractEvents function can be leveraged. This function accepts additional filtering parameters such as block range; here's an example:
async function fetchPastUserOperationEvents() {
try {
const pastLogs = await publicEventClient.getContractEvents({
address: entryPointAddress,
abi: entryPointAbi,
eventName: "UserOperationEvent",
// Optionally search events in a block range:
fromBlock: 0n,
toBlock: "latest"
});
console.log("Fetched past UserOperation events:", pastLogs);
} catch (error) {
console.error("Error fetching past events:", error);
}
}
// Call the function to fetch historical event data
fetchPastUserOperationEvents();
This function is essential for dApps that need to display historical transaction data or analytics on past operations.
Integrating the Functionality in a TypeScript Application
To consolidate these functionalities into your application, consider grouping the code into modules or hooks if you are using a framework like React. This modular approach not only helps with code organization but also makes it easier to reuse the logic for event subscriptions and User Operation submissions across various components of your application.
Example Table of Key Parameters
| Parameter | Description | Example |
|---|---|---|
| sender | The address of the smart account sending the transaction. | 0xYourSmartAccountAddress |
| nonce | Current transaction nonce for the smart account, ensuring operation uniqueness. | 0 |
| callData | Encoded function call data for the target contract. | 0xEncodedCallData |
| Gas Limits | Includes callGasLimit, verificationGasLimit, preVerificationGas for managing gas cost. | 2000000, 1000000, 21000 |
| Fee Settings | Max fee per gas and max priority fee settings for transaction execution. | 1000000000 |
| Signature | Cryptographic signature authorizing the operation. (Calculated securely in production) | 0xYourSignature |
This table summarizes vital parameters required when forming a User Operation, ensuring that your transactions align with the ERC-4337 standards.
Security and Best Practices
While integrating ERC-4337 functionality improves the flexibility and user experience of blockchain transactions, adhering to security best practices is paramount:
Secure Key Management
Never hardcode private keys in your source code. Use secure key vaults or environment variables and ensure that any generated keys are handled securely.
Error Handling and Monitoring
Implement robust error handling around transaction submissions and event listening to handle cases where the blockchain might temporarily be unreachable or where operations may fail due to insufficient gas or nonce mismatches. Use try/catch blocks and monitor for specific errors in logs to alert users or trigger retries.
Gas Optimization Strategies
ERC-4337 offers methods for batching operations and utilizing sponsored transactions or paymasters to reduce user cost. Regularly review gas configurations and consider dynamic gas pricing strategies based on current network conditions.
Conclusion and Final Thoughts
In this guide, we have explored the key steps to integrate ERC-4337 smart contract interactions within a TypeScript web application using Viem V2. We covered setting up the environment, configuring a smart account, preparing and sending a User Operation, and finally listening to events emitted by smart contracts such as the EntryPoint and Paymaster.
The modular setup, employing libraries like permissionless.js, simplifies the task of dealing with raw blockchain interactions, allowing you to focus on your application’s business logic and user experience. By following robust practices for error handling and security, your dApp can seamlessly integrate advanced transaction models while maintaining a high level of trust and reliability among users.
Adapting these strategies will not only improve your application's performance but also allow for future scalability as new features or optimizations in the ERC-4337 ecosystem emerge. Always test your implementation thoroughly on test networks, and consider incorporating additional logging or monitoring tools to track blockchain events and transactions in real time.
References
- Pimlico Permissionless Documentation - Pimlico
- Viem Docs on watchContractEvent - Viem
- ERC-4337 Official Website - ERC-4337
- Permissionless.js GitHub Repository - GitHub
Recommended Related Queries
Last updated February 24, 2025