Initial commit
This commit is contained in:
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