Transaction Management
The SDK provides simplified methods for monitoring and managing blockchain transactions and user operations. These methods help you track transaction status and wait for confirmations with built-in error handling.
Methods Overview
waitForTransaction()- Wait for regular transaction confirmationwaitForUserOperation()- Wait for user operation completion
waitForTransaction()
Waits for a regular blockchain transaction to be confirmed and included in a block.
Method Signature
async waitForTransaction(transactionHash: Hash): Promise<TransactionReceipt>Parameters
transactionHash: Transaction hash to wait for (as returned by external transactions)
Return Value
Returns a TransactionReceipt object containing transaction details.
Usage Example
import { ClientRepository } from "@humanwallet/sdk"
// For external transactions (not from HumanWallet)
const externalTxHash = "0x..." // From external source
try {
const receipt = await client.waitForTransaction(externalTxHash)
console.log("Transaction confirmed!")
console.log("Block number:", receipt.blockNumber)
console.log("Gas used:", receipt.gasUsed.toString())
console.log("Status:", receipt.status) // 'success' or 'reverted'
} catch (error) {
console.error("Transaction failed or timed out:", error)
}When to Use
This method is primarily for monitoring external transactions that weren't created through HumanWallet's writeContract or writeContracts methods. For HumanWallet transactions, use waitForUserOperation() instead.
waitForUserOperation()
Waits for a HumanWallet user operation (gasless transaction) to be completed. This is the primary method for monitoring transactions created through the SDK.
Method Signature
async waitForUserOperation(userOperationHash: UserOperationHash): Promise<UserOperationReceipt>Parameters
userOperationHash: User operation hash returned bywriteContract()orwriteContracts()
Return Value
Returns a UserOperationReceipt object containing:
transactionHash: The actual on-chain transaction hashblockNumber: Block number where it was includedsuccess: Whether the operation succeededgasUsed: Gas consumed by the operationlogs: Event logs emitted
Usage Example
// Execute a transaction and wait for completion
try {
// Execute transaction
const userOpHash = await client.writeContract({
address: "0x...",
abi: contractAbi,
functionName: "mint",
args: [client.address, 1],
})
console.log("Transaction submitted:", userOpHash)
// Wait for completion
const receipt = await client.waitForUserOperation(userOpHash)
if (receipt.success) {
console.log("✅ Transaction successful!")
console.log("On-chain hash:", receipt.transactionHash)
console.log("Block number:", receipt.blockNumber)
console.log("Gas used:", receipt.gasUsed.toString())
} else {
console.log("❌ Transaction failed")
}
} catch (error) {
console.error("Transaction error:", error)
}Complete Transaction Workflows
Simple Transaction Flow
class TransactionService {
constructor(private client: ClientRepository) {}
async executeTransaction(contractParams: WriteContractParameters): Promise<{
success: boolean
transactionHash?: string
error?: string
}> {
try {
// Ensure user is authenticated
if (!this.client.isConnected) {
throw new Error("User not authenticated")
}
console.log("Submitting transaction...")
const userOpHash = await this.client.writeContract(contractParams)
console.log("Waiting for confirmation...")
const receipt = await this.client.waitForUserOperation(userOpHash)
if (receipt.success) {
return {
success: true,
transactionHash: receipt.transactionHash,
}
} else {
return {
success: false,
error: "Transaction reverted",
}
}
} catch (error) {
return {
success: false,
error: (error as Error).message,
}
}
}
}Batch Transaction Monitoring
class BatchTransactionService {
constructor(private client: ClientRepository) {}
async executeBatch(operations: WriteContractParameters[]): Promise<{
success: boolean
transactionHash?: string
individualResults?: any[]
error?: string
}> {
try {
console.log(`Executing batch of ${operations.length} operations...`)
const batchHash = await this.client.writeContracts(operations)
console.log("Batch submitted:", batchHash)
const receipt = await this.client.waitForUserOperation(batchHash)
if (receipt.success) {
// Parse logs to get individual operation results
const individualResults = this.parseOperationLogs(receipt.logs)
return {
success: true,
transactionHash: receipt.transactionHash,
individualResults,
}
} else {
return {
success: false,
error: "One or more operations in the batch failed",
}
}
} catch (error) {
return {
success: false,
error: (error as Error).message,
}
}
}
private parseOperationLogs(logs: any[]): any[] {
// Parse logs to extract results from individual operations
return logs.map((log, index) => ({
operationIndex: index,
eventData: log,
success: true, // Determine based on log data
}))
}
}Transaction with Progress Tracking
interface TransactionProgress {
status: "pending" | "confirmed" | "failed"
userOpHash?: string
transactionHash?: string
receipt?: any
error?: string
}
class ProgressTrackingService {
constructor(private client: ClientRepository) {}
async executeWithProgress(
contractParams: WriteContractParameters,
onProgress?: (progress: TransactionProgress) => void,
): Promise<TransactionProgress> {
try {
// Initial state
onProgress?.({ status: "pending" })
// Submit transaction
const userOpHash = await this.client.writeContract(contractParams)
onProgress?.({
status: "pending",
userOpHash,
})
// Wait for completion
const receipt = await this.client.waitForUserOperation(userOpHash)
const finalProgress: TransactionProgress = {
status: receipt.success ? "confirmed" : "failed",
userOpHash,
transactionHash: receipt.transactionHash,
receipt,
}
onProgress?.(finalProgress)
return finalProgress
} catch (error) {
const errorProgress: TransactionProgress = {
status: "failed",
error: (error as Error).message,
}
onProgress?.(errorProgress)
return errorProgress
}
}
}React Integration
Transaction Hook
import { useState, useCallback } from "react"
import { ClientRepository, WriteContractParameters } from "@humanwallet/sdk"
interface TransactionState {
isLoading: boolean
userOpHash?: string
transactionHash?: string
success?: boolean
error?: string
}
export function useTransaction(client: ClientRepository) {
const [state, setState] = useState<TransactionState>({ isLoading: false })
const executeTransaction = useCallback(
async (params: WriteContractParameters) => {
setState({ isLoading: true })
try {
// Submit transaction
const userOpHash = await client.writeContract(params)
setState((prev) => ({ ...prev, userOpHash }))
// Wait for completion
const receipt = await client.waitForUserOperation(userOpHash)
setState({
isLoading: false,
userOpHash,
transactionHash: receipt.transactionHash,
success: receipt.success,
error: receipt.success ? undefined : "Transaction reverted",
})
return receipt
} catch (error) {
setState({
isLoading: false,
error: (error as Error).message,
success: false,
})
throw error
}
},
[client],
)
const executeBatch = useCallback(
async (operations: WriteContractParameters[]) => {
setState({ isLoading: true })
try {
const batchHash = await client.writeContracts(operations)
setState((prev) => ({ ...prev, userOpHash: batchHash }))
const receipt = await client.waitForUserOperation(batchHash)
setState({
isLoading: false,
userOpHash: batchHash,
transactionHash: receipt.transactionHash,
success: receipt.success,
error: receipt.success ? undefined : "Batch transaction failed",
})
return receipt
} catch (error) {
setState({
isLoading: false,
error: (error as Error).message,
success: false,
})
throw error
}
},
[client],
)
const reset = useCallback(() => {
setState({ isLoading: false })
}, [])
return {
...state,
executeTransaction,
executeBatch,
reset,
}
}Transaction Status Component
import React from 'react'
interface TransactionStatusProps {
isLoading: boolean
userOpHash?: string
transactionHash?: string
success?: boolean
error?: string
}
export function TransactionStatus({
isLoading,
userOpHash,
transactionHash,
success,
error
}: TransactionStatusProps) {
if (isLoading) {
return (
<div className="transaction-status pending">
<div className="spinner" />
<div>
<p>Transaction pending...</p>
{userOpHash && (
<p className="hash">User Op: {userOpHash.slice(0, 10)}...</p>
)}
</div>
</div>
)
}
if (success === true) {
return (
<div className="transaction-status success">
<span>✅</span>
<div>
<p>Transaction confirmed!</p>
{transactionHash && (
<p className="hash">
Hash: {transactionHash.slice(0, 10)}...
<a
href={`https://etherscan.io/tx/${transactionHash}`}
target="_blank"
rel="noopener noreferrer"
>
View on Etherscan
</a>
</p>
)}
</div>
</div>
)
}
if (success === false || error) {
return (
<div className="transaction-status error">
<span>❌</span>
<div>
<p>Transaction failed</p>
{error && <p className="error-message">{error}</p>}
</div>
</div>
)
}
return null
}Complete Transaction Flow Component
import React, { useState } from 'react'
import { useTransaction } from './useTransaction'
import { TransactionStatus } from './TransactionStatus'
import { ClientRepository } from '@humanwallet/sdk'
interface TokenTransferProps {
client: ClientRepository
tokenAddress: string
tokenAbi: any[]
}
export function TokenTransfer({ client, tokenAddress, tokenAbi }: TokenTransferProps) {
const [recipient, setRecipient] = useState('')
const [amount, setAmount] = useState('')
const transaction = useTransaction(client)
const handleTransfer = async (e: React.FormEvent) => {
e.preventDefault()
if (!recipient || !amount) {
alert('Please fill all fields')
return
}
try {
await transaction.executeTransaction({
address: tokenAddress,
abi: tokenAbi,
functionName: 'transfer',
args: [recipient, BigInt(amount)]
})
} catch (error) {
console.error('Transfer failed:', error)
}
}
return (
<div>
<h3>Token Transfer</h3>
<form onSubmit={handleTransfer}>
<div>
<label>Recipient:</label>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="0x..."
disabled={transaction.isLoading}
/>
</div>
<div>
<label>Amount:</label>
<input
type="text"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="1000000000000000000"
disabled={transaction.isLoading}
/>
</div>
<button
type="submit"
disabled={transaction.isLoading || !client.isConnected}
>
{transaction.isLoading ? 'Transferring...' : 'Transfer'}
</button>
</form>
<TransactionStatus
isLoading={transaction.isLoading}
userOpHash={transaction.userOpHash}
transactionHash={transaction.transactionHash}
success={transaction.success}
error={transaction.error}
/>
</div>
)
}Error Handling and Retries
Advanced Error Handler
export class TransactionErrorHandler {
static getErrorMessage(error: Error): string {
const message = error.message.toLowerCase()
if (message.includes("user operation failed")) {
return "Transaction execution failed on the blockchain"
}
if (message.includes("timeout")) {
return "Transaction timed out - it may still be processing"
}
if (message.includes("insufficient funds")) {
return "Insufficient balance for transaction"
}
if (message.includes("user rejected")) {
return "Transaction cancelled by user"
}
if (message.includes("network")) {
return "Network error - please try again"
}
return "Transaction failed"
}
static isRetryableError(error: Error): boolean {
const message = error.message.toLowerCase()
return message.includes("network") || message.includes("timeout") || message.includes("connection failed")
}
}Transaction with Retry Logic
class RobustTransactionService {
constructor(private client: ClientRepository) {}
async executeWithRetry(
contractParams: WriteContractParameters,
maxRetries: number = 3,
timeout: number = 60000,
): Promise<any> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`Transaction attempt ${attempt}/${maxRetries}`)
const userOpHash = await this.client.writeContract(contractParams)
// Wait with custom timeout
const receipt = await Promise.race([
this.client.waitForUserOperation(userOpHash),
new Promise((_, reject) => setTimeout(() => reject(new Error("Transaction timeout")), timeout)),
])
console.log(`Transaction successful on attempt ${attempt}`)
return receipt
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error)
if (attempt === maxRetries || !TransactionErrorHandler.isRetryableError(error as Error)) {
throw error
}
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000)
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
}
}Monitoring and Analytics
Transaction Analytics
class TransactionAnalytics {
private metrics: {
totalTransactions: number
successfulTransactions: number
failedTransactions: number
averageConfirmationTime: number
gasUsed: bigint
} = {
totalTransactions: 0,
successfulTransactions: 0,
failedTransactions: 0,
averageConfirmationTime: 0,
gasUsed: 0n,
}
async trackTransaction(contractParams: WriteContractParameters, client: ClientRepository): Promise<any> {
const startTime = Date.now()
this.metrics.totalTransactions++
try {
const userOpHash = await client.writeContract(contractParams)
const receipt = await client.waitForUserOperation(userOpHash)
const confirmationTime = Date.now() - startTime
if (receipt.success) {
this.metrics.successfulTransactions++
this.metrics.gasUsed += receipt.gasUsed
// Update average confirmation time
this.metrics.averageConfirmationTime =
(this.metrics.averageConfirmationTime * (this.metrics.successfulTransactions - 1) + confirmationTime) /
this.metrics.successfulTransactions
} else {
this.metrics.failedTransactions++
}
console.log("Transaction metrics:", this.getMetrics())
return receipt
} catch (error) {
this.metrics.failedTransactions++
throw error
}
}
getMetrics() {
return {
...this.metrics,
successRate: this.metrics.successfulTransactions / this.metrics.totalTransactions,
averageGasUsed:
this.metrics.successfulTransactions > 0
? this.metrics.gasUsed / BigInt(this.metrics.successfulTransactions)
: 0n,
}
}
}Best Practices
1. Always Wait for User Operations
// Good: Wait for completion
const userOpHash = await client.writeContract(params)
const receipt = await client.waitForUserOperation(userOpHash)
if (receipt.success) {
showSuccessMessage("Transaction completed!")
}2. Provide User Feedback
// Good: Show progress to user
console.log("Submitting transaction...")
const userOpHash = await client.writeContract(params)
console.log("Waiting for confirmation...")
const receipt = await client.waitForUserOperation(userOpHash)
console.log("Transaction completed!")3. Handle Errors Gracefully
try {
const receipt = await executeTransaction()
} catch (error) {
const userMessage = TransactionErrorHandler.getErrorMessage(error)
showUserError(userMessage)
// Log technical details for debugging
console.error("Transaction failed:", error)
}4. Use Appropriate Timeouts
// For simple transactions
const receipt = await client.waitForUserOperation(hash) // Default timeout
// For complex batch operations, consider longer timeouts if available
// (Note: SDK may not expose timeout options, but this is the pattern)