Initial commit
This commit is contained in:
318
skills/subgraph-migration/SKILL.md
Normal file
318
skills/subgraph-migration/SKILL.md
Normal file
@@ -0,0 +1,318 @@
|
||||
---
|
||||
name: Subgraph Migration
|
||||
description: This skill should be used when the user asks to "migrate from subgraph", "convert subgraph to hyperindex", "migrate from thegraph", "port subgraph", "convert subgraph handlers", "migrate assemblyscript to typescript", or mentions TheGraph, subgraph migration, subgraph.yaml conversion, or converting from TheGraph to Envio. For core HyperIndex development patterns, refer to the hyperindex-development skill.
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Subgraph to HyperIndex Migration
|
||||
|
||||
Migrate from TheGraph subgraphs to Envio HyperIndex. HyperIndex delivers up to 100x faster indexing with a developer-friendly TypeScript API.
|
||||
|
||||
## Migration Overview
|
||||
|
||||
Three major steps:
|
||||
1. `subgraph.yaml` → `config.yaml`
|
||||
2. Schema migration (near copy-paste)
|
||||
3. Event handler migration (AssemblyScript → TypeScript)
|
||||
|
||||
## Step 1: Config Migration
|
||||
|
||||
### subgraph.yaml → config.yaml
|
||||
|
||||
**TheGraph:**
|
||||
```yaml
|
||||
specVersion: 0.0.4
|
||||
dataSources:
|
||||
- kind: ethereum/contract
|
||||
name: Factory
|
||||
network: mainnet
|
||||
source:
|
||||
address: "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"
|
||||
startBlock: 10000835
|
||||
abi: Factory
|
||||
mapping:
|
||||
eventHandlers:
|
||||
- event: PairCreated(indexed address,indexed address,address,uint256)
|
||||
handler: handlePairCreated
|
||||
templates:
|
||||
- name: Pair
|
||||
source:
|
||||
abi: Pair
|
||||
```
|
||||
|
||||
**HyperIndex:**
|
||||
```yaml
|
||||
name: my-indexer
|
||||
networks:
|
||||
- id: 1
|
||||
start_block: 10000835
|
||||
contracts:
|
||||
- name: Factory
|
||||
address: 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f
|
||||
handler: src/factory.ts
|
||||
events:
|
||||
- event: PairCreated(address indexed token0, address indexed token1, address pair, uint256)
|
||||
- name: Pair
|
||||
handler: src/pair.ts # No address - dynamic
|
||||
events:
|
||||
- event: Swap(...)
|
||||
```
|
||||
|
||||
**Key differences:**
|
||||
- Remove `dataSources`, `templates`, `mapping` nesting
|
||||
- Use `networks` → `contracts` structure
|
||||
- Event signatures include parameter names
|
||||
- Dynamic contracts have no address field
|
||||
|
||||
## Step 2: Schema Migration
|
||||
|
||||
### Remove @entity decorator
|
||||
|
||||
```graphql
|
||||
# TheGraph
|
||||
type Token @entity {
|
||||
id: ID!
|
||||
symbol: String!
|
||||
}
|
||||
|
||||
# HyperIndex
|
||||
type Token {
|
||||
id: ID!
|
||||
symbol: String!
|
||||
}
|
||||
```
|
||||
|
||||
### Convert Bytes to String
|
||||
|
||||
```graphql
|
||||
# TheGraph
|
||||
address: Bytes!
|
||||
|
||||
# HyperIndex
|
||||
address: String!
|
||||
```
|
||||
|
||||
### Entity Relationships
|
||||
|
||||
```graphql
|
||||
# TheGraph
|
||||
type Transfer @entity {
|
||||
token: Token!
|
||||
}
|
||||
|
||||
# HyperIndex - use _id suffix
|
||||
type Transfer {
|
||||
token_id: String!
|
||||
}
|
||||
```
|
||||
|
||||
### Entity Arrays MUST have @derivedFrom
|
||||
|
||||
```graphql
|
||||
# TheGraph (sometimes implicit)
|
||||
type Token @entity {
|
||||
transfers: [Transfer!]!
|
||||
}
|
||||
|
||||
# HyperIndex - REQUIRED explicit @derivedFrom
|
||||
type Token {
|
||||
transfers: [Transfer!]! @derivedFrom(field: "token")
|
||||
}
|
||||
|
||||
type Transfer {
|
||||
token_id: String! # The field referenced by @derivedFrom
|
||||
}
|
||||
```
|
||||
|
||||
**Critical:** Arrays without `@derivedFrom` cause codegen error EE211.
|
||||
|
||||
## Step 3: Handler Migration
|
||||
|
||||
### Basic Pattern
|
||||
|
||||
**TheGraph (AssemblyScript):**
|
||||
```typescript
|
||||
export function handleTransfer(event: TransferEvent): void {
|
||||
let entity = new Transfer(event.transaction.hash.toHexString());
|
||||
entity.from = event.params.from;
|
||||
entity.to = event.params.to;
|
||||
entity.amount = event.params.value;
|
||||
entity.save();
|
||||
}
|
||||
```
|
||||
|
||||
**HyperIndex (TypeScript):**
|
||||
```typescript
|
||||
import { MyContract } from "generated";
|
||||
|
||||
MyContract.Transfer.handler(async ({ event, context }) => {
|
||||
const entity = {
|
||||
id: `${event.chainId}-${event.transaction.hash}-${event.logIndex}`,
|
||||
from: event.params.from,
|
||||
to: event.params.to,
|
||||
amount: event.params.value,
|
||||
blockNumber: BigInt(event.block.number),
|
||||
};
|
||||
|
||||
context.Transfer.set(entity);
|
||||
});
|
||||
```
|
||||
|
||||
### Entity Loading
|
||||
|
||||
**TheGraph:**
|
||||
```typescript
|
||||
let token = Token.load(id);
|
||||
if (token == null) {
|
||||
token = new Token(id);
|
||||
}
|
||||
```
|
||||
|
||||
**HyperIndex:**
|
||||
```typescript
|
||||
let token = await context.Token.get(id);
|
||||
if (!token) {
|
||||
token = { id, name: "Unknown", /* ... */ };
|
||||
}
|
||||
```
|
||||
|
||||
### Entity Updates
|
||||
|
||||
**TheGraph:**
|
||||
```typescript
|
||||
token.totalSupply = newSupply;
|
||||
token.save();
|
||||
```
|
||||
|
||||
**HyperIndex (use spread - entities are immutable):**
|
||||
```typescript
|
||||
context.Token.set({
|
||||
...token,
|
||||
totalSupply: newSupply,
|
||||
});
|
||||
```
|
||||
|
||||
### Dynamic Contract Registration
|
||||
|
||||
**TheGraph:**
|
||||
```typescript
|
||||
import { Pair as PairTemplate } from "../generated/templates";
|
||||
PairTemplate.create(event.params.pair);
|
||||
```
|
||||
|
||||
**HyperIndex:**
|
||||
```typescript
|
||||
// Register BEFORE handler
|
||||
Factory.PairCreated.contractRegister(({ event, context }) => {
|
||||
context.addPair(event.params.pair);
|
||||
});
|
||||
|
||||
Factory.PairCreated.handler(async ({ event, context }) => {
|
||||
// Handle event...
|
||||
});
|
||||
```
|
||||
|
||||
### Contract State (RPC Calls)
|
||||
|
||||
**TheGraph:**
|
||||
```typescript
|
||||
let contract = ERC20.bind(address);
|
||||
let name = contract.name();
|
||||
```
|
||||
|
||||
**HyperIndex (use Effect API):**
|
||||
```typescript
|
||||
import { createEffect, S } from "envio";
|
||||
|
||||
export const getTokenName = createEffect({
|
||||
name: "getTokenName",
|
||||
input: S.string,
|
||||
output: S.string,
|
||||
cache: true,
|
||||
}, async ({ input: address }) => {
|
||||
// Use viem for RPC calls
|
||||
const name = await client.readContract({
|
||||
address: address as `0x${string}`,
|
||||
abi: ERC20_ABI,
|
||||
functionName: "name",
|
||||
});
|
||||
return name;
|
||||
});
|
||||
|
||||
// In handler
|
||||
const name = await context.effect(getTokenName, address);
|
||||
```
|
||||
|
||||
## Common Migration Issues
|
||||
|
||||
### Missing async/await
|
||||
|
||||
```typescript
|
||||
// WRONG - returns {} instead of entity
|
||||
const token = context.Token.get(id);
|
||||
|
||||
// CORRECT
|
||||
const token = await context.Token.get(id);
|
||||
```
|
||||
|
||||
### Field Selection for Transaction Data
|
||||
|
||||
```yaml
|
||||
# Add to config.yaml for event.transaction.hash access
|
||||
events:
|
||||
- event: Transfer(...)
|
||||
field_selection:
|
||||
transaction_fields:
|
||||
- hash
|
||||
```
|
||||
|
||||
### Multichain ID Prefixes
|
||||
|
||||
```typescript
|
||||
// Always prefix with chainId for multichain
|
||||
const id = `${event.chainId}-${originalId}`;
|
||||
```
|
||||
|
||||
### BigDecimal Precision
|
||||
|
||||
Maintain precision from original subgraph:
|
||||
|
||||
```typescript
|
||||
import { BigDecimal } from "generated";
|
||||
|
||||
const ZERO_BD = new BigDecimal(0);
|
||||
const ONE_BD = new BigDecimal(1);
|
||||
|
||||
// Don't simplify to JavaScript numbers
|
||||
```
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
- [ ] Convert subgraph.yaml to config.yaml
|
||||
- [ ] Remove @entity decorators from schema
|
||||
- [ ] Change Bytes! to String!
|
||||
- [ ] Use _id suffix for relationships
|
||||
- [ ] Add @derivedFrom to all entity arrays
|
||||
- [ ] Add async to all handlers
|
||||
- [ ] Add await to all context.Entity.get() calls
|
||||
- [ ] Use spread operator for entity updates
|
||||
- [ ] Replace Template.create() with contractRegister
|
||||
- [ ] Add field_selection for transaction data
|
||||
- [ ] Prefix IDs with chainId for multichain
|
||||
- [ ] Convert contract bindings to Effect API
|
||||
|
||||
## Additional Resources
|
||||
|
||||
### Reference Files
|
||||
|
||||
For detailed migration patterns:
|
||||
- **`references/migration-patterns.md`** - Complete pattern reference
|
||||
- **`references/common-mistakes.md`** - Pitfalls and solutions
|
||||
|
||||
### External Resources
|
||||
|
||||
- Full docs: https://docs.envio.dev/docs/HyperIndex-LLM/hyperindex-complete
|
||||
- Migration guide: https://docs.envio.dev/docs/migration-guide
|
||||
- Example indexers:
|
||||
- Uniswap v4: https://github.com/enviodev/uniswap-v4-indexer
|
||||
- Safe: https://github.com/enviodev/safe-analysis-indexer
|
||||
372
skills/subgraph-migration/references/common-mistakes.md
Normal file
372
skills/subgraph-migration/references/common-mistakes.md
Normal 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)
|
||||
648
skills/subgraph-migration/references/migration-patterns.md
Normal file
648
skills/subgraph-migration/references/migration-patterns.md
Normal 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,
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user