Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:26:05 +08:00
commit 8bcde7080b
26 changed files with 5957 additions and 0 deletions

View File

@@ -0,0 +1,372 @@
# Common Migration Mistakes and Solutions
Common pitfalls when migrating from TheGraph to HyperIndex, with solutions.
## 1. Missing async/await
**Problem:** Entity loading returns empty object `{}` instead of entity.
```typescript
// WRONG - Missing await
const token = context.Token.get(tokenId);
if (token) {
// token is {} not the actual entity
}
// CORRECT
const token = await context.Token.get(tokenId);
if (token) {
// token is the actual entity
}
```
**Note:** `context.Entity.set()` does NOT need await - it's synchronous.
## 2. Entity Arrays Without @derivedFrom
**Problem:** Codegen fails with error "EE211: Arrays of entities is unsupported"
```graphql
# WRONG - Missing @derivedFrom
type Token {
transfers: [Transfer!]!
}
# CORRECT - Must have @derivedFrom
type Token {
transfers: [Transfer!]! @derivedFrom(field: "token")
}
type Transfer {
token_id: String! # Field referenced in @derivedFrom
}
```
**Important:** @derivedFrom arrays are virtual fields - they exist only in GraphQL API, not in handlers.
## 3. Accessing @derivedFrom Arrays in Handlers
**Problem:** Trying to access virtual arrays in handlers.
```typescript
// WRONG - derivedFrom arrays don't exist in handlers
const transfers = token.transfers; // undefined
// CORRECT - Query using the relationship field
// Use indexed field operations if available:
const transfers = await context.Transfer.getWhere.token_id.eq(tokenId);
// Or query by ID if you know specific transfer IDs
const transfer = await context.Transfer.get(transferId);
```
## 4. Mutating Entities Directly
**Problem:** Entities are read-only, direct mutation doesn't work.
```typescript
// WRONG - Entities are immutable
token.totalSupply = newSupply;
context.Token.set(token); // Changes not saved
// CORRECT - Use spread operator
context.Token.set({
...token,
totalSupply: newSupply,
});
```
## 5. Missing Field Selection for Transaction Data
**Problem:** `event.transaction.hash` is undefined.
```yaml
# WRONG - No field selection
events:
- event: Transfer(address indexed from, address indexed to, uint256 value)
# CORRECT - Add field_selection
events:
- event: Transfer(address indexed from, address indexed to, uint256 value)
field_selection:
transaction_fields:
- hash
```
## 6. Direct Relationship References
**Problem:** Using entity objects instead of ID strings.
```typescript
// WRONG - TheGraph style
const transfer = {
token: tokenObject, // Direct reference
};
// CORRECT - HyperIndex uses _id fields
const transfer = {
token_id: tokenObject.id, // String ID reference
};
```
Schema must also use `_id` suffix:
```graphql
type Transfer {
token_id: String! # Not token: Token!
}
```
## 7. Bytes vs String Type Mismatch
**Problem:** Using Bytes type which doesn't exist in HyperIndex.
```graphql
# WRONG - TheGraph type
sender: Bytes!
transactionHash: Bytes!
# CORRECT - HyperIndex uses String
sender: String!
transactionHash: String!
```
## 8. Missing Multichain ID Prefixes
**Problem:** ID collisions across chains in multichain indexers.
```typescript
// WRONG - ID collision between chains
const id = event.params.tokenId.toString();
// CORRECT - Prefix with chainId
const id = `${event.chainId}-${event.params.tokenId}`;
// For chain-specific singleton entities
const bundleId = `${event.chainId}-1`; // Not just "1"
```
## 9. Contract Address in Dynamic Contract Config
**Problem:** Including address for dynamically registered contracts.
```yaml
# WRONG - Dynamic contract shouldn't have address
contracts:
- name: Pair
address: 0xSomeAddress # Remove this!
handler: src/pair.ts
events:
- event: Swap(...)
# CORRECT - No address field
contracts:
- name: Pair
handler: src/pair.ts
events:
- event: Swap(...)
```
## 10. Missing contractRegister
**Problem:** Dynamic contracts not being indexed.
```typescript
// WRONG - Only handler, no registration
Factory.PairCreated.handler(async ({ event, context }) => {
// Pairs won't be indexed!
});
// CORRECT - Register before handler
Factory.PairCreated.contractRegister(({ event, context }) => {
context.addPair(event.params.pair);
});
Factory.PairCreated.handler(async ({ event, context }) => {
// Now pairs will be indexed
});
```
## 11. Duplicate Contract Names in Multichain
**Problem:** Defining handlers in network sections instead of globally.
```yaml
# WRONG - Duplicates contract definition
networks:
- id: 1
contracts:
- name: Factory
handler: src/factory.ts # Don't repeat here
events: [...]
address: [...]
- id: 10
contracts:
- name: Factory
handler: src/factory.ts # Don't repeat here
events: [...]
address: [...]
# CORRECT - Global definition, network-specific addresses
contracts:
- name: Factory
handler: src/factory.ts
events:
- event: PairCreated(...)
networks:
- id: 1
contracts:
- name: Factory
address: 0xEthereumAddress
- id: 10
contracts:
- name: Factory
address: 0xOptimismAddress
```
## 12. Losing BigDecimal Precision
**Problem:** Converting financial values to JavaScript numbers.
```typescript
// WRONG - Loses precision
const price = Number(amount) / 10 ** 18;
export const ZERO_BD = 0; // Wrong type
// CORRECT - Maintain BigDecimal precision
import { BigDecimal } from "generated";
const ZERO_BD = new BigDecimal(0);
function convertToDecimal(amount: bigint, decimals: bigint): BigDecimal {
return new BigDecimal(amount.toString()).div(
new BigDecimal((10n ** decimals).toString())
);
}
```
## 13. Wrong Entity Type Imports
**Problem:** Importing contract handlers instead of entity types.
```typescript
// WRONG - Imports contract
import { Pair, Token } from "generated";
// CORRECT - Import entity types
import { Pair_t, Token_t } from "generated/src/db/Entities.gen";
// Or use inferred types
const pair: Pair_t = {
id: "...",
// ...
};
```
## 14. External Calls Without Effect API
**Problem:** Direct RPC calls with preload_handlers enabled.
```typescript
// WRONG - Direct call runs twice during preload
const balance = await client.readContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: "balanceOf",
});
// CORRECT - Use Effect API
import { createEffect, S } from "envio";
export const getBalance = createEffect({
name: "getBalance",
input: S.string,
output: S.string,
cache: true,
}, async ({ input: address }) => {
const balance = await client.readContract({
address: address as `0x${string}`,
abi: ERC20_ABI,
functionName: "balanceOf",
});
return balance.toString();
});
// In handler
const balance = await context.effect(getBalance, tokenAddress);
```
## 15. Hardcoded Factory Addresses
**Problem:** Using hardcoded addresses instead of constants.
```typescript
// WRONG - Hardcoded address
const factory = await context.Factory.get("0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f");
// CORRECT - Use constants
import { FACTORY_ADDRESS } from "./constants";
const factory = await context.Factory.get(`${event.chainId}-${FACTORY_ADDRESS}`);
```
## 16. Schema Int vs BigInt Mismatch
**Problem:** Using wrong type for numeric fields.
```graphql
# Schema defines
date: Int! # Expects JavaScript number
amount: BigInt! # Expects bigint
```
```typescript
// WRONG - Type mismatch
const entity = {
date: BigInt(timestamp), // Schema expects number
amount: 123, // Schema expects bigint
};
// CORRECT - Match schema types
const entity = {
date: timestamp, // number for Int!
amount: BigInt(123), // bigint for BigInt!
};
```
## 17. Null vs Undefined for Optional Fields
**Problem:** Using null instead of undefined.
```typescript
// WRONG - TheGraph uses null
const entity = {
optionalField: null,
};
// CORRECT - HyperIndex uses undefined
const entity = {
optionalField: undefined,
};
// Or omit the field entirely
const entity = {
// optionalField not included
};
```
## Migration Validation Checklist
After migrating, verify:
- [ ] `pnpm codegen` runs without errors
- [ ] `pnpm tsc --noEmit` compiles successfully
- [ ] `TUI_OFF=true pnpm dev` runs and indexes events
- [ ] All handlers have async keyword
- [ ] All context.Entity.get() have await
- [ ] All entity updates use spread operator
- [ ] All relationships use _id suffix
- [ ] All entity arrays have @derivedFrom
- [ ] Transaction access has field_selection
- [ ] IDs are prefixed with chainId for multichain
- [ ] Dynamic contracts use contractRegister
- [ ] BigDecimal precision is maintained
- [ ] External calls use Effect API (if preload_handlers enabled)

View File

@@ -0,0 +1,648 @@
# 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:**
```yaml
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:**
```yaml
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:**
```yaml
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:**
```yaml
templates:
- kind: ethereum/contract
name: Pair
network: mainnet
source:
abi: Pair
mapping:
eventHandlers:
- event: Swap(...)
handler: handleSwap
```
**HyperIndex:**
```yaml
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
```graphql
# TheGraph
type Token @entity {
id: ID!
}
# HyperIndex - remove @entity
type Token {
id: ID!
}
```
### Immutable Entities
```graphql
# TheGraph
type Transfer @entity(immutable: true) {
id: ID!
}
# HyperIndex - just remove decorator
type Transfer {
id: ID!
}
```
### Entity Relationships
**Direct reference → _id field:**
```graphql
# TheGraph
type Transfer @entity {
token: Token!
}
# HyperIndex
type Transfer {
token_id: String! # Reference by ID
}
```
**Arrays must have @derivedFrom:**
```graphql
# 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:**
```graphql
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:**
```graphql
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:
```typescript
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:**
```typescript
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:**
```typescript
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
```typescript
// 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`:
```yaml
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:**
```typescript
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:**
```typescript
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,
});
}
});
```