Files
2025-11-29 18:26:05 +08:00

14 KiB

Complete Migration Patterns Reference

Comprehensive patterns for migrating from TheGraph subgraphs to HyperIndex.

File Structure Migration

Subgraph Structure

subgraph/
├── subgraph.yaml
├── schema.graphql
├── abis/
│   └── Contract.json
└── src/
    ├── mapping.ts
    └── utils/
        └── helpers.ts

HyperIndex Structure

hyperindex/
├── config.yaml
├── schema.graphql
├── abis/
│   └── Contract.json
└── src/
    ├── EventHandlers.ts  # or split by contract
    ├── factory.ts
    ├── pair.ts
    └── utils/
        └── helpers.ts

Config Migration Patterns

Single Contract

Subgraph:

dataSources:
  - kind: ethereum/contract
    name: MyContract
    network: mainnet
    source:
      address: "0x..."
      startBlock: 12345678
      abi: MyContract
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      entities:
        - Entity1
        - Entity2
      abis:
        - name: MyContract
          file: ./abis/MyContract.json
      eventHandlers:
        - event: Transfer(indexed address,indexed address,uint256)
          handler: handleTransfer

HyperIndex:

name: my-indexer
networks:
  - id: 1
    start_block: 12345678
    contracts:
      - name: MyContract
        address: 0x...
        handler: src/EventHandlers.ts
        events:
          - event: Transfer(address indexed from, address indexed to, uint256 value)

Multichain Configuration

HyperIndex multichain:

name: multichain-indexer
unordered_multichain_mode: true

# Global contract definitions
contracts:
  - name: Factory
    handler: src/factory.ts
    events:
      - event: PairCreated(address indexed token0, address indexed token1, address pair)

networks:
  - id: 1  # Ethereum
    start_block: 10000835
    contracts:
      - name: Factory
        address: 0xEthereumFactory

  - id: 10  # Optimism
    start_block: 1234567
    contracts:
      - name: Factory
        address: 0xOptimismFactory

  - id: 137  # Polygon
    start_block: 9876543
    contracts:
      - name: Factory
        address: 0xPolygonFactory

Key: Define handlers/events globally, addresses per network.

Dynamic Contracts (Templates)

Subgraph templates:

templates:
  - kind: ethereum/contract
    name: Pair
    network: mainnet
    source:
      abi: Pair
    mapping:
      eventHandlers:
        - event: Swap(...)
          handler: handleSwap

HyperIndex:

contracts:
  # Factory has address
  - name: Factory
    address: 0xFactoryAddress
    handler: src/factory.ts
    events:
      - event: PairCreated(...)

  # Pair has NO address - registered dynamically
  - name: Pair
    handler: src/pair.ts
    events:
      - event: Swap(...)
      - event: Mint(...)
      - event: Burn(...)
      - event: Sync(...)

Schema Migration Patterns

Type Conversions

TheGraph HyperIndex
Bytes! String!
BigInt! BigInt!
BigDecimal! BigDecimal!
Int! Int!
ID! ID!
Boolean! Boolean!

Entity Decorator

# TheGraph
type Token @entity {
  id: ID!
}

# HyperIndex - remove @entity
type Token {
  id: ID!
}

Immutable Entities

# TheGraph
type Transfer @entity(immutable: true) {
  id: ID!
}

# HyperIndex - just remove decorator
type Transfer {
  id: ID!
}

Entity Relationships

Direct reference → _id field:

# TheGraph
type Transfer @entity {
  token: Token!
}

# HyperIndex
type Transfer {
  token_id: String!  # Reference by ID
}

Arrays must have @derivedFrom:

# HyperIndex - REQUIRED
type Token {
  id: ID!
  transfers: [Transfer!]! @derivedFrom(field: "token")
}

type Transfer {
  id: ID!
  token_id: String!  # Field referenced by @derivedFrom
}

Complete Schema Example

TheGraph:

type Factory @entity {
  id: ID!
  pairCount: BigInt!
  pairs: [Pair!]! @derivedFrom(field: "factory")
}

type Token @entity {
  id: ID!
  symbol: String!
  name: String!
  decimals: BigInt!
}

type Pair @entity {
  id: ID!
  factory: Factory!
  token0: Token!
  token1: Token!
  reserve0: BigDecimal!
  reserve1: BigDecimal!
  swaps: [Swap!]! @derivedFrom(field: "pair")
}

type Swap @entity(immutable: true) {
  id: ID!
  pair: Pair!
  sender: Bytes!
  amount0In: BigDecimal!
  amount1In: BigDecimal!
  timestamp: BigInt!
  transaction: Bytes!
}

HyperIndex:

type Factory {
  id: ID!
  pairCount: BigInt!
  pairs: [Pair!]! @derivedFrom(field: "factory")
}

type Token {
  id: ID!
  symbol: String!
  name: String!
  decimals: BigInt!
}

type Pair {
  id: ID!
  factory_id: String!
  token0_id: String!
  token1_id: String!
  reserve0: BigDecimal!
  reserve1: BigDecimal!
  swaps: [Swap!]! @derivedFrom(field: "pair")
}

type Swap {
  id: ID!
  pair_id: String!
  sender: String!
  amount0In: BigDecimal!
  amount1In: BigDecimal!
  timestamp: BigInt!
  transactionHash: String!
}

Handler Migration Patterns

Basic Handler

TheGraph:

export function handleTransfer(event: TransferEvent): void {
  let id = event.transaction.hash.toHexString() + "-" + event.logIndex.toString();
  let transfer = new Transfer(id);

  transfer.from = event.params.from;
  transfer.to = event.params.to;
  transfer.value = event.params.value;
  transfer.timestamp = event.block.timestamp;
  transfer.blockNumber = event.block.number;

  transfer.save();
}

HyperIndex:

import { MyContract } from "generated";

MyContract.Transfer.handler(async ({ event, context }) => {
  const id = `${event.chainId}-${event.transaction.hash}-${event.logIndex}`;

  const transfer = {
    id,
    from: event.params.from,
    to: event.params.to,
    value: event.params.value,
    timestamp: BigInt(event.block.timestamp),
    blockNumber: BigInt(event.block.number),
  };

  context.Transfer.set(transfer);
});

Entity Loading and Updates

TheGraph:

export function handleApproval(event: ApprovalEvent): void {
  let token = Token.load(event.address.toHexString());

  if (token == null) {
    token = new Token(event.address.toHexString());
    token.symbol = "UNKNOWN";
    token.name = "Unknown Token";
    token.decimals = BigInt.fromI32(18);
    token.totalSupply = BigInt.fromI32(0);
  }

  token.approvalCount = token.approvalCount.plus(BigInt.fromI32(1));
  token.save();
}

HyperIndex:

MyContract.Approval.handler(async ({ event, context }) => {
  const tokenId = `${event.chainId}-${event.srcAddress}`;
  let token = await context.Token.get(tokenId);

  if (!token) {
    token = {
      id: tokenId,
      symbol: "UNKNOWN",
      name: "Unknown Token",
      decimals: BigInt(18),
      totalSupply: BigInt(0),
      approvalCount: BigInt(0),
    };
  }

  // Use spread operator - entities are immutable
  context.Token.set({
    ...token,
    approvalCount: token.approvalCount + BigInt(1),
  });
});

Dynamic Contract Registration

TheGraph:

import { Pair as PairTemplate } from "../generated/templates";

export function handlePairCreated(event: PairCreatedEvent): void {
  // Create template instance
  PairTemplate.create(event.params.pair);

  // Create Pair entity
  let pair = new Pair(event.params.pair.toHexString());
  pair.token0 = event.params.token0.toHexString();
  pair.token1 = event.params.token1.toHexString();
  pair.save();
}

HyperIndex:

import { Factory, Pair } from "generated";

// Register contract BEFORE handler
Factory.PairCreated.contractRegister(({ event, context }) => {
  context.addPair(event.params.pair);  // Method name: add{ContractName}
});

Factory.PairCreated.handler(async ({ event, context }) => {
  const pairId = `${event.chainId}-${event.params.pair}`;

  const pair = {
    id: pairId,
    token0_id: `${event.chainId}-${event.params.token0}`,
    token1_id: `${event.chainId}-${event.params.token1}`,
    reserve0: BigInt(0),
    reserve1: BigInt(0),
  };

  context.Pair.set(pair);
});

BigDecimal Handling

Maintain precision from original subgraph:

import { BigDecimal } from "generated";

// Constants
export const ZERO_BI = BigInt(0);
export const ONE_BI = BigInt(1);
export const ZERO_BD = new BigDecimal(0);
export const ONE_BD = new BigDecimal(1);
export const BI_18 = BigInt(18);

// Convert to decimal with proper precision
export function exponentToBigDecimal(decimals: bigint): BigDecimal {
  let bd = ONE_BD;
  for (let i = ZERO_BI; i < decimals; i = i + ONE_BI) {
    bd = bd.times(new BigDecimal(10));
  }
  return bd;
}

export function convertTokenToDecimal(
  tokenAmount: bigint,
  exchangeDecimals: bigint
): BigDecimal {
  if (exchangeDecimals === ZERO_BI) {
    return new BigDecimal(tokenAmount.toString());
  }
  return new BigDecimal(tokenAmount.toString()).div(
    exponentToBigDecimal(exchangeDecimals)
  );
}

Effect API for RPC Calls

TheGraph contract bindings:

import { ERC20 } from "../generated/templates/Pair/ERC20";

export function fetchTokenSymbol(tokenAddress: Address): string {
  let contract = ERC20.bind(tokenAddress);
  let result = contract.try_symbol();
  if (result.reverted) {
    return "UNKNOWN";
  }
  return result.value;
}

HyperIndex Effect API:

import { createEffect, S } from "envio";
import { createPublicClient, http, parseAbi } from "viem";

const ERC20_ABI = parseAbi([
  "function symbol() view returns (string)",
  "function name() view returns (string)",
  "function decimals() view returns (uint8)",
]);

const client = createPublicClient({
  transport: http(process.env.RPC_URL),
});

export const fetchTokenSymbol = createEffect(
  {
    name: "fetchTokenSymbol",
    input: S.string,
    output: S.string,
    cache: true,
  },
  async ({ input: tokenAddress }) => {
    try {
      const symbol = await client.readContract({
        address: tokenAddress as `0x${string}`,
        abi: ERC20_ABI,
        functionName: "symbol",
      });
      return symbol;
    } catch {
      return "UNKNOWN";
    }
  }
);

// Usage in handler
const symbol = await context.effect(fetchTokenSymbol, tokenAddress);

Timestamp and Block Data

// TheGraph
entity.timestamp = event.block.timestamp;
entity.blockNumber = event.block.number;

// HyperIndex - always cast to BigInt
entity.timestamp = BigInt(event.block.timestamp);
entity.blockNumber = BigInt(event.block.number);

// For day-based aggregations
const dayTimestamp = Math.floor(event.block.timestamp / 86400) * 86400;
const dayId = `${event.chainId}-${dayTimestamp}-${tokenAddress}`;

Field Selection for Transaction Data

When handler needs event.transaction.hash:

events:
  - event: Transfer(address indexed from, address indexed to, uint256 value)
    field_selection:
      transaction_fields:
        - hash
        - from  # optional
        - to    # optional

Without field_selection, event.transaction will be {}.

Complete Handler Migration Example

Original TheGraph handler:

import { BigInt, BigDecimal, Address } from "@graphprotocol/graph-ts";
import { Swap as SwapEvent } from "../generated/templates/Pair/Pair";
import { Swap, Pair, Token, Factory } from "../generated/schema";
import { convertTokenToDecimal, ZERO_BD, ONE_BI } from "./helpers";

export function handleSwap(event: SwapEvent): void {
  let pair = Pair.load(event.address.toHexString());
  if (pair === null) return;

  let token0 = Token.load(pair.token0);
  let token1 = Token.load(pair.token1);
  if (token0 === null || token1 === null) return;

  let amount0In = convertTokenToDecimal(event.params.amount0In, token0.decimals);
  let amount1In = convertTokenToDecimal(event.params.amount1In, token1.decimals);
  let amount0Out = convertTokenToDecimal(event.params.amount0Out, token0.decimals);
  let amount1Out = convertTokenToDecimal(event.params.amount1Out, token1.decimals);

  let swap = new Swap(
    event.transaction.hash.toHexString() + "-" + event.logIndex.toString()
  );
  swap.pair = pair.id;
  swap.sender = event.params.sender;
  swap.to = event.params.to;
  swap.amount0In = amount0In;
  swap.amount1In = amount1In;
  swap.amount0Out = amount0Out;
  swap.amount1Out = amount1Out;
  swap.timestamp = event.block.timestamp;
  swap.transaction = event.transaction.hash;
  swap.save();

  // Update pair
  pair.txCount = pair.txCount.plus(ONE_BI);
  pair.save();

  // Update factory
  let factory = Factory.load("1");
  if (factory !== null) {
    factory.txCount = factory.txCount.plus(ONE_BI);
    factory.save();
  }
}

Migrated HyperIndex handler:

import { Pair } from "generated";
import { convertTokenToDecimal, ZERO_BD, ONE_BI } from "./utils/helpers";

Pair.Swap.handler(async ({ event, context }) => {
  const pairId = `${event.chainId}-${event.srcAddress}`;
  const pair = await context.Pair.get(pairId);
  if (!pair) return;

  const token0 = await context.Token.get(pair.token0_id);
  const token1 = await context.Token.get(pair.token1_id);
  if (!token0 || !token1) return;

  const amount0In = convertTokenToDecimal(event.params.amount0In, token0.decimals);
  const amount1In = convertTokenToDecimal(event.params.amount1In, token1.decimals);
  const amount0Out = convertTokenToDecimal(event.params.amount0Out, token0.decimals);
  const amount1Out = convertTokenToDecimal(event.params.amount1Out, token1.decimals);

  const swap = {
    id: `${event.chainId}-${event.transaction.hash}-${event.logIndex}`,
    pair_id: pairId,
    sender: event.params.sender,
    to: event.params.to,
    amount0In,
    amount1In,
    amount0Out,
    amount1Out,
    timestamp: BigInt(event.block.timestamp),
    transactionHash: event.transaction.hash,
  };

  context.Swap.set(swap);

  // Update pair - use spread operator
  context.Pair.set({
    ...pair,
    txCount: pair.txCount + ONE_BI,
  });

  // Update factory
  const factoryId = `${event.chainId}-factory`;
  const factory = await context.Factory.get(factoryId);
  if (factory) {
    context.Factory.set({
      ...factory,
      txCount: factory.txCount + ONE_BI,
    });
  }
});