
Overview of Advanced Subgraph Development
Advanced subgraph development involves optimizing and extending subgraphs to efficiently index and query blockchain data. Key areas typically include:
- Complex Data Relationships:
- Use entity relationships (e.g., one-to-many, many-to-many) to model sophisticated data structures.
- Example: Mapping a User entity to multiple Transaction entities using @derivedFrom for reverse lookups.
- Optimize mappings to handle nested queries efficiently.
- Event Handling and Performance:
- Process high-volume events (e.g., ERC-20 transfers) with batch processing in mapping files.
- Use context or global state to reduce redundant calls to smart contracts.
- Example: Cache contract data in memory to minimize Contract.call() overhead.
- Dynamic Data Sources:
- Implement data source templates for contracts created dynamically (e.g., Uniswap pairs).
- Configure subgraphs to listen for factory contract events and instantiate new data sources.
- Example: Index new token pairs from a PairCreated event.
- Full-Text Search and Filtering:
- Enable @fulltext queries for searching indexed data (e.g., metadata in NFTs).
- Optimize query performance with indexed fields and pagination.
- Error Handling and Debugging:
- Use logging (graph log) to troubleshoot mapping issues.
- Handle edge cases like reverted transactions or missing data gracefully.
- Example: Check for null values before accessing entity fields.
- Subgraph Composition:
- Combine multiple subgraphs into a single endpoint for cross-protocol queries.
- Use GraphQL schema stitching to integrate data from different blockchains.
- Testing and Deployment:
- Write unit tests for mappings using tools like graph test.
- Simulate blockchain events with local Graph Node setups.
- Deploy to hosted services or decentralized networks, ensuring schema migrations are backward-compatible.
Table of Contents
Example: Optimizing a Mapping
typescript
import { Transfer } from "../generated/ERC20/ERC20";
import { User, Transaction } from "../generated/schema";
export function handleTransfer(event: Transfer): void {
let fromUser = User.load(event.params.from.toHex());
if (!fromUser) {
fromUser = new User(event.params.from.toHex());
fromUser.balance = BigInt.fromI32(0);
}
let tx = new Transaction(event.transaction.hash.toHex());
tx.from = fromUser.id;
tx.to = event.params.to.toHex();
tx.amount = event.params.value;
tx.save();
fromUser.balance = fromUser.balance.minus(event.params.value);
fromUser.save();
}
- Optimization: Batch User updates if multiple transfers occur in one block to reduce store operations.
Additional Tools
- Graph CLI: Automate schema generation and codegen (graph codegen).
- Subgraph Studio: Test and deploy subgraphs with a user-friendly interface.
- Hosted Service vs. Decentralized: Understand trade-offs (e.g., cost vs. censorship resistance).
Indexing multiple contracts
Indexing multiple contracts in a subgraph involves configuring the subgraph to track events, calls, or blocks from several smart contracts, often to aggregate related data or handle dynamic contract deployments (e.g., factory patterns). Below is a concise guide to indexing multiple contracts in The Graph, tailored to advanced subgraph development. If you have specific contracts or use cases in mind, share more details for a deeper dive.
Steps to Index Multiple Contracts
- Define Data Sources in subgraph.yaml:
- Specify each contract as a dataSource or use templates for dynamically created contracts.
- Include the contract address, ABI, and starting block for each.
- Example for static contracts: yaml
dataSources:
- kind: ethereum/contract
name: TokenA
network: mainnet
source:
address: "0x123..."
abi: ERC20
startBlock: 12345678
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Transfer
abis:
- name: ERC20
file: ./abis/ERC20.json
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
file: ./src/mapping.ts
- kind: ethereum/contract
name: TokenB
network: mainnet
source:
address: "0x456..."
abi: ERC20
startBlock: 12345678
mapping:
# Same as above, reuse mapping logic if identical
- Handle Dynamic Contracts with Templates:
- For contracts created by a factory (e.g., Uniswap pairs), use templates to instantiate data sources dynamically.
- Example for a factory pattern:yaml
dataSources:
- kind: ethereum/contract
name: Factory
network: mainnet
source:
address: "0xFactoryAddress..."
abi: Factory
startBlock: 12345678
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Pair
abis:
- name: Factory
file: ./abis/Factory.json
- name: Pair
file: ./abis/Pair.json
eventHandlers:
- event: PairCreated(indexed address,indexed address,address,uint256)
handler: handlePairCreated
file: ./src/mapping.ts
templates:
- kind: ethereum/contract
name: Pair
network: mainnet
source:
abi: Pair
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Swap
abis:
- name: Pair
file: ./abis/Pair.json
eventHandlers:
- event: Swap(indexed address,uint256,uint256)
handler: handleSwap
file: ./src/mapping.ts
- Reuse ABIs and Mappings:
- If contracts share the same ABI (e.g., ERC-20 tokens), reference the same ABI file to reduce redundancy.
- Write reusable mapping logic in src/mapping.ts to handle similar events across contracts.
- Example mapping for transfers:typescript
import { Transfer as TransferEvent } from "../generated/TokenA/ERC20"; // Reused for TokenB
import { Transfer } from "../generated/schema";
export function handleTransfer(event: TransferEvent): void {
let transfer = new Transfer(event.transaction.hash.toHex() + "-" + event.logIndex.toString());
transfer.from = event.params.from;
transfer.to = event.params.to;
transfer.amount = event.params.value;
transfer.contract = event.address.toHex(); // Track which contract
transfer.save();
}
- Dynamic Data Source Creation:
- For factory contracts, handle events that emit new contract addresses and create data sources dynamically.
- Example for PairCreated:typescript
import { PairCreated } from "../generated/Factory/Factory";
import { Pair as PairTemplate } from "../generated/templates";
export function handlePairCreated(event: PairCreated): void {
// Start indexing the new pair contract
PairTemplate.create(event.params.pair);
}
- Optimize Schema for Multiple Contracts:
- Design the GraphQL schema to support querying across contracts.
- Add fields like contract to distinguish data from different contracts.
- Example schema.graphql:graphql
type Transfer @entity {
id: ID!
from: Bytes!
to: Bytes!
amount: BigInt!
contract: Bytes! # Differentiates TokenA vs. TokenB
}
- Performance Considerations:
- Batch Processing: Process events from multiple contracts in batches to minimize store operations.
- Indexing: Use @index on fields like contract or from for faster queries.
- Start Blocks: Set appropriate startBlock values to avoid indexing unnecessary historical data.
- Context: Use dataSource.context() to pass contract-specific metadata (e.g., token symbol) to mappings.
- Testing and Deployment:
- Test mappings with graph test to ensure events from all contracts are handled correctly.
- Deploy to a local Graph Node or hosted service to verify indexing.
- Use Subgraph Studio to monitor query performance across contracts.
Example Use Case: Indexing Two ERC-20 Tokens
- Goal: Index Transfer events from TokenA and TokenB.
- subgraph.yaml: Define two data sources with the same ERC-20 ABI.
- Mapping: Reuse handleTransfer to store transfers with a contract field.
- Query Example:graphql
query {
transfers(where: { contract: "0x123..." }) {
id
from
to
amount
}
}
Common Challenges and Solutions
- Event Overlap: If contracts emit similar events, use event.address to identify the source contract.
- Scalability: For many contracts, consider subgraph composition or limit indexed events to essentials.
- Dynamic Addresses: Ensure factory events are correctly mapped to instantiate templates.
If you’re indexing specific contracts (e.g., Uniswap, Aave) or need help with a particular setup (like a factory or cross-chain contracts), let me know! I can provide a more targeted example or analyze any code/config you share. Alternatively, I can search for real-time examples or tools to streamline your setup.
Handling optional fields, BigInts, and Bytes
When indexing multiple contracts in a subgraph, handling optional fields, BigInts, and Bytes requires careful schema design, mapping logic, and type management to ensure robustness and query efficiency. Below is a focused guide on these aspects within the context of advanced subgraph development for The Graph. If you have a specific contract, event, or issue (e.g., a schema or mapping), share it for a more tailored response.
1. Handling Optional Fields
Optional fields in a subgraph schema represent data that may not always be present, such as token metadata or event parameters that are only set in certain cases.
- Schema Definition:
- Use the ! operator for required fields and omit it for optional ones.
- Example in schema.graphql:graphql
type Token @entity {
id: ID!
name: String # Optional, as some tokens may lack a name
symbol: String # Optional
totalSupply: BigInt! # Required
}
- Mapping Logic:
- Check for null or default values before assigning optional fields.
- Use contract calls to fetch optional data only when necessary to optimize performance.
- Example in src/mapping.ts:typescript
import { Address } from "@graphprotocol/graph-ts";
import { Token as TokenContract } from "../generated/TokenA/Token";
import { Token } from "../generated/schema";
export function handleTokenCreation(event: TokenCreated): void {
let token = new Token(event.params.tokenAddress.toHex());
token.totalSupply = event.params.totalSupply;
// Bind contract to fetch optional metadata
let contract = TokenContract.bind(event.params.tokenAddress);
let name = contract.try_name();
if (!name.reverted) {
token.name = name.value;
}
let symbol = contract.try_symbol();
if (!symbol.reverted) {
token.symbol = symbol.value;
}
token.save();
}
- Note: Use try_ methods for contract calls to handle cases where the call reverts (e.g., if the contract doesn’t implement name()).
- Querying Optional Fields:
- GraphQL queries gracefully handle null for optional fields.
- Example query:graphql
query {
tokens {
id
name # Returns null if not set
symbol
totalSupply
}
}
- Best Practices:
- Avoid excessive contract calls for optional fields; cache results if reused.
- Use default values (e.g., empty string) only if semantically appropriate.
- Document optional fields in the schema for clarity.
2. Handling BigInts
BigInt is used for large numbers, like token amounts or balances, to avoid JavaScript’s floating-point precision issues.
- Schema Definition:
- Define fields as BigInt for numerical data that may exceed 32-bit integers.
- Example:graphql
type Transfer @entity {
id: ID!
from: Bytes!
to: Bytes!
amount: BigInt!
block: BigInt! # For block number
}
- Mapping Logic:
- Use @graphprotocol/graph-ts utilities for BigInt operations.
- Example for a Transfer event:typescript
import { BigInt } from "@graphprotocol/graph-ts";
import { Transfer as TransferEvent } from "../generated/TokenA/Token";
import { Transfer } from "../generated/schema";
export function handleTransfer(event: TransferEvent): void {
let transfer = new Transfer(event.transaction.hash.toHex() + "-" + event.logIndex.toString());
transfer.from = event.params.from;
transfer.to = event.params.to;
transfer.amount = event.params.value; // Already a BigInt from ABI
transfer.block = BigInt.fromI32(event.block.number.toI32()); // Convert to BigInt
transfer.save();
// Update balance (example)
let fromUser = User.load(transfer.from.toHex());
if (fromUser) {
fromUser.balance = fromUser.balance.minus(transfer.amount);
fromUser.save();
}
}
- Common Operations:
- Arithmetic: BigInt.plus(), BigInt.minus(), BigInt.times(), BigInt.dividedBy().
- Conversion: BigInt.fromI32(123) or BigInt.fromString(“123”).
- Comparison: BigInt.equals(), BigInt.gt(), BigInt.lt().
- Best Practices:
- Initialize BigInt fields to avoid null errors (e.g., BigInt.zero()).
- Avoid converting BigInt to i32 unless safe, as it may truncate large values.
- Use @index on BigInt fields like block for faster sorting/filtering.
- Challenges:
- Overflow/Underflow: Ensure calculations (e.g., balance.minus(amount)) check for negative results if needed.
- Precision: Use BigDecimal for fixed-point math (e.g., token prices), but convert carefully.
3. Handling Bytes
Bytes is used for Ethereum addresses, transaction hashes, or arbitrary data, offering flexibility but requiring careful handling.
- Schema Definition:
- Use Bytes for addresses, hashes, or raw data; String for human-readable data.
- Example:graphql
type Transaction @entity {
id: ID!
txHash: Bytes!
from: Bytes!
to: Bytes!
data: Bytes # Optional raw input data
}
- Mapping Logic:
- Convert addresses and hashes to hex strings for storage or querying.
- Example for indexing multiple contracts:typescript
import { Transfer as TransferEvent } from "../generated/TokenA/Token";
import { Transaction } from "../generated/schema";
export function handleTransfer(event: TransferEvent): void {
let tx = new Transaction(event.transaction.hash.toHex());
tx.txHash = event.transaction.hash;
tx.from = event.params.from;
tx.to = event.params.to;
tx.data = event.transaction.input; // Optional raw data
tx.contract = event.address; // Bytes to track contract
tx.save();
}
- Working with Bytes:
- Conversion: Use toHex() for string IDs (e.g., address.toHex()).
- Comparison: Compare Bytes directly or as hex strings.
- Slicing: Use slice() for extracting parts of Bytes (e.g., function selectors).
- Best Practices:
- Store addresses as Bytes (not String) for consistency and efficiency.
- Use Bytes! for required fields like addresses to enforce presence.
- Validate Bytes length if expecting specific formats (e.g., 20 bytes for addresses).
- Challenges:
- Dynamic Contracts: When indexing multiple contracts, store event.address as Bytes to differentiate sources.
- Nullability: Handle cases where Bytes fields (e.g., data) may be empty (Bytes.fromI32(0)).
- Querying: Use hex string filters for Bytes fields in GraphQL queries.
Example: Combining Optional Fields, BigInts, and Bytes
For indexing Transfer events across multiple ERC-20 contracts:
- Schema (schema.graphql):graphql
type Transfer @entity {
id: ID!
from: Bytes!
to: Bytes!
amount: BigInt!
contract: Bytes!
memo: Bytes # Optional, for contracts with extra data
block: BigInt!
}
- Mapping (src/mapping.ts):typescript
import { BigInt, Bytes } from "@graphprotocol/graph-ts";
import { Transfer as TransferEvent } from "../generated/TokenA/Token";
import { Transfer } from "../generated/schema";
export function handleTransfer(event: TransferEvent): void {
let id = event.transaction.hash.toHex() + "-" + event.logIndex.toString();
let transfer = new Transfer(id);
transfer.from = event.params.from;
transfer.to = event.params.to;
transfer.amount = event.params.value;
transfer.contract = event.address;
transfer.block = BigInt.fromI32(event.block.number.toI32());
// Optional memo field (hypothetical)
if (event.params.value.gt(BigInt.fromI32(1000))) {
// Example condition for setting optional field
transfer.memo = Bytes.fromUTF8("Large transfer");
}
transfer.save();
}
- Query:graphql
query {
transfers(where: { contract: "0x123..." }) {
id
from
to
amount
memo # Null if not set
block
}
}
Tips for Multiple Contracts
- Optional Fields: Contracts may implement different optional functions (e.g., name() vs. none). Use try_ calls and check for reverts.
- BigInts: Ensure consistency in BigInt units (e.g., decimals) across contracts. Normalize if needed (e.g., amount.divDecimal(10 ** decimals)).
- Bytes: Use contract as a Bytes field to filter events by contract address. Validate addresses to avoid indexing invalid ones.
Performance Considerations
- Optional Fields: Minimize contract calls for optional data; batch where possible.
- BigInts: Use indexed BigInt fields for frequent filters (e.g., amount > 1000).
- Bytes: Index Bytes fields like from or contract for faster lookups.
If you’re dealing with specific contracts (e.g., varying decimals for BigInt, optional metadata, or complex Bytes data), I can refine the examples. Upload any schema/mapping code, or specify a scenario (e.g., “handling optional description in an NFT contract”), and I’ll dig deeper. I can also search for real-time patterns or tools to optimize your setup if needed.
Entity relationships (one-to-many, many-to-many)
In advanced subgraph development for The Graph, entity relationships like one-to-many and many-to-many are critical for modeling complex data structures from multiple contracts, enabling efficient querying of related blockchain data. Below is a concise guide on defining and implementing these relationships, focusing on indexing multiple contracts, while integrating handling of optional fields, BigInts, and Bytes as relevant. If you have a specific use case (e.g., contracts, events, or schema), share it for a more targeted response.
1. One-to-Many Relationships
A one-to-many relationship connects one entity to multiple related entities, such as a user owning multiple transactions or a contract having multiple transfers.
- Schema Definition (schema.graphql):
- Use a list type for the “many” side and a single reference for the “one” side.
- Example: One User to many Transfers:graphql
type User @entity {
id: ID! # Address as hex string
address: Bytes! # Ethereum address
balance: BigInt! # Tracks token balance
transfers: [Transfer!]! @derivedFrom(field: "from") # Reverse lookup
}
type Transfer @entity {
id: ID! # txHash-logIndex
from: User! # Reference to User entity
to: Bytes! # Recipient address
amount: BigInt!
contract: Bytes! # Contract address
memo: Bytes # Optional
block: BigInt!
}
- Notes:
- @derivedFrom creates a virtual field (transfers) populated by querying Transfer entities where from matches the User.
- balance uses BigInt for large numbers.
- contract and address use Bytes for Ethereum addresses.
- Mapping Logic (src/mapping.ts):
- Link entities by setting the reference field (e.g., from) and update related fields (e.g., balance).
- Example for a Transfer event across multiple ERC-20 contracts:typescript
import { BigInt, Bytes } from "@graphprotocol/graph-ts";
import { Transfer as TransferEvent } from "../generated/TokenA/Token";
import { User, Transfer } from "../generated/schema";
export function handleTransfer(event: TransferEvent): void {
// Load or create User
let fromUser = User.load(event.params.from.toHex());
if (!fromUser) {
fromUser = new User(event.params.from.toHex());
fromUser.address = event.params.from;
fromUser.balance = BigInt.zero();
}
// Create Transfer
let transferId = event.transaction.hash.toHex() + "-" + event.logIndex.toString();
let transfer = new Transfer(transferId);
transfer.from = fromUser.id; // Link to User
transfer.to = event.params.to;
transfer.amount = event.params.value;
transfer.contract = event.address;
transfer.block = BigInt.fromI32(event.block.number.toI32());
// Optional memo (example)
if (event.params.value.gt(BigInt.fromI32(1000))) {
transfer.memo = Bytes.fromUTF8("Large transfer");
}
// Update balance
fromUser.balance = fromUser.balance.minus(transfer.amount);
transfer.save();
fromUser.save();
}
- Querying:
- Access the “many” side via the derived field.
- Example:graphql
query {
users(where: { address: "0x123..." }) {
id
balance
transfers(first: 10, orderBy: block, orderDirection: desc) {
id
to
amount
memo
contract
}
}
}
- Best Practices:
- Use @derivedFrom for reverse lookups to avoid manual list maintenance.
- Index fields like from or block for faster queries.
- Initialize lists as empty ([]) implicitly via ! to avoid null.
2. Many-to-Many Relationships
A many-to-many relationship involves multiple entities on both sides, such as users participating in multiple pools and pools having multiple users. This requires a join entity to model the relationship.
- Schema Definition (schema.graphql):
- Create a join entity to link the two sides, with references to both.
- Example: Users and liquidity Pools (e.g., Uniswap pairs):graphql
type User @entity {
id: ID! # Address as hex string
address: Bytes!
balance: BigInt!
poolMemberships: [PoolMembership!]! @derivedFrom(field: "user")
}
type Pool @entity {
id: ID! # Pair contract address
contract: Bytes!
totalLiquidity: BigInt!
members: [PoolMembership!]! @derivedFrom(field: "pool")
}
type PoolMembership @entity {
id: ID! # userAddress-poolAddress
user: User!
pool: Pool!
stake: BigInt! # Amount staked
joinedBlock: BigInt!
memo: Bytes # Optional metadata
}
- Mapping Logic (src/mapping.ts):
- For dynamic contracts (e.g., Uniswap pairs), handle factory events to create Pools and update memberships on events like Mint (liquidity added).
- Example for a Mint event:typescript
import { BigInt, Bytes } from "@graphprotocol/graph-ts";
import { Mint as MintEvent } from "../generated/templates/Pair/Pair";
import { User, Pool, PoolMembership } from "../generated/schema";
export function handleMint(event: MintEvent): void {
// Load or create Pool
let poolId = event.address.toHex();
let pool = Pool.load(poolId);
if (!pool) {
pool = new Pool(poolId);
pool.contract = event.address;
pool.totalLiquidity = BigInt.zero();
}
pool.totalLiquidity = pool.totalLiquidity.plus(event.params.amount);
// Load or create User
let userId = event.params.sender.toHex();
let user = User.load(userId);
if (!user) {
user = new User(userId);
user.address = event.params.sender;
user.balance = BigInt.zero();
}
// Create or update PoolMembership
let membershipId = userId + "-" + poolId;
let membership = PoolMembership.load(membershipId);
if (!membership) {
membership = new PoolMembership(membershipId);
membership.user = user.id;
membership.pool = pool.id;
membership.joinedBlock = BigInt.fromI32(event.block.number.toI32());
}
membership.stake = membership.stake.plus(event.params.amount);
// Optional memo
if (event.params.amount.gt(BigInt.fromI32(1000000))) {
membership.memo = Bytes.fromUTF8("Significant stake");
}
membership.save();
pool.save();
user.save();
}
- Dynamic Pool Creation (for multiple contracts):
- Use a template in subgraph.yaml to index new pools:yaml
templates:
- kind: ethereum/contract
name: Pair
network: mainnet
source:
abi: Pair
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Pool
- PoolMembership
abis:
- name: Pair
file: ./abis/Pair.json
eventHandlers:
- event: Mint(indexed address,uint256)
handler: handleMint
file: ./src/mapping.ts
- Trigger template instantiation in a factory handler (e.g., PairCreated).
- Querying:
- Access both sides via the join entity.
- Example:graphql
query {
pools(where: { contract: "0xPoolAddress..." }) {
id
totalLiquidity
members {
user {
address
}
stake
memo
}
}
}
- Best Practices:
- Use unique IDs for join entities (e.g., userId-poolId) to avoid collisions.
- Index user and pool fields in PoolMembership for efficient filtering.
- Update aggregated fields like totalLiquidity atomically to maintain consistency.
Integrating Optional Fields, BigInts, and Bytes
- Optional Fields:
- memo in Transfer or PoolMembership is optional (Bytes without !).
- Check conditions before setting (e.g., if (amount.gt(…))).
- BigInts:
- Used for balance, amount, stake, totalLiquidity, and block.
- Initialize with BigInt.zero() and use safe arithmetic (e.g., plus, minus).
- Bytes:
- Used for address, contract, and to to store Ethereum addresses or hashes.
- Ensure proper hex conversion (e.g., toHex()) for IDs or queries.
Example: Indexing Multiple Contracts
For two ERC-20 contracts and a Uniswap factory:
- One-to-Many: User to Transfers for token movements.
- Many-to-Many: User to Pools via PoolMembership for liquidity provision.
- Mapping:
- Handle Transfer events for both tokens, linking to User.
- Handle Mint events for Uniswap pairs, creating PoolMemberships.
- Schema:graphql
type User @entity {
id: ID!
address: Bytes!
balance: BigInt!
transfers: [Transfer!]! @derivedFrom(field: "from")
poolMemberships: [PoolMembership!]! @derivedFrom(field: "user")
}
type Transfer @entity {
id: ID!
from: User!
to: Bytes!
amount: BigInt!
contract: Bytes!
memo: Bytes
block: BigInt!
}
type Pool @entity {
id: ID!
contract: Bytes!
totalLiquidity: BigInt!
members: [PoolMembership!]! @derivedFrom(field: "pool")
}
type PoolMembership @entity {
id: ID!
user: User!
pool: Pool!
stake: BigInt!
joinedBlock: BigInt!
memo: Bytes
}
Performance Considerations
- One-to-Many:
- @derivedFrom avoids manual list updates but can slow queries for large datasets. Use first and skip for pagination.
- Index reference fields (e.g., from) and sort fields (e.g., block).
- Many-to-Many:
- Join entities increase storage but simplify queries. Index user and pool.
- Batch updates to aggregated fields (e.g., totalLiquidity) in high-volume events.
- General:
- Minimize contract calls for optional fields (e.g., fetch memo only if needed).
- Use BigInt indexing for range queries (e.g., stake > 1000).
- Validate Bytes fields (e.g., ensure contract is a valid address).
Challenges and Solutions
- Multiple Contracts:
- Use contract: Bytes! to differentiate Transfers or Pools.
- Reuse mappings for similar events (e.g., Transfer across tokens).
- Dynamic Contracts:
- Templates handle new pools, but ensure factory events correctly trigger instantiation.
- Data Consistency:
- Update related entities (e.g., User.balance, Pool.totalLiquidity) in the same handler to avoid partial updates.
- Optional Fields:
- Handle missing data gracefully (e.g., null for memo).
If you’re working with specific contracts (e.g., ERC-721s with one-to-many token ownership, or a DEX with many-to-many trader-pair relationships), I can refine the schema or mappings. Share your subgraph.yaml, schema, or mapping code, or describe the contracts/events, and I’ll provide a detailed example. I can also search for real-time patterns or debug issues if needed.
Using templates for dynamic data sources
In advanced subgraph development for The Graph, templates for dynamic data sources are essential when indexing multiple contracts that are created dynamically, such as Uniswap pairs, ERC-20 tokens, or NFT collections spawned by factory contracts. Templates allow a subgraph to instantiate new data sources on-the-fly based on events (e.g., a PairCreated event). This response focuses on using templates for dynamic data sources, integrating entity relationships (one-to-many, many-to-many), optional fields, BigInts, and Bytes as relevant, tailored to indexing multiple contracts. If you have a specific factory contract, event, or schema, share details for a more targeted example.
Overview of Templates for Dynamic Data Sources
- Purpose: Templates define a reusable configuration for contracts created dynamically, avoiding the need to hardcode each contract’s address in subgraph.yaml.
- Common Use Case: Indexing contracts like Uniswap pairs, where a factory emits events with new contract addresses.
- Process:
- Define a dataSource for the factory contract to listen for creation events.
- Define a template for the dynamically created contracts.
- Use mappings to instantiate templates when creation events occur.
- Link entities (e.g., one-to-many, many-to-many) to model relationships.
Steps to Use Templates
1. Configure subgraph.yaml
- Factory Data Source: Specify the factory contract to listen for events that signal new contract creation.
- Template: Define a template for the dynamic contracts, including their ABI and event handlers.
- Example for a Uniswap-like factory creating ERC-20 pairs:yaml
dataSources:
- kind: ethereum/contract
name: Factory
network: mainnet
source:
address: "0xFactoryAddress..." # e.g., Uniswap V2 Factory
abi: Factory
startBlock: 12345678
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Pool
abis:
- name: Factory
file: ./abis/Factory.json
- name: Pair
file: ./abis/Pair.json
eventHandlers:
- event: PairCreated(indexed address,indexed address,address,uint256)
handler: handlePairCreated
file: ./src/mapping.ts
templates:
- kind: ethereum/contract
name: Pair
network: mainnet
source:
abi: Pair
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Pool
- Transfer
- PoolMembership
abis:
- name: Pair
file: ./abis/Pair.json
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
- event: Mint(indexed address,uint256)
handler: handleMint
file: ./src/mapping.ts
- Notes:
- The Factory data source listens for PairCreated to trigger new Pair instances.
- The Pair template handles events like Transfer and Mint for each instantiated pair.
- Multiple ABIs (Factory, Pair) are included to support both contracts.
2. Define Schema with Relationships
- Model entities to capture data from dynamic contracts, incorporating one-to-many and many-to-many relationships.
- Example schema.graphql:graphql
type User @entity {
id: ID! # Address as hex string
address: Bytes!
balance: BigInt!
transfers: [Transfer!]! @derivedFrom(field: "from") # One-to-many
poolMemberships: [PoolMembership!]! @derivedFrom(field: "user") # Many-to-many
}
type Pool @entity {
id: ID! # Pair contract address
contract: Bytes!
token0: Bytes!
token1: Bytes!
totalLiquidity: BigInt!
members: [PoolMembership!]! @derivedFrom(field: "pool") # Many-to-many
transfers: [Transfer!]! @derivedFrom(field: "pool") # One-to-many
}
type PoolMembership @entity {
id: ID! # userAddress-poolAddress
user: User!
pool: Pool!
stake: BigInt!
joinedBlock: BigInt!
memo: Bytes # Optional
}
type Transfer @entity {
id: ID! # txHash-logIndex
pool: Pool! # Link to dynamic pair
from: User!
to: Bytes!
amount: BigInt!
memo: Bytes # Optional
block: BigInt!
}
- Relationships:
- One-to-Many: Pool to Transfers (via @derivedFrom(field: “pool”)).
- Many-to-Many: User to Pool via PoolMembership.
- Types:
- BigInt for balance, stake, amount, totalLiquidity, block.
- Bytes for address, contract, token0, token1, to, memo.
- memo is optional (no !).
3. Implement Mappings
- Factory Handler: Instantiate templates when new contracts are created.
- Template Handlers: Process events from dynamic contracts, linking entities.
- Example src/mapping.ts:typescript
import { BigInt, Bytes } from "@graphprotocol/graph-ts";
import { PairCreated } from "../generated/Factory/Factory";
import { Transfer as TransferEvent, Mint as MintEvent } from "../generated/templates/Pair/Pair";
import { Pair as PairTemplate } from "../generated/templates";
import { User, Pool, Transfer, PoolMembership } from "../generated/schema";
// Handle new pair creation
export function handlePairCreated(event: PairCreated): void {
let poolId = event.params.pair.toHex();
let pool = new Pool(poolId);
pool.contract = event.params.pair;
pool.token0 = event.params.token0;
pool.token1 = event.params.token1;
pool.totalLiquidity = BigInt.zero();
pool.save();
// Instantiate template for new pair
PairTemplate.create(event.params.pair);
}
// Handle transfers in pairs
export function handleTransfer(event: TransferEvent): void {
let poolId = event.address.toHex();
let pool = Pool.load(poolId);
if (!pool) return; // Safety check
// Load or create User
let fromUserId = event.params.from.toHex();
let fromUser = User.load(fromUserId);
if (!fromUser) {
fromUser = new User(fromUserId);
fromUser.address = event.params.from;
fromUser.balance = BigInt.zero();
}
// Create Transfer
let transferId = event.transaction.hash.toHex() + "-" + event.logIndex.toString();
let transfer = new Transfer(transferId);
transfer.pool = poolId; // Link to Pool
transfer.from = fromUserId; // Link to User
transfer.to = event.params.to;
transfer.amount = event.params.value;
transfer.block = BigInt.fromI32(event.block.number.toI32());
// Optional memo
if (event.params.value.gt(BigInt.fromI32(1000))) {
transfer.memo = Bytes.fromUTF8("Large transfer");
}
// Update balance
fromUser.balance = fromUser.balance.minus(transfer.amount);
transfer.save();
fromUser.save();
}
// Handle liquidity addition
export function handleMint(event: MintEvent): void {
let poolId = event.address.toHex();
let pool = Pool.load(poolId);
if (!pool) return;
// Update total liquidity
pool.totalLiquidity = pool.totalLiquidity.plus(event.params.amount);
// Load or create User
let userId = event.params.sender.toHex();
let user = User.load(userId);
if (!user) {
user = new User(userId);
user.address = event.params.sender;
user.balance = BigInt.zero();
}
// Create or update PoolMembership (many-to-many)
let membershipId = userId + "-" + poolId;
let membership = PoolMembership.load(membershipId);
if (!membership) {
membership = new PoolMembership(membershipId);
membership.user = user.id;
membership.pool = pool.id;
membership.joinedBlock = BigInt.fromI32(event.block.number.toI32());
membership.stake = BigInt.zero();
}
membership.stake = membership.stake.plus(event.params.amount);
// Optional memo
if (event.params.amount.gt(BigInt.fromI32(1000000))) {
membership.memo = Bytes.fromUTF8("Significant stake");
}
membership.save();
pool.save();
user.save();
}
4. Instantiate Templates
- The PairTemplate.create(event.params.pair) call in handlePairCreated starts indexing the new contract using the Pair template.
- The Graph Node automatically applies the template’s event handlers (e.g., handleTransfer, handleMint) to the new contract address.
5. Querying
- Query dynamic contracts and their relationships.
- Example:graphql
query {
pools(where: { token0: "0xTokenAddress..." }) {
id
contract
totalLiquidity
transfers(first: 10, orderBy: block, orderDirection: desc) {
from {
address
}
to
amount
memo
}
members {
user {
address
}
stake
memo
}
}
}
Integrating Key Concepts
- Entity Relationships:
- One-to-Many: Pool to Transfers via @derivedFrom(field: “pool”).
- Many-to-Many: User to Pool via PoolMembership.
- Optional Fields:
- memo in Transfer and PoolMembership is set conditionally (e.g., for large amounts).
- BigInts:
- Used for balance, amount, stake, totalLiquidity, block.
- Initialized with BigInt.zero() and updated with safe methods (e.g., plus, minus).
- Bytes:
- Used for address, contract, token0, token1, to, memo.
- Converted to hex for IDs (e.g., toHex()).
- Multiple Contracts:
- Templates handle dynamic pair contracts.
- contract: Bytes! in Pool and Transfer distinguishes sources.
Performance Considerations
- Template Instantiation:
- Ensure create is called only once per contract address to avoid duplicates.
- Use Pool.load() to verify existence before creating entities.
- Relationships:
- Index pool, from, and user fields for faster lookups.
- Use pagination (first, skip) for large @derivedFrom lists.
- Data Efficiency:
- Batch updates to totalLiquidity or balance in high-volume events.
- Minimize contract calls for optional fields (e.g., avoid fetching metadata unless needed).
- Indexing:
- Index BigInt fields like block or amount for range queries.
- Index Bytes fields like contract or token0 for filtering.
Common Challenges and Solutions
- Missing Templates:
- Verify PairTemplate.create uses the correct address from the event.
- Check subgraph.yaml for matching template names and ABIs.
- Event Overlap:
- Use event.address to distinguish events from different pairs.
- Scalability:
- For many dynamic contracts, limit indexed events or use subgraph composition.
- Consistency:
- Update related entities (e.g., Pool.totalLiquidity, User.balance) in the same handler.
- Optional Fields:
- Handle null for memo gracefully in queries and mappings.
Example Use Case: Uniswap V2 Pairs
- Factory: Listens for PairCreated(token0, token1, pair, index).
- Pairs: Each pair emits Transfer and Mint events.
- Entities:
- Pool for each pair, tracking totalLiquidity and tokens.
- Transfer for LP token movements, linked to Pool and User.
- PoolMembership for users staking liquidity, linking User and Pool.
- Outcome: Query all transfers and liquidity providers for any pair dynamically.
If you’re working with a specific factory (e.g., Aave, OpenZeppelin Clone, or an NFT minting contract), I can customize the schema, mappings, or subgraph.yaml. Share your contract details, ABI, or existing code, and I’ll refine the setup. I can also search for real-time examples or debug issues like missing events or slow indexing. Let me know what you need!
Example: NFT marketplace with dynamic collections
Let’s create an example of a subgraph for an NFT marketplace with dynamic collections, where a factory contract spawns new NFT collections (e.g., ERC-721 contracts), and we need to index tokens, transfers, and user interactions across these collections. This will use templates for dynamic data sources, incorporate entity relationships (one-to-many, many-to-many), and handle optional fields, BigInts, and Bytes, tailored to advanced subgraph development for The Graph. The example assumes a marketplace similar to OpenSea or Rarible, where collections are created dynamically, and NFTs are minted and transferred.
Scenario
- Factory Contract: Deploys new ERC-721 collections (e.g., CollectionCreated(address collection)).
- Collection Contracts: Each collection emits Transfer events for mints and transfers, and optionally supports metadata (e.g., name, symbol).
- Marketplace Interactions: Users buy/sell NFTs, creating ownership and transfer records.
- Goals:
- Index all collections dynamically.
- Track NFTs, their owners, and transfer history.
- Model relationships (e.g., one user to many NFTs, one collection to many NFTs).
- Handle optional metadata and token IDs (BigInts).
1. Subgraph Configuration (subgraph.yaml)
We define a data source for the factory and a template for dynamic NFT collections.
yaml
dataSources:
- kind: ethereum/contract
name: Factory
network: mainnet
source:
address: "0xFactoryAddress..." # Replace with actual factory address
abi: Factory
startBlock: 12345678
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Collection
abis:
- name: Factory
file: ./abis/Factory.json
- name: ERC721
file: ./abis/ERC721.json
eventHandlers:
- event: CollectionCreated(address)
handler: handleCollectionCreated
file: ./src/mapping.ts
templates:
- kind: ethereum/contract
name: Collection
network: mainnet
source:
abi: ERC721
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- NFT
- Transfer
- User
abis:
- name: ERC721
file: ./abis/ERC721.json
eventHandlers:
- event: Transfer(indexed address,indexed address,indexed uint256)
handler: handleTransfer
file: ./src/mapping.ts
- Factory: Listens for CollectionCreated to instantiate new collections.
- Template: Defines how each Collection (ERC-721 contract) handles Transfer events.
- ABIs:
- Factory.json: Includes CollectionCreated(address).
- ERC721.json: Standard ERC-721 ABI with Transfer(from, to, tokenId) and optional name()/symbol().
2. Schema Definition (schema.graphql)
We model entities to capture collections, NFTs, users, and transfers, with relationships and appropriate types.
graphql
type User @entity {
id: ID! # Address as hex string
address: Bytes!
ownedNFTs: [NFT!]! @derivedFrom(field: "owner") # One-to-many
transfers: [Transfer!]! @derivedFrom(field: "from") # One-to-many
}
type Collection @entity {
id: ID! # Collection contract address
contract: Bytes!
name: String # Optional
symbol: String # Optional
nfts: [NFT!]! @derivedFrom(field: "collection") # One-to-many
totalSupply: BigInt! # Tracks number of NFTs
}
type NFT @entity {
id: ID! # collectionAddress-tokenId
collection: Collection! # One-to-many (reverse)
tokenId: BigInt!
owner: User! # One-to-many (reverse)
metadataURI: String # Optional
transfers: [Transfer!]! @derivedFrom(field: "nft") # One-to-many
}
type Transfer @entity {
id: ID! # txHash-logIndex
nft: NFT!
from: User!
to: Bytes!
block: BigInt!
timestamp: BigInt!
memo: Bytes # Optional, e.g., for marketplace notes
}
- Relationships:
- One-to-Many:
- Collection to NFTs (one collection has many NFTs).
- User to NFTs (one user owns many NFTs).
- NFT to Transfers (one NFT has many transfers).
- User to Transfers (one user initiates many transfers).
- Many-to-Many: Not explicitly modeled here, but could be added (e.g., users bidding on multiple NFTs via a Bid entity).
- One-to-Many:
- Types:
- BigInt: tokenId, totalSupply, block, timestamp.
- Bytes: address, contract, to, memo.
- Optional Fields: name, symbol, metadataURI, memo.
3. Mappings (src/mapping.ts)
We implement handlers for the factory’s CollectionCreated event and the collections’ Transfer events, linking entities and handling dynamic data sources.
typescript
import { BigInt, Bytes, Address } from "@graphprotocol/graph-ts";
import { CollectionCreated } from "../generated/Factory/Factory";
import { Transfer as TransferEvent } from "../generated/templates/Collection/ERC721";
import { Collection as CollectionTemplate } from "../generated/templates";
import { Collection, NFT, User, Transfer } from "../generated/schema";
import { ERC721 } from "../generated/templates/Collection/ERC721";
// Handle new collection creation
export function handleCollectionCreated(event: CollectionCreated): void {
let collectionId = event.params.collection.toHex();
let collection = new Collection(collectionId);
collection.contract = event.params.collection;
collection.totalSupply = BigInt.zero();
// Fetch optional metadata
let contract = ERC721.bind(event.params.collection);
let name = contract.try_name();
if (!name.reverted) {
collection.name = name.value;
}
let symbol = contract.try_symbol();
if (!symbol.reverted) {
collection.symbol = symbol.value;
}
collection.save();
// Instantiate template for new collection
CollectionTemplate.create(event.params.collection);
}
// Handle NFT transfers (including mints)
export function handleTransfer(event: TransferEvent): void {
let collectionId = event.address.toHex();
let collection = Collection.load(collectionId);
if (!collection) return; // Safety check
// Load or create User (from)
let fromUserId = event.params.from.toHex();
let fromUser = User.load(fromUserId);
if (!fromUser) {
fromUser = new User(fromUserId);
fromUser.address = event.params.from;
}
// Load or create User (to)
let toUserId = event.params.to.toHex();
let toUser = User.load(toUserId);
if (!toUser) {
toUser = new User(toUserId);
toUser.address = event.params.to;
}
// Load or create NFT
let nftId = collectionId + "-" + event.params.tokenId.toString();
let nft = NFT.load(nftId);
if (!nft) {
nft = new NFT(nftId);
nft.collection = collectionId;
nft.tokenId = event.params.tokenId;
collection.totalSupply = collection.totalSupply.plus(BigInt.fromI32(1));
// Fetch optional metadataURI
let contract = ERC721.bind(event.address);
let uri = contract.try_tokenURI(event.params.tokenId);
if (!uri.reverted) {
nft.metadataURI = uri.value;
}
}
// Update NFT owner
nft.owner = toUserId;
// Create Transfer
let transferId = event.transaction.hash.toHex() + "-" + event.logIndex.toString();
let transfer = new Transfer(transferId);
transfer.nft = nftId;
transfer.from = fromUserId;
transfer.to = event.params.to;
transfer.block = BigInt.fromI32(event.block.number.toI32());
transfer.timestamp = event.block.timestamp;
// Optional memo (example: mark mints or high-value transfers)
if (event.params.from.equals(Address.zero())) {
transfer.memo = Bytes.fromUTF8("Mint");
} else if (event.transaction.value.gt(BigInt.fromI32(1000000))) {
transfer.memo = Bytes.fromUTF8("High-value transfer");
}
// Save entities
transfer.save();
nft.save();
fromUser.save();
toUser.save();
collection.save();
}
- Factory Handler:
- Creates a Collection entity for each new ERC-721 contract.
- Fetches optional name and symbol using try_ to handle non-compliant contracts.
- Calls CollectionTemplate.create to start indexing the new collection.
- Transfer Handler:
- Handles mints (from == 0x0) and transfers.
- Creates or updates NFT, User, and Transfer entities.
- Links relationships: NFT.collection, NFT.owner, Transfer.nft, Transfer.from.
- Updates Collection.totalSupply for mints.
- Sets optional metadataURI and memo conditionally.
- Types:
- BigInt: tokenId, totalSupply, block, timestamp.
- Bytes: contract, address, to, memo.
- Hex strings for IDs (e.g., toHex()).
4. Querying
Query the subgraph to retrieve collections, NFTs, owners, and transfers.
graphql
query {
collections(where: { name_contains: "Art" }) {
id
contract
name
symbol
totalSupply
nfts(first: 10) {
tokenId
owner {
address
}
metadataURI
transfers(orderBy: timestamp, orderDirection: desc) {
from {
address
}
to
memo
timestamp
}
}
}
users(where: { ownedNFTs_some: { collection: "0xCollectionAddress..." } }) {
id
address
ownedNFTs {
tokenId
collection {
name
}
}
}
}
- Use Cases:
- List all NFTs in a collection with their owners and transfer history.
- Find users who own NFTs from a specific collection.
- Filter transfers by memo (e.g., mints).
5. Integrating Key Concepts
- Dynamic Data Sources:
- CollectionTemplate.create instantiates new ERC-721 contracts based on CollectionCreated.
- Each collection indexes Transfer events independently.
- Entity Relationships:
- One-to-Many:
- Collection to NFTs (nfts via @derivedFrom(field: “collection”)).
- User to NFTs (ownedNFTs via @derivedFrom(field: “owner”)).
- NFT to Transfers (transfers via @derivedFrom(field: “nft”)).
- User to Transfers (transfers via @derivedFrom(field: “from”)).
- Many-to-Many (Optional Extension):
- Could add a Bid entity to track users bidding on multiple NFTs:graphql
- One-to-Many:
type Bid @entity {
id: ID! # userAddress-nftId
user: User!
nft: NFT!
amount: BigInt!
memo: Bytes
}
- Optional Fields:
- name, symbol (collection metadata, fetched via try_).
- metadataURI (NFT metadata, fetched via try_tokenURI).
- memo (set for mints or high-value transfers).
- BigInts:
- tokenId, totalSupply, block, timestamp.
- Initialized with BigInt.zero() for totalSupply.
- Safe arithmetic (e.g., plus(BigInt.fromI32(1))).
- Bytes:
- contract, address, to, memo.
- Validated (e.g., check from == Address.zero() for mints).
- Converted to hex for IDs (e.g., collectionId).
6. Performance Considerations
- Dynamic Sources:
- Ensure CollectionTemplate.create is called only once per address (handled by unique collectionId).
- Verify factory events are emitted correctly to avoid missing collections.
- Relationships:
- Index collection, owner, nft, and from fields for faster queries.
- Use first and orderBy for paginated @derivedFrom lists (e.g., nfts(first: 10)).
- Data Efficiency:
- Cache contract bindings (e.g., ERC721.bind) within handlers to reduce calls.
- Fetch optional fields (name, metadataURI) only once per entity.
- Indexing:
- Index BigInt fields (tokenId, timestamp) for sorting/filtering.
- Index Bytes fields (contract, address) for exact matches.
- Storage:
- Combine collectionId and tokenId for unique NFT IDs to avoid collisions.
7. Example Workflow
- Factory Event: CollectionCreated(0xNewCollection).
- Creates Collection entity with id: “0xNewCollection”.
- Fetches name/symbol if available.
- Starts indexing 0xNewCollection via template.
- Mint Event: Transfer(0x0, user1, tokenId: 1).
- Creates NFT with id: “0xNewCollection-1”.
- Links to Collection and User (owner).
- Sets metadataURI if available.
- Creates Transfer with memo: “Mint”.
- Increments Collection.totalSupply.
- Transfer Event: Transfer(user1, user2, tokenId: 1).
- Updates NFT.owner to user2.
- Creates new Transfer with optional memo for high-value sales.
- Query:
- Retrieve all NFTs in 0xNewCollection, their owners, and transfer history.
8. Common Challenges and Solutions
- Non-Standard Contracts:
- Some ERC-721s lack name/symbol or tokenURI. Use try_ to handle reverts.
- Solution: Set optional fields to null if unavailable.
- High-Volume Mints:
- Batch mints may emit many Transfer events.
- Solution: Optimize handleTransfer with minimal store operations (e.g., reuse loaded entities).
- Unique IDs:
- Risk of ID collisions for NFTs across collections.
- Solution: Use collectionId-tokenId as NFT.id.
- Missing Events:
- Factory might emit malformed events.
- Solution: Add logging (log.info) and validate event.params.collection.
- Scalability:
- Many collections can slow indexing.
- Solution: Limit indexed events (e.g., only Transfer) or use subgraph composition.
9. Extending the Example
To add marketplace features:
- Sales:
- Index Sale events (e.g., Sale(nft, buyer, price)).
- Add Sale entity:graphql
type Sale @entity {
id: ID!
nft: NFT!
buyer: User!
seller: User!
price: BigInt!
block: BigInt!
}
- Bids (Many-to-Many):
- Model users bidding on NFTs:graphql
type Bid @entity {
id: ID! # userAddress-nftId
user: User!
nft: NFT!
amount: BigInt!
memo: Bytes
}
- Update User and NFT:graphql
type User @entity {
# ... existing fields
bids: [Bid!]! @derivedFrom(field: "user")
}
type NFT @entity {
# ... existing fields
bids: [Bid!]! @derivedFrom(field: "nft")
}
10. Testing and Deployment
- Testing:
- Use graph test to simulate CollectionCreated and Transfer events.
- Mock contract calls for name, symbol, tokenURI to test optional fields.
- Local Deployment:
- Run a local Graph Node and deploy with graph deploy –node http://localhost:8020/.
- Verify indexing with graph logs.
- Hosted Service:
- Deploy to Subgraph Studio or The Graph’s hosted service.
- Monitor query performance for large collections.
- Query Optimization:
- Add @index to tokenId, block, contract for faster filters.
- Use pagination for nfts and transfers.
This example provides a robust foundation for an NFT marketplace subgraph with dynamic collections. If you have a specific factory ABI, contract address, or additional events (e.g., sales, auctions), I can refine the schema or mappings. Share your subgraph.yaml, schema, or code snippets, or describe further requirements (e.g., “add bidding logic” or “handle ERC-1155”), and I’ll customize it. I can also search for real-time NFT marketplace patterns or debug issues like slow indexing or missing templates. Let me know!