Skip to content

Contract Interactions

The SDK provides simplified methods for interacting with smart contracts, including reading contract state and executing transactions. All write operations are gasless through account abstraction.

Methods Overview

writeContract()

Executes a single contract function call as a gasless transaction.

Method Signature

async writeContract(args: WriteContractParameters): Promise<string>

Parameters

args: WriteContractParameters

  • address: Contract address (0x${string})
  • abi: Contract ABI array
  • functionName: Function name to call
  • args: Array of function arguments (optional)
  • value: ETH value to send (optional, defaults to 0n)

Return Value

Returns the user operation hash as a string.

Usage Examples

Token Transfer

import { ClientRepository } from "@humanwallet/sdk"
import { parseEther } from "viem"
 
// Ensure user is authenticated
if (!client.isConnected) {
  await client.login("alice")
}
 
// Execute token transfer
const hash = await client.writeContract({
  address: "0x...", // Token contract address
  abi: [
    {
      name: "transfer",
      type: "function",
      inputs: [
        { name: "to", type: "address" },
        { name: "amount", type: "uint256" },
      ],
      outputs: [{ name: "", type: "bool" }],
      stateMutability: "nonpayable",
    },
  ],
  functionName: "transfer",
  args: ["0x742d35Cc6634C0532925a3b8D52ECC5c5e9fA008", parseEther("1")], // 1 token
})
 
console.log("Transaction hash:", hash)
 
// Wait for transaction to complete
const receipt = await client.waitForUserOperation(hash)
console.log("Transaction completed:", receipt.success)

NFT Minting

const mintHash = await client.writeContract({
  address: "0x...", // NFT contract address
  abi: nftAbi,
  functionName: "mint",
  args: [client.address, 1], // mint to self, token ID 1
})
 
console.log("NFT mint transaction:", mintHash)

Contract with ETH Value

const donationHash = await client.writeContract({
  address: "0x...", // Donation contract
  abi: donationAbi,
  functionName: "donate",
  value: parseEther("0.1"), // Send 0.1 ETH
})
 
console.log("Donation transaction:", donationHash)

writeContracts()

Executes multiple contract function calls in a single batch transaction.

Method Signature

async writeContracts(contracts: WriteContractParameters[]): Promise<string>

Parameters

  • contracts: Array of contract call parameters

Return Value

Returns the batch user operation hash as a string.

Usage Examples

Token Approval and Swap

// Batch approve and swap operations
const batchHash = await client.writeContracts([
  {
    address: TOKEN_CONTRACT, // Token contract
    abi: erc20Abi,
    functionName: "approve",
    args: [DEX_CONTRACT, parseEther("1")],
  },
  {
    address: DEX_CONTRACT, // DEX contract
    abi: dexAbi,
    functionName: "swapExactTokensForTokens",
    args: [
      parseEther("1"), // amountIn
      parseEther("0.95"), // amountOutMin
      [TOKEN_A, TOKEN_B], // path
      client.address, // to
      Math.floor(Date.now() / 1000) + 600, // deadline (10 minutes)
    ],
  },
])
 
console.log("Batch transaction hash:", batchHash)
 
// Wait for batch completion
const receipt = await client.waitForUserOperation(batchHash)
if (receipt.success) {
  console.log("Token swap completed successfully!")
}

Multi-Contract Setup

// Setup multiple contracts in one transaction
const setupHash = await client.writeContracts([
  {
    address: REGISTRY_CONTRACT,
    abi: registryAbi,
    functionName: "register",
    args: ["alice"],
  },
  {
    address: SETTINGS_CONTRACT,
    abi: settingsAbi,
    functionName: "setPreferences",
    args: [true, 100, "dark"],
  },
  {
    address: TOKEN_CONTRACT,
    abi: erc20Abi,
    functionName: "approve",
    args: [SPENDING_CONTRACT, parseEther("1000")],
  },
])
 
console.log("Setup transaction hash:", setupHash)

Benefits of Batching

  • Atomic Operations: All transactions succeed or fail together
  • Gas Efficiency: Single user operation instead of multiple
  • Better UX: One signature for multiple actions
  • Reduced Latency: Faster than sequential transactions

readContract()

Reads data from a smart contract without executing a transaction.

Method Signature

async readContract(args: ReadContractParameters): Promise<any>

Parameters

args: ReadContractParameters

  • address: Contract address
  • abi: Contract ABI array
  • functionName: Function name to call
  • args: Array of function arguments (optional)

Usage Examples

Token Balance

const balance = await client.readContract({
  address: "0x...", // Token contract
  abi: [
    {
      name: "balanceOf",
      type: "function",
      inputs: [{ name: "account", type: "address" }],
      outputs: [{ name: "", type: "uint256" }],
      stateMutability: "view",
    },
  ],
  functionName: "balanceOf",
  args: [client.address],
})
 
console.log("Token balance:", balance.toString())

Contract State

// Read multiple contract properties
const [totalSupply, name, symbol, decimals] = await Promise.all([
  client.readContract({
    address: TOKEN_CONTRACT,
    abi: erc20Abi,
    functionName: "totalSupply",
  }),
  client.readContract({
    address: TOKEN_CONTRACT,
    abi: erc20Abi,
    functionName: "name",
  }),
  client.readContract({
    address: TOKEN_CONTRACT,
    abi: erc20Abi,
    functionName: "symbol",
  }),
  client.readContract({
    address: TOKEN_CONTRACT,
    abi: erc20Abi,
    functionName: "decimals",
  }),
])
 
console.log(`${name} (${symbol})`)
console.log(`Total Supply: ${totalSupply.toString()}`)
console.log(`Decimals: ${decimals}`)

Complex Data Structures

// Reading a struct from a staking contract
const userInfo = await client.readContract({
  address: STAKING_CONTRACT,
  abi: stakingAbi,
  functionName: "getUserInfo",
  args: [client.address],
})
 
// userInfo is typically returned as an array
const [stakedAmount, rewardDebt, lastStakeTime] = userInfo
 
console.log("Staking info:", {
  stakedAmount: stakedAmount.toString(),
  rewardDebt: rewardDebt.toString(),
  lastStakeTime: new Date(Number(lastStakeTime) * 1000),
})

signTypedData()

Signs structured data according to EIP-712 standard.

Method Signature

async signTypedData(args: SignTypedDataParameters): Promise<Signature>

Parameters

args: SignTypedDataParameters

  • domain: EIP-712 domain separator
  • types: Type definitions for the data
  • primaryType: The primary type being signed
  • message: The actual data to sign

Usage Example

// Sign a permit message for gasless token approval
const signature = await client.signTypedData({
  domain: {
    name: "MyToken",
    version: "1",
    chainId: 11155111,
    verifyingContract: TOKEN_CONTRACT,
  },
  types: {
    Permit: [
      { name: "owner", type: "address" },
      { name: "spender", type: "address" },
      { name: "value", type: "uint256" },
      { name: "nonce", type: "uint256" },
      { name: "deadline", type: "uint256" },
    ],
  },
  primaryType: "Permit",
  message: {
    owner: client.address!,
    spender: SPENDER_CONTRACT,
    value: parseEther("1000"),
    nonce: 0n,
    deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour
  },
})
 
console.log("Signature:", signature)
// Use signature for gasless permit transaction

High-Level Service Classes

Token Service

class TokenService {
  constructor(
    private client: ClientRepository,
    private tokenAddress: `0x${string}`,
    private tokenAbi: any[],
  ) {}
 
  async getBalance(address?: `0x${string}`): Promise<bigint> {
    const account = address || this.client.address
    if (!account) throw new Error("No address provided")
 
    return await this.client.readContract({
      address: this.tokenAddress,
      abi: this.tokenAbi,
      functionName: "balanceOf",
      args: [account],
    })
  }
 
  async getTokenInfo() {
    const [name, symbol, decimals, totalSupply] = await Promise.all([
      this.client.readContract({
        address: this.tokenAddress,
        abi: this.tokenAbi,
        functionName: "name",
      }),
      this.client.readContract({
        address: this.tokenAddress,
        abi: this.tokenAbi,
        functionName: "symbol",
      }),
      this.client.readContract({
        address: this.tokenAddress,
        abi: this.tokenAbi,
        functionName: "decimals",
      }),
      this.client.readContract({
        address: this.tokenAddress,
        abi: this.tokenAbi,
        functionName: "totalSupply",
      }),
    ])
 
    return { name, symbol, decimals, totalSupply }
  }
 
  async transfer(to: `0x${string}`, amount: bigint): Promise<string> {
    if (!this.client.isConnected) {
      throw new Error("User not authenticated")
    }
 
    return await this.client.writeContract({
      address: this.tokenAddress,
      abi: this.tokenAbi,
      functionName: "transfer",
      args: [to, amount],
    })
  }
 
  async approve(spender: `0x${string}`, amount: bigint): Promise<string> {
    if (!this.client.isConnected) {
      throw new Error("User not authenticated")
    }
 
    return await this.client.writeContract({
      address: this.tokenAddress,
      abi: this.tokenAbi,
      functionName: "approve",
      args: [spender, amount],
    })
  }
 
  async getAllowance(spender: `0x${string}`): Promise<bigint> {
    if (!this.client.address) {
      throw new Error("User not authenticated")
    }
 
    return await this.client.readContract({
      address: this.tokenAddress,
      abi: this.tokenAbi,
      functionName: "allowance",
      args: [this.client.address, spender],
    })
  }
}

DeFi Service

class DeFiService {
  constructor(
    private client: ClientRepository,
    private dexAddress: `0x${string}`,
    private dexAbi: any[],
  ) {}
 
  async swapTokens(
    tokenA: `0x${string}`,
    tokenB: `0x${string}`,
    amountIn: bigint,
    minAmountOut: bigint,
  ): Promise<string> {
    if (!this.client.isConnected) {
      throw new Error("User not authenticated")
    }
 
    // Check current allowance
    const tokenService = new TokenService(this.client, tokenA, erc20Abi)
    const currentAllowance = await tokenService.getAllowance(this.dexAddress)
 
    const operations: WriteContractParameters[] = []
 
    // Add approval if needed
    if (currentAllowance < amountIn) {
      operations.push({
        address: tokenA,
        abi: erc20Abi,
        functionName: "approve",
        args: [this.dexAddress, amountIn],
      })
    }
 
    // Add swap operation
    operations.push({
      address: this.dexAddress,
      abi: this.dexAbi,
      functionName: "swapExactTokensForTokens",
      args: [
        amountIn,
        minAmountOut,
        [tokenA, tokenB],
        this.client.address,
        BigInt(Math.floor(Date.now() / 1000) + 600), // 10 minute deadline
      ],
    })
 
    // Execute as batch if approval needed, otherwise single transaction
    if (operations.length > 1) {
      return await this.client.writeContracts(operations)
    } else {
      return await this.client.writeContract(operations[0])
    }
  }
 
  async addLiquidity(
    tokenA: `0x${string}`,
    tokenB: `0x${string}`,
    amountA: bigint,
    amountB: bigint,
    minAmountA: bigint,
    minAmountB: bigint,
  ): Promise<string> {
    // Approve both tokens and add liquidity in batch
    return await this.client.writeContracts([
      {
        address: tokenA,
        abi: erc20Abi,
        functionName: "approve",
        args: [this.dexAddress, amountA],
      },
      {
        address: tokenB,
        abi: erc20Abi,
        functionName: "approve",
        args: [this.dexAddress, amountB],
      },
      {
        address: this.dexAddress,
        abi: this.dexAbi,
        functionName: "addLiquidity",
        args: [
          tokenA,
          tokenB,
          amountA,
          amountB,
          minAmountA,
          minAmountB,
          this.client.address,
          BigInt(Math.floor(Date.now() / 1000) + 600),
        ],
      },
    ])
  }
}

Error Handling

Transaction Error Handler

export class ContractErrorHandler {
  static getErrorMessage(error: Error): string {
    const message = error.message.toLowerCase()
 
    if (message.includes("user not authenticated")) {
      return "Please login to perform transactions"
    }
 
    if (message.includes("insufficient funds")) {
      return "Insufficient balance for transaction"
    }
 
    if (message.includes("execution reverted")) {
      return "Contract execution failed"
    }
 
    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")
  }
}

Transaction with Error Handling

async function executeTransactionWithRetry(
  client: ClientRepository,
  contractParams: WriteContractParameters,
  maxRetries: number = 3,
): Promise<string> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      if (!client.isConnected) {
        throw new Error("User not authenticated")
      }
 
      const hash = await client.writeContract(contractParams)
      console.log(`Transaction successful on attempt ${attempt}:`, hash)
      return hash
    } catch (error) {
      const errorMessage = ContractErrorHandler.getErrorMessage(error as Error)
      console.error(`Attempt ${attempt} failed:`, errorMessage)
 
      if (attempt === maxRetries || !ContractErrorHandler.isRetryableError(error as Error)) {
        throw new Error(`Transaction failed after ${attempt} attempts: ${errorMessage}`)
      }
 
      // Wait before retry (exponential backoff)
      await new Promise((resolve) => setTimeout(resolve, 1000 * attempt))
    }
  }
 
  throw new Error("Transaction failed after all retry attempts")
}

React Integration

Contract Hook

import { useState, useCallback } from "react"
import { ClientRepository, WriteContractParameters } from "@humanwallet/sdk"
 
export function useContract(client: ClientRepository) {
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string>()
 
  const writeContract = useCallback(
    async (params: WriteContractParameters) => {
      if (!client.isConnected) {
        throw new Error("User not authenticated")
      }
 
      setIsLoading(true)
      setError(undefined)
 
      try {
        const hash = await client.writeContract(params)
        return hash
      } catch (err) {
        const errorMessage = ContractErrorHandler.getErrorMessage(err as Error)
        setError(errorMessage)
        throw err
      } finally {
        setIsLoading(false)
      }
    },
    [client],
  )
 
  const readContract = useCallback(
    async (params: any) => {
      setError(undefined)
 
      try {
        return await client.readContract(params)
      } catch (err) {
        const errorMessage = (err as Error).message
        setError(errorMessage)
        throw err
      }
    },
    [client],
  )
 
  return {
    writeContract,
    readContract,
    isLoading,
    error,
  }
}

Best Practices

1. Always Check Authentication

// Good: Check authentication before contract interactions
if (!client.isConnected) {
  await client.login("username")
}
 
const hash = await client.writeContract(params)

2. Use Batch Transactions When Possible

// Good: Batch related operations
const batchHash = await client.writeContracts([approveOperation, swapOperation])
 
// Avoid: Sequential transactions
// const approveHash = await client.writeContract(approveOperation)
// const swapHash = await client.writeContract(swapOperation)

3. Handle Errors Gracefully

try {
  const hash = await client.writeContract(params)
  await client.waitForUserOperation(hash)
  showSuccessMessage("Transaction completed!")
} catch (error) {
  const userFriendlyMessage = ContractErrorHandler.getErrorMessage(error)
  showErrorMessage(userFriendlyMessage)
}

4. Use Type-Safe Contract Interfaces

interface ERC20Contract {
  address: `0x${string}`
  abi: typeof ERC20_ABI
}
 
class TypedTokenService {
  constructor(
    private client: ClientRepository,
    private contract: ERC20Contract,
  ) {}
 
  async transfer(to: `0x${string}`, amount: bigint): Promise<string> {
    return this.client.writeContract({
      address: this.contract.address,
      abi: this.contract.abi,
      functionName: "transfer",
      args: [to, amount],
    })
  }
}