| name | subgraph-migration |
| description | Migrate a TheGraph subgraph to Envio HyperIndex using TDD. Covers schema conversion (remove @entity, Bytes->String, @derivedFrom), handler translation (save->set, store.get->context.get, templates->contractRegister), and verification against subgraph data. |
TheGraph to Envio HyperIndex Migration
Migrate from TheGraph subgraph to Envio HyperIndex using Test-Driven Development.
Before Starting
Ask the user for:
- Subgraph GraphQL endpoint (e.g.,
https://api.thegraph.com/subgraphs/name/org/subgraph)
- Chain ID and contract addresses
- Block range for testing (pick blocks with representative events)
The subgraph GraphQL is the source of truth — use it to verify HyperIndex produces identical data.
Prerequisites: Subgraph folder in workspace, Node.js v22+, pnpm, Docker
References:
Quick Reference: Key Differences
| TheGraph | Envio |
|---|
@entity decorator | No decorator needed |
Bytes! | String! |
entity.save() | context.Entity.set(entity) |
store.get("Entity", id) | await context.Entity.get(id) |
ContractTemplate.create(addr) | context.addContract(addr) in contractRegister |
| Direct array access | @derivedFrom (virtual, query via getWhere) |
.bind() for RPC | Effect API with context.effect() |
Runtime Testing Mandate
After EVERY code change, run pnpm test. TypeScript compilation only catches syntax errors — runtime errors (database issues, missing entities, logic errors) only appear when running the indexer.
See references/step-by-step.md for the full runtime testing checklist.
TDD Migration Flow
- Query subgraph for expected entity state at specific blocks
- Write test that processes those blocks and asserts expected output
- Implement handler until test passes
- Repeat for each handler
import { describe, it, expect } from "vitest";
import { createTestIndexer } from "generated";
describe("Migration Verification", () => {
it("Should match subgraph data for Factory.PairCreated", async () => {
const indexer = createTestIndexer();
const result = await indexer.process({
chains: {
1: { startBlock: 10_000_000, endBlock: 10_000_100 },
},
});
expect(result.changes).toMatchInlineSnapshot(`...`);
});
});
Run tests: pnpm test
Migration Steps Overview
After each step, run: pnpm codegen && pnpm tsc --noEmit && pnpm test
Full step-by-step details with quality checks: references/step-by-step.md
Step 1: Clear Boilerplate
Clear generated event handlers and replace with empty TODO handlers.
Step 2: Migrate Schema
- Remove
@entity decorators
Bytes! → String!
- Keep
BigInt!, BigDecimal!, ID!
- ALL entity arrays MUST have
@derivedFrom (causes a codegen error without it)
@derivedFrom arrays are virtual — cannot access in handlers, only API queries
type Transaction { mints: [Mint!]! }
type Transaction { mints: [Mint!]! @derivedFrom(field: "transaction") }
Run: pnpm codegen (required after schema changes)
Step 3: Refactor File Structure
Mirror subgraph file structure with exact filenames. Update config.yaml with global contract definitions and chain-specific addresses only.
Step 4: Register Dynamic Contracts
For factory-created contracts: add contractRegister BEFORE handler, remove address from dynamic contracts in config.
Factory.PairCreated.contractRegister(({ event, context }) => {
context.addPair(event.params.pair);
});
Step 5: Implement Handlers (TDD)
Implementation order is critical. See references/step-by-step.md for full 5a/5b/5c/5d breakdown.
- 5a: Helper functions with no entity dependencies — implement COMPLETE logic, not placeholders
- 5b: Simple handlers — direct parameter mapping
- 5c: Moderate handlers — calls helpers, multiple entity updates
- 5d: Complex handlers — one at a time, with RPC calls and multiple dependencies
External calls MUST use Effect API — see references/migration-patterns.md for Effect API and contract state fetching patterns.
Step 6: Final Verification
Systematic review of every handler and helper function against subgraph. Multiple passes — first pass always misses things. See references/step-by-step.md for full verification checklist.
Step 7: Environment Variables
Search for all process.env references and update .env.example.
Code Patterns
Full patterns with examples: references/migration-patterns.md
Entity Creation
let entity = new Entity(id);
entity.field = value;
entity.save();
const entity: Entity = {
id: `${event.chainId}-${id}`,
field: value,
blockNumber: BigInt(event.block.number),
transactionHash: event.transaction.hash,
};
context.Entity.set(entity);
Entity Updates (spread required — entities are read-only)
let entity = await context.Entity.get(id);
if (entity) {
context.Entity.set({ ...entity, field: newValue });
}
Related Entity Queries (@derivedFrom → getWhere)
const mints = await context.Mint.getWhere({ transaction_id: { _eq: transactionId } });
BigDecimal Precision
import { BigDecimal } from "generated";
export const ZERO_BD = new BigDecimal(0);
export const ONE_BD = new BigDecimal(1);
export const ZERO_BI = BigInt(0);
export const ONE_BI = BigInt(1);
Contract State Fetching (.bind() → Effect API)
TheGraph uses .bind() for RPC. Envio requires Effect API with viem. Full pattern: references/migration-patterns.md
Common Issues & Fixes
Full quality check guide with 12 common fixes: references/quality-checks.md
Field Names — use _id suffix
Missing async/await
Note: context.Entity.set() is synchronous — no await needed.
Entity Type Imports
import type { Entities } from "generated";
const p: Entities["Pair"] = { ... };
Schema Type Mapping
| Schema | TypeScript |
|---|
Int! | number |
BigInt! | bigint / ZERO_BI |
BigDecimal! | BigDecimal / ZERO_BD |
String! / Bytes! | string |
Entity! | entity_id: string |
Multichain IDs
Prefix all entity IDs: ${event.chainId}-${originalId}
Reference Files