Skip to content

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()

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 by writeContract() or writeContracts()

Return Value

Returns a UserOperationReceipt object containing:

  • transactionHash: The actual on-chain transaction hash
  • blockNumber: Block number where it was included
  • success: Whether the operation succeeded
  • gasUsed: Gas consumed by the operation
  • logs: 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)