7.2 KiB
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
- Always use Effect API - Never call RPC directly in handlers
- Enable caching -
cache: trueprevents duplicate calls - Use multicall - Batch multiple contract reads into one RPC call
- Handle non-standard tokens - Try string first, fallback to bytes32
- Provide fallback values - Don't let RPC failures crash your indexer
- 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://...