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

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

  1. Always cache when possible - Set cache: true for idempotent calls
  2. Handle errors gracefully - Return default values on failure
  3. Batch calls - Use Promise.all() for multiple independent calls
  4. Organize effects - Create src/effects/ directory for effect definitions
  5. Use typed inputs - Define proper schemas for type safety
  6. 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