262 lines
7.2 KiB
Markdown
262 lines
7.2 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
```typescript
|
|
const balance = await client.getBalance({
|
|
address: "0x...",
|
|
blockNumber: BigInt(event.block.number), // Requires archive node
|
|
});
|
|
```
|
|
|
|
## Error Handling Pattern
|
|
|
|
```typescript
|
|
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://...
|
|
```
|