Skip to content

Interacting with Smart Contracts

Learn how to interact with smart contracts on Celo using the MiniPay wallet.

Reading Contract State

Read data from smart contracts using useReadContract:

tsx
import { useReadContract } from "wagmi";
import { erc20Abi } from "viem";

function TokenBalance({ tokenAddress }: { tokenAddress: `0x${string}` }) {
  const { address } = useAccount();

  const { data: balance, isLoading } = useReadContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: "balanceOf",
    args: [address!],
    query: {
      enabled: !!address,
    },
  });

  if (isLoading) return <div>Loading...</div>;

  return <div>Balance: {balance?.toString()}</div>;
}

Reading Multiple Contract Values (Batching)

Batch multiple reads in a single call using useReadContracts:

tsx
import { useReadContracts } from "wagmi";
import { erc20Abi, formatUnits } from "viem";
import type { Hex } from "viem";

function TokenInfo({ tokenAddress }: { tokenAddress: `0x${string}` }) {
  const { address } = useAccount();

  const { data: results, isLoading } = useReadContracts({
    contracts: [
      {
        address: tokenAddress,
        abi: erc20Abi,
        functionName: "balanceOf",
        args: [address as Hex],
      },
      {
        address: tokenAddress,
        abi: erc20Abi,
        functionName: "decimals",
      },
      {
        address: tokenAddress,
        abi: erc20Abi,
        functionName: "symbol",
      },
    ],
    query: {
      enabled: !!address,
    },
  });

  if (isLoading) return <div>Loading...</div>;

  const [balance, decimals, symbol] = results || [];
  const formatted = balance && decimals ? formatUnits(balance, decimals) : "0";

  return (
    <div>
      <p>
        {formatted} {symbol}
      </p>
    </div>
  );
}

Writing to Contracts

Write to contracts using useWriteContract:

tsx
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { encodeFunctionData, erc20Abi, parseUnits } from "viem";

function TransferTokens({
  tokenAddress,
  to,
  amount,
}: {
  tokenAddress: `0x${string}`;
  to: `0x${string}`;
  amount: string;
}) {
  const { writeContract, data: hash, isPending } = useWriteContract();
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
    hash,
  });

  const handleTransfer = () => {
    writeContract({
      address: tokenAddress,
      abi: erc20Abi,
      functionName: "transfer",
      args: [to, parseUnits(amount, 18)],
    });
  };

  return (
    <div>
      <button onClick={handleTransfer} disabled={isPending || isConfirming}>
        {isPending
          ? "Preparing..."
          : isConfirming
            ? "Confirming..."
            : "Transfer"}
      </button>
      {isSuccess && <p>Transfer successful!</p>}
    </div>
  );
}

Custom Contract ABIs

Define and use custom contract ABIs:

tsx
// contracts/my-contract.ts
export const MY_CONTRACT_ABI = [
  {
    inputs: [{ name: "value", type: "uint256" }],
    name: "setValue",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    inputs: [],
    name: "getValue",
    outputs: [{ name: "", type: "uint256" }],
    stateMutability: "view",
    type: "function",
  },
] as const;

// In your component
import { useReadContract, useWriteContract } from "wagmi";
import { MY_CONTRACT_ABI } from "./contracts/my-contract";

const CONTRACT_ADDRESS = "0x..." as `0x${string}`;

function MyContract() {
  const { data: value } = useReadContract({
    address: CONTRACT_ADDRESS,
    abi: MY_CONTRACT_ABI,
    functionName: "getValue",
  });

  const { writeContract } = useWriteContract();

  const handleSetValue = () => {
    writeContract({
      address: CONTRACT_ADDRESS,
      abi: MY_CONTRACT_ABI,
      functionName: "setValue",
      args: [BigInt(100)],
    });
  };

  return (
    <div>
      <p>Value: {value?.toString()}</p>
      <button onClick={handleSetValue}>Set Value</button>
    </div>
  );
}

Contract Address Management

Manage contract addresses per network:

tsx
import { useChainId } from "wagmi";
import { celo, celoSepolia } from "wagmi/chains";
import type { Address } from "viem";

const CONTRACT_ADDRESSES: Record<number, Address> = {
  [celo.id]: "0x..." as Address, // Mainnet
  [celoSepolia.id]: "0x..." as Address, // Testnet
};

function useContractAddress() {
  const chainId = useChainId();
  return CONTRACT_ADDRESSES[chainId];
}

// Usage
function MyComponent() {
  const contractAddress = useContractAddress();

  if (!contractAddress) {
    return <div>Contract not deployed on this network</div>;
  }

  // Use contractAddress with your contract calls
}

Handling Contract Events

Listen for contract events:

tsx
import { useEffect } from "react";
import { usePublicClient, useWatchContractEvent } from "wagmi";
import { MY_CONTRACT_ABI } from "./contracts/my-contract";

function useContractEvents(contractAddress: `0x${string}`) {
  const publicClient = usePublicClient();

  useWatchContractEvent({
    address: contractAddress,
    abi: MY_CONTRACT_ABI,
    eventName: "ValueChanged",
    onLogs(logs) {
      console.log("Value changed:", logs);
      // Handle event
    },
  });
}

Error Handling

Handle contract call errors:

tsx
import { useReadContract, useWriteContract } from "wagmi";

function ContractWithErrorHandling() {
  const {
    data,
    error: readError,
    isLoading,
  } = useReadContract({
    address: "0x...",
    abi: MY_CONTRACT_ABI,
    functionName: "getValue",
  });

  const { writeContract, error: writeError, isPending } = useWriteContract();

  if (readError) {
    return <div>Error reading contract: {readError.message}</div>;
  }

  const handleWrite = () => {
    try {
      writeContract({
        address: "0x...",
        abi: MY_CONTRACT_ABI,
        functionName: "setValue",
        args: [BigInt(100)],
      });
    } catch (error) {
      console.error("Write error:", error);
    }
  };

  return (
    <div>
      {writeError && <div>Error: {writeError.message}</div>}
      <button onClick={handleWrite} disabled={isPending}>
        Set Value
      </button>
    </div>
  );
}

Gas Estimation

Estimate gas before writing to contracts:

tsx
import { useEstimateGas } from "wagmi";

function useContractGasEstimate(
  contractAddress: `0x${string}`,
  functionName: string,
  args: unknown[],
) {
  const { data: gasEstimate, isLoading } = useEstimateGas({
    to: contractAddress,
    data: encodeFunctionData({
      abi: MY_CONTRACT_ABI,
      functionName,
      args,
    }),
  });

  return { gasEstimate, isLoading };
}

Best Practices

  1. Always validate addresses: Ensure contract addresses are valid before making calls
  2. Handle loading states: Show loading indicators while reading contract data
  3. Error handling: Provide user-friendly error messages for failed contract calls
  4. Network-specific addresses: Use different contract addresses for mainnet and testnet
  5. ABI management: Keep ABIs in separate files for better organization
  6. Type safety: Use TypeScript types for contract addresses and ABIs
  7. Contract verification: All contract source code must be published and verified on Celoscan (mainnet) or Celo Sepolia Celoscan (testnet). This is required for Mini App listing.
  8. Sample transactions: When submitting your Mini App, provide links to sample transactions for every user-facing contract method. See Submit your MiniApp for submission requirements.

Next Steps