Files
gh-enviodev-envio-plugins-p…/skills/hyperindex-development/references/contract-state.md
2025-11-29 18:26:05 +08:00

7.2 KiB

Accessing Contract State

Read on-chain contract state (token metadata, balances, etc.) from your event handlers using the Effect API with viem.

When You Need This

  • Token metadata (name, symbol, decimals) not in events
  • Current balances or allowances
  • Contract configuration values
  • Any on-chain data not emitted in events

Basic Setup

1. Create a Viem Client

// src/effects/client.ts
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";

export const client = createPublicClient({
  chain: mainnet,
  batch: { multicall: true },  // Enable multicall batching
  transport: http(process.env.RPC_URL, { batch: true }),
});

2. Create an Effect for RPC Calls

// src/effects/tokenMetadata.ts
import { experimental_createEffect, S } from "envio";
import { client } from "./client";

const ERC20_ABI = [
  { name: "name", type: "function", inputs: [], outputs: [{ type: "string" }] },
  { name: "symbol", type: "function", inputs: [], outputs: [{ type: "string" }] },
  { name: "decimals", type: "function", inputs: [], outputs: [{ type: "uint8" }] },
] as const;

export const getTokenMetadata = experimental_createEffect(
  {
    name: "getTokenMetadata",
    input: S.object({
      tokenAddress: S.string,
      chainId: S.number,
    }),
    output: S.object({
      name: S.string,
      symbol: S.string,
      decimals: S.number,
    }),
    cache: true,  // Cache results - same token won't be fetched twice
  },
  async ({ input }) => {
    const address = input.tokenAddress as `0x${string}`;

    const [name, symbol, decimals] = await client.multicall({
      allowFailure: false,
      contracts: [
        { address, abi: ERC20_ABI, functionName: "name" },
        { address, abi: ERC20_ABI, functionName: "symbol" },
        { address, abi: ERC20_ABI, functionName: "decimals" },
      ],
    });

    return { name, symbol, decimals: Number(decimals) };
  }
);

3. Use in Handler

import { getTokenMetadata } from "./effects/tokenMetadata";

UniswapV3Factory.PoolCreated.handler(async ({ event, context }) => {
  // Fetch token metadata via Effect API
  const token0Data = await context.effect(getTokenMetadata, {
    tokenAddress: event.params.token0,
    chainId: event.chainId,
  });

  context.Token.set({
    id: event.params.token0,
    name: token0Data.name,
    symbol: token0Data.symbol,
    decimals: token0Data.decimals,
  });
});

Handling Non-Standard Tokens

Some tokens (like MKR, SAI) return bytes32 instead of string for name/symbol:

import { hexToString } from "viem";

export const getTokenMetadata = experimental_createEffect(
  {
    name: "getTokenMetadata",
    input: S.string,
    output: S.object({
      name: S.string,
      symbol: S.string,
      decimals: S.number,
    }),
    cache: true,
  },
  async ({ input: tokenAddress }) => {
    const address = tokenAddress as `0x${string}`;

    // Try standard ERC20 first
    try {
      const [name, symbol, decimals] = await client.multicall({
        allowFailure: false,
        contracts: [
          { address, abi: ERC20_ABI, functionName: "name" },
          { address, abi: ERC20_ABI, functionName: "symbol" },
          { address, abi: ERC20_ABI, functionName: "decimals" },
        ],
      });
      return { name, symbol, decimals: Number(decimals) };
    } catch {
      // Fallback: Try bytes32 variant
      try {
        const [name, symbol, decimals] = await client.multicall({
          allowFailure: false,
          contracts: [
            { address, abi: ERC20_BYTES_ABI, functionName: "name" },
            { address, abi: ERC20_BYTES_ABI, functionName: "symbol" },
            { address, abi: ERC20_BYTES_ABI, functionName: "decimals" },
          ],
        });
        return {
          name: hexToString(name).replace(/\u0000/g, ""),
          symbol: hexToString(symbol).replace(/\u0000/g, ""),
          decimals: Number(decimals),
        };
      } catch {
        // Final fallback
        return { name: "Unknown", symbol: "???", decimals: 18 };
      }
    }
  }
);

const ERC20_BYTES_ABI = [
  { name: "name", type: "function", inputs: [], outputs: [{ type: "bytes32" }] },
  { name: "symbol", type: "function", inputs: [], outputs: [{ type: "bytes32" }] },
  { name: "decimals", type: "function", inputs: [], outputs: [{ type: "uint8" }] },
] as const;

Multichain RPC

For indexers spanning multiple chains:

import { createPublicClient, http } from "viem";
import { mainnet, optimism, arbitrum } from "viem/chains";

const CHAINS: Record<number, { chain: any; rpcUrl: string }> = {
  1: { chain: mainnet, rpcUrl: process.env.ETH_RPC_URL! },
  10: { chain: optimism, rpcUrl: process.env.OP_RPC_URL! },
  42161: { chain: arbitrum, rpcUrl: process.env.ARB_RPC_URL! },
};

function getClient(chainId: number) {
  const config = CHAINS[chainId];
  if (!config) throw new Error(`No RPC configured for chain ${chainId}`);

  return createPublicClient({
    chain: config.chain,
    batch: { multicall: true },
    transport: http(config.rpcUrl, { batch: true }),
  });
}

export const getBalance = experimental_createEffect(
  {
    name: "getBalance",
    input: S.object({ chainId: S.number, address: S.string }),
    output: S.string,
    cache: true,
  },
  async ({ input }) => {
    const client = getClient(input.chainId);
    const balance = await client.getBalance({
      address: input.address as `0x${string}`,
    });
    return balance.toString();
  }
);

Important: Current vs Historical State

RPC calls return current blockchain state, not historical state at the event's block.

For most use cases (token metadata), this is fine - names/symbols/decimals rarely change.

For historical data (balance at specific block), you'd need archive node access:

const balance = await client.getBalance({
  address: "0x...",
  blockNumber: BigInt(event.block.number),  // Requires archive node
});

Error Handling Pattern

MyContract.Event.handler(async ({ event, context }) => {
  try {
    const metadata = await context.effect(getTokenMetadata, {
      tokenAddress: event.params.token,
      chainId: event.chainId,
    });

    context.Token.set({
      id: event.params.token,
      ...metadata,
    });
  } catch (error) {
    context.log.error("Failed to fetch token metadata", {
      token: event.params.token,
      error,
    });
    // Create with defaults or skip
    context.Token.set({
      id: event.params.token,
      name: "Unknown",
      symbol: "???",
      decimals: 18,
    });
  }
});

Best Practices

  1. Always use Effect API - Never call RPC directly in handlers
  2. Enable caching - cache: true prevents duplicate calls
  3. Use multicall - Batch multiple contract reads into one RPC call
  4. Handle non-standard tokens - Try string first, fallback to bytes32
  5. Provide fallback values - Don't let RPC failures crash your indexer
  6. Document required env vars - List all RPC URLs in .env.example

File Organization

src/
├── effects/
│   ├── index.ts           # Export all effects
│   ├── client.ts          # Viem client setup
│   └── tokenMetadata.ts   # Token-related effects
├── EventHandlers.ts
└── .env.example           # RPC_URL=https://...