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()- Execute single contract transactionswriteContracts()- Execute multiple transactions in batchreadContract()- Read contract statesignTypedData()- Sign structured data
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 arrayfunctionName: Function name to callargs: Array of function arguments (optional)value: ETH value to send (optional, defaults to0n)
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 addressabi: Contract ABI arrayfunctionName: Function name to callargs: 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 separatortypes: Type definitions for the dataprimaryType: The primary type being signedmessage: 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 transactionHigh-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],
})
}
}