5.9 KiB
5.9 KiB
Effect API for External Calls
When preload_handlers: true is enabled in config.yaml, all external calls (RPC, API, fetch) MUST use the Effect API.
Why Effect API?
With preload optimizations enabled:
- Handlers run twice (preload phase + execution phase)
- Direct external calls would execute twice
- Effect API caches results and handles preload correctly
Creating an Effect
import { S, createEffect } from "envio";
export const getTokenMetadata = createEffect(
{
name: "getTokenMetadata", // For debugging
input: S.string, // Input schema
output: S.object({ // Output schema
name: S.string,
symbol: S.string,
decimals: S.number,
}),
rateLimit: false, // Disable rate limiting
cache: true, // Enable caching
},
async ({ input: tokenAddress, context }) => {
// External call here
const response = await fetch(`https://api.example.com/token/${tokenAddress}`);
return response.json();
}
);
Schema Definition (S module)
The S module provides schema builders:
import { S } from "envio";
// Primitives
S.string
S.number
S.boolean
S.bigint
// Nullable
S.union([S.string, S.null])
// Objects
S.object({
name: S.string,
value: S.number,
})
// Arrays
S.array(S.string)
// Tuples
S.tuple([S.string, S.number])
Full schema API: https://raw.githubusercontent.com/DZakh/sury/refs/tags/v9.3.0/docs/js-usage.md
Using Effects in Handlers
import { getTokenMetadata } from "./effects";
MyContract.Event.handler(async ({ event, context }) => {
// Call effect with context.effect()
const metadata = await context.effect(getTokenMetadata, event.params.token);
// Use the result
const entity = {
id: event.params.token,
name: metadata.name,
symbol: metadata.symbol,
decimals: BigInt(metadata.decimals),
};
context.Token.set(entity);
});
RPC Calls with Viem
For blockchain RPC calls, use viem with Effect API:
import { createEffect, S } from "envio";
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";
const ERC20_ABI = parseAbi([
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function totalSupply() view returns (uint256)",
]);
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
export const fetchTokenData = createEffect(
{
name: "fetchTokenData",
input: S.string,
output: S.object({
name: S.string,
symbol: S.string,
decimals: S.number,
totalSupply: S.string,
}),
cache: true,
},
async ({ input: tokenAddress }) => {
try {
const [name, symbol, decimals, totalSupply] = await Promise.all([
client.readContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: "name",
}),
client.readContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: "symbol",
}),
client.readContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: "decimals",
}),
client.readContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: "totalSupply",
}),
]);
return {
name,
symbol,
decimals: Number(decimals),
totalSupply: totalSupply.toString(),
};
} catch (error) {
// Return defaults on error
return {
name: "Unknown",
symbol: "???",
decimals: 18,
totalSupply: "0",
};
}
}
);
Multichain RPC
For multichain indexers, pass chainId to select correct RPC:
import { createEffect, S } from "envio";
import { createPublicClient, http } from "viem";
const RPC_URLS: Record<number, string> = {
1: process.env.ETH_RPC_URL!,
10: process.env.OP_RPC_URL!,
137: process.env.POLYGON_RPC_URL!,
};
export const fetchBalance = createEffect(
{
name: "fetchBalance",
input: S.object({
chainId: S.number,
address: S.string,
}),
output: S.string,
cache: true,
},
async ({ input }) => {
const rpcUrl = RPC_URLS[input.chainId];
if (!rpcUrl) throw new Error(`No RPC for chain ${input.chainId}`);
const client = createPublicClient({
transport: http(rpcUrl),
});
const balance = await client.getBalance({
address: input.address as `0x${string}`,
});
return balance.toString();
}
);
// Usage in handler
MyContract.Event.handler(async ({ event, context }) => {
const balance = await context.effect(fetchBalance, {
chainId: event.chainId,
address: event.params.user,
});
});
Skipping Preload Logic
Use !context.isPreload to skip non-essential logic during preload:
MyContract.Event.handler(async ({ event, context }) => {
// This always runs
const entity = await context.Token.get(event.params.token);
if (!context.isPreload) {
// This only runs during actual execution
console.log("Processing event:", event.logIndex);
}
context.Token.set(entity);
});
Best Practices
- Always cache when possible - Set
cache: truefor idempotent calls - Handle errors gracefully - Return default values on failure
- Batch calls - Use
Promise.all()for multiple independent calls - Organize effects - Create
src/effects/directory for effect definitions - Use typed inputs - Define proper schemas for type safety
- Document env vars - List required RPC URLs in
.env.example
File Organization
src/
├── effects/
│ ├── index.ts # Export all effects
│ ├── tokenMetadata.ts # Token-related effects
│ └── pricing.ts # Price fetching effects
├── EventHandlers.ts
└── utils.ts