
Hey, it looks like you’re referencing a specific chapter, maybe from a book or guide on subgraphs, possibly related to blockchain, GraphQL, or The Graph protocol. Since I don’t have direct access to that exact chapter, I’ll assume you’re asking about general concepts related to testing, debugging, and optimizing subgraphs, which are indexed datasets used in decentralized applications. If you meant something else or have a specific question, please clarify!
Table of Contents
Testing Subgraphs
Testing ensures a subgraph correctly indexes blockchain data and serves accurate queries. Key practices include:
- Unit Tests: Write tests for mapping functions (e.g., in AssemblyScript or TypeScript) to verify logic for event handling, like processing a smart contract’s Transfer event.
- Integration Tests: Deploy the subgraph to a local or testnet node (e.g., Hardhat or Ganache) and query it with GraphQL to check data consistency.
- Mock Data: Use mock blockchain events to simulate real-world scenarios, ensuring edge cases are covered.
- Tools: Leverage tools like graph-cli for deployment and jest or mocha for running tests.
Debugging Subgraphs
Debugging involves identifying and fixing issues in subgraph indexing or query results. Steps include:
- Logs: Check indexing logs in the Graph Node or hosted service (e.g., Subgraph Studio) for errors like mapping failures or schema mismatches.
- Event Inspection: Verify that blockchain events are correctly captured by mappings. Use tools like Etherscan to cross-check raw events.
- Query Validation: Run GraphQL queries against the subgraph and compare results with expected outputs. Tools like GraphiQL help.
- Breakpoints: If using local development, set breakpoints in mappings to step through logic (supported in some IDEs with AssemblyScript plugins).
- Common Issues: Look for schema typos, incorrect block numbers, or missing entity fields.
Optimizing Subgraphs
Optimization improves indexing speed and query performance. Strategies include:
- Efficient Mappings: Minimize computation in mappings. Avoid redundant data storage or excessive loops.
- Indexing Filters: Use event filters in the subgraph manifest (subgraph.yaml) to index only relevant contracts or events.
- Entity Design: Normalize entities to reduce redundancy (e.g., separate User and Transaction entities instead of duplicating user data).
- Batching: Process multiple events in a single handler when possible to reduce database writes.
- Query Optimization: Design GraphQL schemas to support efficient queries, like adding indexes for frequently filtered fields (e.g., createdAt timestamps).
- Scaling: For large datasets, consider pagination in queries and optimize for Graph Node’s performance limits.
If you’re working with The Graph, tools like Subgraph Studio, Graph Explorer, or local Graph Node setups are critical for these tasks. For deeper dives, I can search for specific resources or analyze related content if you share more details (e.g., a code snippet, error message, or specific optimization goal). Want to focus on any of these aspects, or is there a particular problem you’re tackling?
Debugging mappings and handlers
Debugging mappings and handlers in subgraphs (e.g., for The Graph protocol) involves pinpointing issues in the logic that processes blockchain events and updates entities. Mappings, typically written in AssemblyScript or TypeScript, define how events or function calls from smart contracts are handled to populate the subgraph’s data store. Here’s a focused guide to debugging them effectively:
1. Inspect Logs
- Where to Look: Check the indexing logs in your Graph Node (local setup) or hosted service (e.g., Subgraph Studio, Graph Explorer).
- What to Find: Look for errors like:
- TypeError or NullReferenceError (common in AssemblyScript for undefined variables or null accesses).
- Mapping aborts due to unhandled event types or schema mismatches.
- Failed assertions in the handler logic.
- Tip: Enable verbose logging in the Graph Node config (graph log-level debug) to see detailed execution traces.
2. Validate Event Data
- Cross-Check Events: Ensure the handler is receiving the correct event data from the blockchain.
- Use tools like Etherscan or a blockchain explorer to verify the raw event logs (e.g., Transfer event parameters like from, to, value).
- Compare with the event object in your mapping (e.g., event.params in AssemblyScript).
- Common Issues:
- Incorrect event decoding (e.g., wrong parameter types in subgraph.yaml).
- Missing events due to incorrect contract addresses or ABI definitions.
- Fix: Update the manifest (subgraph.yaml) to match the contract’s ABI and redeploy.
3. Trace Handler Logic
- Manual Logging: Add temporary log.info() or console.log() statements in your mappings to print variable values or confirm execution flow.
- Example in AssemblyScript: typescript
import { log } from 'graph-ts';
export function handleTransfer(event: Transfer): void {
log.info('Processing Transfer from {} to {}', [
event.params.from.toHex(),
event.params.to.toHex()
]);
// Your logic here
}
- Local Debugging: If running a local Graph Node, use an IDE with AssemblyScript support (e.g., VS Code with plugins) to set breakpoints and step through handlers.
- Tip: Test with a single event by triggering it on a testnet or local chain (e.g., Hardhat) to isolate issues.
4. Check Entity Updates
- Verify Writes: Ensure handlers are correctly updating entities in the data store.
- Query the subgraph with GraphQL after processing an event to confirm entity fields (e.g., User.balance or Transaction.timestamp).
- Example query: graphqlb
query {
users(first: 1) {
id
balance
}
}
- Common Issues:
- Forgetting to call entity.save() after modifying an entity.
- Overwriting fields unintentionally (e.g., resetting a counter).
- Incorrect entity IDs (e.g., using event.transaction.hash vs. a unique event ID).
- Fix: Double-check load() and save() calls and ensure IDs are unique and consistent.
5. Handle Edge Cases
- Null Checks: Always check for null or undefined values, especially when accessing event.params or loading entities.
- Example: typescript
let user = User.load(event.params.from.toHex());
if (user == null) {
user = new User(event.params.from.toHex());
user.balance = BigInt.fromI32(0);
}
- Reorgs: Account for blockchain reorganizations by ensuring handlers are idempotent (same input produces same output).
- Gas Limits: Avoid complex computations in handlers to prevent indexing slowdowns.
6. Use Debugging Tools
- Graph CLI: Run graph deploy –debug to catch manifest or schema errors before deployment.
- GraphiQL/Playground: Test queries against your subgraph endpoint to verify handler outputs.
- Subgraph Studio: Use the dashboard to monitor indexing status and error messages.
- Local Node: Spin up a local Graph Node with a test chain to debug in a controlled environment (e.g., docker-compose setup with Hardhat).
7. Reproduce and Test
- Reproduce Errors: Trigger the problematic event or call on a testnet/local chain to replicate the issue.
- Unit Tests: Write tests for handlers using frameworks like jest or matchstick (The Graph’s testing tool).
- Example with Matchstick: typescript
import { test, assert } from 'matchstick-as';
import { handleTransfer } from '../src/mapping';
import { createTransferEvent } from './utils';
test('Handle Transfer updates user balance', () => {
let event = createTransferEvent('0x123...', '0x456...', 100);
handleTransfer(event);
let user = User.load('0x456...');
assert.fieldEquals('User', '0x456...', 'balance', '100');
});
- Iterate: Fix, redeploy, and retest until the handler behaves as expected.
Example Debugging Workflow
Suppose your handleTransfer function isn’t updating a User.balance correctly:
- Check logs for errors (e.g., NullReferenceError).
- Verify the Transfer event data matches subgraph.yaml and ABI.
- Add log.info() to trace event.params.value and User.balance.
- Query the subgraph to confirm User entity state.
- Write a unit test to simulate the event and assert the balance update.
- Fix the handler (e.g., add missing entity.save() or null check).
- Redeploy and validate with a new event.
If you’re hitting a specific error or bug (e.g., a code snippet or log message), share it, and I can tailor the advice further. Want to dig into a particular issue or see an example for a specific event handler?
Improving query performance
Improving query performance for subgraphs (e.g., in The Graph protocol) is critical for ensuring fast, efficient data retrieval in decentralized applications. Since you’re focused on debugging mappings and handlers from Chapter , I’ll tie this to optimizing GraphQL queries while considering the impact of mappings. Below are key strategies to enhance query performance, assuming a subgraph indexing blockchain data with entities like User, Transaction, or Token.
1. Optimize GraphQL Schema Design
- Minimize Fields: Only include necessary fields in entities to reduce database overhead. Avoid overly complex nested structures unless required.
- Example: Instead of storing redundant computed fields (e.g., totalBalance in User), calculate them in queries or frontend logic.
- Use IDs Wisely: Ensure entity IDs are unique and indexed (e.g., id: ID! in schema). Use deterministic IDs in mappings (e.g., event.transaction.hash.toHex() + “-” + event.logIndex.toString() for unique Transaction IDs).
- Relations: Define clear @derivedFrom relationships to avoid manual joins.
- Example: For a User with many Transactions: graphql
type User @entity {
id: ID!
transactions: [Transaction!]! @derivedFrom(field: "user")
}
type Transaction @entity {
id: ID!
user: User!
}
- This lets you query user.transactions efficiently without manual lookups.
2. Efficient Query Writing
- Select Only Needed Fields: Avoid overfetching by specifying exact fields in queries.
- Inefficient: graphql
query {
users(first: 100) {
id
balance
transactions { id, amount, timestamp }
}
}
- Efficient: graphql
query {
users(first: 100) {
id
balance
}
}
- Pagination: Use first, skip, or where to limit results.
- Example: graphql
query {
transactions(first: 10, where: { timestamp_gt: "2025-01-01" }, orderBy: timestamp, orderDirection: desc) {
id
amount
}
}
- Filter Early: Apply where clauses to reduce the dataset before sorting or grouping.
- Example: Filter by user to avoid scanning all Transactions: graphql
query {
transactions(where: { user: "0x123..." }) {
id
amount
}
}
3. Indexing for Query Performance
- Add Indexes: In the schema, mark frequently filtered or sorted fields for indexing.
- Example: If querying Transactions by timestamp often, ensure the schema supports it: graphql
type Transaction @entity {
id: ID!
user: User!
amount: BigInt!
timestamp: BigInt! @index
}
- In mappings, ensure timestamp is set efficiently (e.g., event.block.timestamp).
- Avoid Over-Indexing: Too many indexes slow down writes during indexing. Only index fields used in where, orderBy, or joins.
- Check Mappings: Ensure handlers don’t overwrite indexed fields unnecessarily, as this can trigger reindexing.
4. Optimize Mappings for Query Support
- Minimize Entity Updates: In handlers, avoid redundant entity.save() calls, as each write impacts indexing speed and indirectly slows queries.
- Example: Instead of: typescript
export function handleTransfer(event: Transfer): void {
let user = User.load(event.params.to.toHex());
if (user == null) {
user = new User(event.params.to.toHex());
}
user.balance = user.balance.plus(event.params.value);
user.save();
// Avoid redundant saves elsewhere
}
- Consolidate updates before saving.
- Precompute Data: Store frequently queried values in entities to avoid runtime calculations.
- Example: Store user.transactionCount in User and increment it in the handler: typescript
user.transactionCount = user.transactionCount.plus(BigInt.fromI32(1));
- This avoids querying user.transactions.length repeatedly.
- Batch Processing: Process multiple events in a handler to reduce database operations, improving indexing speed and query readiness.
5. Leverage Caching
- Frontend Caching: Use libraries like Apollo Client or URQL to cache query results on the client side for repeated queries.
- CDN/Edge Caching: If using a hosted service (e.g., The Graph’s Hosted Service or Decentralized Network), configure API gateways to cache common queries.
- Subgraph-Level: Ensure mappings produce stable, predictable entity data to maximize cache hits (e.g., consistent IDs and field values).
6. Monitor and Profile Queries
- GraphiQL/Playground: Test queries in the subgraph’s query interface to measure response times. Identify slow queries by experimenting with filters and fields.
- Subgraph Studio: Use the dashboard to check query performance metrics and indexing status. Slow indexing can bottleneck queries.
- Graph Node Logs: Check for query execution bottlenecks (e.g., slow SQL operations in the underlying PostgreSQL database).
- Example: If a query like transactions(orderBy: amount) is slow, verify that amount is indexed and the handler sets it efficiently.
7. Handle Large Datasets
- Pagination Limits: Enforce reasonable first values (e.g., 100) to avoid overwhelming the Graph Node.
- Chunked Queries: For large datasets, query in smaller chunks using skip or id_gt filters.
- Example: graphql
query {
transactions(first: 100, where: { id_gt: "lastProcessedId" }) {
id
amount
}
}
- Aggregate in Mappings: Store aggregated data (e.g., totalVolume in Token) to avoid complex queries like:
- Inefficient: graphql
query {
transactions {
amount
}
}
- Precompute in handler: typescript
token.totalVolume = token.totalVolume.plus(event.params.value);
token.save();
Example: Optimizing a Slow Query
Suppose this query is slow:
graphql
query {
users(first: 100, orderBy: balance, orderDirection: desc) {
id
balance
transactions(first: 1000) {
id
amount
}
}
- Issues:
- Fetching 1000 transactions per user is excessive.
- Sorting by balance may be slow if not indexed.
- Nested transactions query increases complexity.
- Fixes:
- Limit nested results: graphql
users(first: 100, orderBy: balance, orderDirection: desc) {
id
balance
transactions(first: 10) {
id
amount
}
}
- Ensure balance is indexed in the schema: graphql
type User @entity {
id: ID!
balance: BigInt! @index
transactions: [Transaction!]! @derivedFrom(field: "user")
}
- In mappings, update balance efficiently: typescript
let user = User.load(event.params.to.toHex());
if (user == null) {
user = new User(event.params.to.toHex());
user.balance = BigInt.fromI32(0);
}
user.balance = user.balance.plus(event.params.value);
user.save();
Debugging Tie-In
Since you’re debugging mappings, ensure handlers don’t introduce query bottlenecks:
- Log Slow Operations: Add log.info() to track time-intensive handler logic.
- Test Query Impact: After fixing a handler, run sample queries to verify performance (e.g., users(first: 10)).
- Avoid Overwrites: Ensure mappings don’t repeatedly update the same entity unnecessarily, as this slows indexing and queries.
If you have a specific query or handler causing performance issues (e.g., a code snippet or query response time), share it, and I can analyze it further. Want to focus on a particular query pattern or entity setup?
Logging and error handling
Since you’re diving into debugging mappings and handlers and improving query performance for subgraphs (likely in The Graph protocol, based on our discussion of Chapter 7), let’s focus on logging and error handling in the context of subgraph mappings and handlers. These practices are crucial for identifying issues in event processing, ensuring robust indexing, and maintaining query reliability. I’ll assume you’re working with AssemblyScript mappings (common for The Graph) but can adjust if you’re using another language or setup. Below are strategies tailored to logging and error handling, with ties to debugging and query performance.
Logging in Mappings and Handlers
Logging helps track the execution flow, inspect variable values, and diagnose issues in mappings without disrupting indexing.
1. Use Built-In Logging Utilities
- AssemblyScript (The Graph): Use the graph-ts library’s log module for logging in mappings.
- Example: typescript
import { log } from 'graph-ts';
export function handleTransfer(event: Transfer): void {
log.info('Processing Transfer: from {}, to {}, value {}', [
event.params.from.toHex(),
event.params.to.toHex(),
event.params.value.toString(),
]);
// Handler logic
}
- Levels: log.debug, log.info, log.warning, log.error, log.critical (use debug for detailed traces, error for failures).
- Output: Logs appear in the Graph Node’s console or hosted service (e.g., Subgraph Studio’s indexing logs).
2. Log Key Data
- Event Parameters: Log critical event data to verify correctness.
- Example: Confirm Transfer event details: typescript
log.debug('Event data: block {}, tx hash {}', [
event.block.number.toString(),
event.transaction.hash.toHex(),
]);
- Entity State: Log before and after updating entities to catch unexpected changes.
- Example: typescript
let user = User.load(event.params.to.toHex());
log.info('Before update: user balance {}', [user ? user.balance.toString() : 'null']);
if (user == null) {
user = new User(event.params.to.toHex());
user.balance = BigInt.fromI32(0);
}
user.balance = user.balance.plus(event.params.value);
log.info('After update: user balance {}', [user.balance.toString()]);
user.save();
- Performance Metrics: Log timestamps to identify slow operations (useful for query performance).
- Example: typescript
let start = event.block.timestamp;
// Expensive operation
log.info('Operation took {} seconds', [
event.block.timestamp.minus(start).toString(),
]);
3. Control Log Volume
- Selective Logging: Avoid logging in tight loops or for every event to prevent performance degradation.
- Bad: typescript
for (let i = 0; i < array.length; i++) {
log.info('Processing item {}', [i.toString()]);
}
- Better: typescript
if (array.length > 1000) {
log.warning('Large array detected: {} items', [array.length.toString()]);
}
- Conditional Logging: Use log.debug for development and disable in production (e.g., adjust Graph Node’s log level to info or higher).
- Query Impact: Excessive logging can slow indexing, indirectly affecting query performance. Monitor indexing speed in Subgraph Studio.
4. Access Logs
- Local Graph Node: View logs in the terminal or Docker container running the Graph Node (docker logs <container>).
- Hosted Service: Check Subgraph Studio or Graph Explorer dashboards for indexing logs.
- Filter Errors: Search logs for ERROR, WARNING, or specific handler names to pinpoint issues.
Error Handling in Mappings and Handlers
Robust error handling prevents indexing failures, ensures data consistency, and aids debugging without crashing the subgraph.
1. Handle Null Cases
- Entity Loading: Always check if load() returns null to avoid NullReferenceError.
- Example: typescript
let user = User.load(event.params.from.toHex());
if (user == null) {
log.warning('User {} not found, creating new', [event.params.from.toHex()]);
user = new User(event.params.from.toHex());
user.balance = BigInt.fromI32(0);
}
- Event Parameters: Validate optional or edge-case parameters.
- Example: typescript
if (event.params.value.isZero()) {
log.error('Invalid transfer: zero value from {}', [event.params.from.toHex()]);
return; // Skip processing
}
2. Guard Against Type Errors
- Type Casting: Ensure safe conversions (e.g., from Bytes to Address).
- Example: typescript
import { Address } from 'graph-ts';
let addr = Address.fromBytes(event.params.from);
if (addr.toHex() == '0x0') {
log.error('Invalid address in event', []);
return;
}
- BigInt/BigDecimal: Handle division or precision issues.
- Example: typescript
import { BigInt } from 'graph-ts';
let divisor = BigInt.fromI32(0);
if (divisor.isZero()) {
log.error('Division by zero avoided', []);
return;
}
3. Catch Indexing Failures
- Try-Catch (Limited): AssemblyScript for The Graph doesn’t support full try-catch, so rely on defensive programming.
- Example: Validate before operations: typescript
let entity = Entity.load(someId);
if (!entity) {
log.error('Entity {} not found, skipping update', [someId]);
return;
}
- Assertions: Use assert() for critical invariants, but sparingly to avoid aborts.
- Example: typescript
import { assert } from 'graph-ts';
assert(user.balance >= BigInt.fromI32(0), 'Negative balance detected');
4. Handle Blockchain Edge Cases
- Reorgs: Ensure handlers are idempotent to handle blockchain reorganizations.
- Example: Use event.block.number checks or avoid overwriting unchanged fields: typescript
if (user.lastUpdatedBlock >= event.block.number) {
log.info('Skipping reorged block {}', [event.block.number.toString()]);
return;
}
user.lastUpdatedBlock = event.block.number;
- Missing Events: Log and skip if expected events are absent.
- Example: typescript
if (!event.receipt) {
log.warning('No receipt for tx {}, skipping', [event.transaction.hash.toHex()]);
return;
}
5. Graceful Recovery
- Skip Invalid Events: Log errors and continue indexing instead of crashing.
- Example: typescript
if (!isValidEvent(event)) {
log.error('Invalid event data for tx {}', [event.transaction.hash.toHex()]);
return;
}
- Fallback Values: Use defaults for missing data to keep entities consistent.
- Example: typescript
user.name = user.name || 'Unknown';
6. Monitor Errors
- Subgraph Studio: Check for failed indexing statuses and error messages in the UI.
- Graph Node Metrics: Use Prometheus/GraphQL endpoints to track handler errors (e.g., subgraph_errors_total).
- Alerting: Set up notifications for critical errors in production subgraphs.
Tie-In to Debugging and Query Performance
- Debugging: Logs are your primary tool for tracing handler issues (e.g., why a User.balance isn’t updating). Use log.debug to inspect intermediate states during development, then switch to log.info or log.error for production.
- Query Performance: Poor error handling (e.g., frequent null checks or redundant saves) can slow indexing, delaying query readiness. Optimize by:
- Logging only critical errors in production to reduce overhead.
- Ensuring handlers don’t create inconsistent entities (e.g., missing fields), which can break queries like: graphql
query {
users(where: { balance_gt: 0 }) {
id
balance
}
}
Example: Logging and Error Handling in a Handler
For a Transfer event handler:
typescript
import { log, BigInt } from 'graph-ts';
import { Transfer } from '../generated/Contract/Contract';
import { User, Transaction } from '../generated/schema';
export function handleTransfer(event: Transfer): void {
// Log event context
log.info('Handling Transfer: tx {}, block {}', [
event.transaction.hash.toHex(),
event.block.number.toString(),
]);
// Validate event data
if (event.params.value.isZero()) {
log.warning('Skipping zero-value transfer from {}', [event.params.from.toHex()]);
return;
}
// Load or create recipient
let toUser = User.load(event.params.to.toHex());
if (toUser == null) {
log.info('Creating new user {}', [event.params.to.toHex()]);
toUser = new User(event.params.to.toHex());
toUser.balance = BigInt.fromI32(0);
toUser.transactionCount = BigInt.fromI32(0);
}
// Update balance
toUser.balance = toUser.balance.plus(event.params.value);
toUser.transactionCount = toUser.transactionCount.plus(BigInt.fromI32(1));
toUser.lastUpdatedBlock = event.block.number;
// Save and log result
toUser.save();
log.debug('Updated user {}: balance {}, tx count {}', [
toUser.id,
toUser.balance.toString(),
toUser.transactionCount.toString(),
]);
// Create transaction record
let txId = event.transaction.hash.toHex() + '-' + event.logIndex.toString();
let transaction = new Transaction(txId);
transaction.user = toUser.id;
transaction.amount = event.params.value;
transaction.timestamp = event.block.timestamp;
// Validate transaction
if (!transaction.amount) {
log.error('Transaction {} has no amount, skipping', [txId]);
return;
}
transaction.save();
log.info('Saved transaction {}', [txId]);
}
- Logging: Tracks event processing, user updates, and transaction creation.
- Error Handling: Skips zero-value transfers, handles null users, validates transaction data.
- Performance: Minimizes logs in critical paths, ensures single save() calls per entity.
Debugging Workflow with Logs
If a handler fails (e.g., User.balance is wrong):
- Add log.debug to trace event.params.value and user.balance before/after updates.
- Check logs in Subgraph Studio or local Graph Node for errors like NullReferenceError.
- Validate event data against blockchain (e.g., Etherscan).
- Test fixes with a local chain (e.g., Hardhat) and query results: graphql
query {
users(id: "0x...") {
balance
}
}
If you’re hitting a specific logging or error issue (e.g., a handler crashing or missing logs), share details like code or error messages, and I can zoom in. Want to explore a particular scenario or need a custom logging setup?
Testing subgraphs locally
Testing subgraphs locally is a critical part of developing and debugging mappings and handlers, especially given your focus on logging, error handling, and query performance in the context of Chapter (likely for The Graph protocol). Local testing allows you to validate subgraph indexing, mappings, and GraphQL queries in a controlled environment before deploying to a hosted service or decentralized network. Below, I’ll outline how to test subgraphs locally, with ties to debugging mappings, optimizing queries, and leveraging logging/error handling for effective testing.
Prerequisites
- Local Environment: Ensure you have Node.js, Docker, and the Graph CLI installed.
- Install Graph CLI: npm install -g @graphprotocol/graph-cli
- Blockchain Node: A local blockchain (e.g., Hardhat, Ganache) to emit events for indexing.
- Subgraph Setup: A subgraph project with:
- subgraph.yaml (manifest defining contracts, events, and mappings).
- Schema (schema.graphql) for entities.
- Mappings (e.g., src/mapping.ts in AssemblyScript).
- Graph Node: A local Graph Node to index and serve the subgraph.
Steps to Test Subgraphs Locally
1. Set Up a Local Blockchain
- Hardhat Example:
- Initialize a Hardhat project: bash
npx hardhat init
- Deploy a sample contract (e.g., an ERC20 token emitting Transfer events): solidity
// contracts/ERC20.sol
pragma solidity ^0.8.0;
contract ERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
function transfer(address to, uint256 value) public returns (bool) {
emit Transfer(msg.sender, to, value);
return true;
}
}
- Deploy with Hardhat: bash
npx hardhat node
npx hardhat run scripts/deploy.js --network localhost
- Note the contract address and ABI for subgraph.yaml.
- Trigger Events: Use Hardhat scripts or a frontend to call contract functions (e.g., transfer) to generate events for testing mappings.
2. Configure the Subgraph
- Update subgraph.yaml:
- Point to the local blockchain (e.g., http://127.0.0.1:8545 for Hardhat).
- Specify the contract address and events to index.
- Example: yaml
specVersion: 0.0.5
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum
name: ERC20
network: localhost
source:
address: "0xYourContractAddress"
abi: ERC20
startBlock: 0
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- User
- Transaction
abis:
- name: ERC20
file: ./abis/ERC20.json
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
file: ./src/mapping.ts
- Schema (schema.graphql):
- Define entities to store event data.
- Example: graphql
type User @entity {
id: ID!
balance: BigInt!
transactionCount: BigInt!
}
type Transaction @entity {
id: ID!
user: User!
amount: BigInt!
timestamp: BigInt!
}
- Mappings (src/mapping.ts):
- Include logging and error handling (as discussed previously).
- Example: typescript
import { log, BigInt } from 'graph-ts';
import { Transfer } from '../generated/ERC20/ERC20';
import { User, Transaction } from '../generated/schema';
export function handleTransfer(event: Transfer): void {
log.info('Handling Transfer: from {}, to {}, value {}', [
event.params.from.toHex(),
event.params.to.toHex(),
event.params.value.toString(),
]);
if (event.params.value.isZero()) {
log.warning('Skipping zero-value transfer', []);
return;
}
let toUser = User.load(event.params.to.toHex());
if (toUser == null) {
log.info('Creating user {}', [event.params.to.toHex()]);
toUser = new User(event.params.to.toHex());
toUser.balance = BigInt.fromI32(0);
toUser.transactionCount = BigInt.fromI32(0);
}
toUser.balance = toUser.balance.plus(event.params.value);
toUser.transactionCount = toUser.transactionCount.plus(BigInt.fromI32(1));
toUser.save();
let txId = event.transaction.hash.toHex() + '-' + event.logIndex.toString();
let transaction = new Transaction(txId);
transaction.user = toUser.id;
transaction.amount = event.params.value;
transaction.timestamp = event.block.timestamp;
transaction.save();
log.info('Saved user {} and tx {}', [toUser.id, txId]);
}
3. Set Up a Local Graph Node
- Docker Setup:
- Clone the Graph Node repo: bash
git clone https://github.com/graphprotocol/graph-node
cd graph-node/docker
- Update docker-compose.yml to point to your local blockchain: yaml
ethereum: 'localhost:http://host.docker.internal:8545'
- Start Graph Node: bash
docker-compose up
- Ensure Graph Node is running at http://localhost:8000 (GraphQL endpoint) and http://localhost:8030 (status).
- Verify Connection:
- Check logs (docker logs graph-node) for errors.
- Confirm the node connects to your blockchain (e.g., Hardhat at http://127.0.0.1:8545).
4. Deploy the Subgraph Locally
- Generate Code:
- Run: bash
graph codegen
- This generates types for your schema and contract ABIs (e.g., generated/ERC20/ERC20.ts).
- Build Subgraph:
- Compile mappings to WASM: bash
graph build
- Deploy to Local Graph Node:
- Create the subgraph: bash
graph create --node http://localhost:8020/ my-subgraph
- Deploy: bash
graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 --version-label v0.0.1 my-subgraph
- Check deployment status in logs or at http://localhost:8030.
5. Test Indexing
- Trigger Events:
- Use Hardhat to call contract functions (e.g., transfer): javascript
// scripts/test.js
const { ethers } = require('hardhat');
async function main() {
const contract = await ethers.getContractAt('ERC20', '0xYourContractAddress');
await contract.transfer('0xRecipientAddress', 100);
console.log('Transfer sent');
}
main();
- Run: npx hardhat run scripts/test.js –network localhost.
- Monitor Indexing:
- Check Graph Node logs for handler execution:
- Look for log.info messages (e.g., Handling Transfer: from 0x…).
- Catch errors like NullReferenceError or schema mismatches.
- Verify indexing progress: bash
- Check Graph Node logs for handler execution:
curl -X POST -d '{"query": "{ indexingStatuses { subgraph synced blockCount } }"}' http://localhost:8030/graphql
- Debug Issues:
- Use logs (as per your logging setup) to trace handler logic.
- Cross-check event data with blockchain logs (e.g., Hardhat console or Etherscan for testnets).
- Fix mappings (e.g., null checks, incorrect IDs) and redeploy.
6. Test Queries
- GraphQL Endpoint: Query the subgraph at http://localhost:8000/subgraphs/name/my-subgraph/graphql.
- Use GraphiQL:
- Open http://localhost:8000 in a browser (if Graph Node exposes the UI) or use a tool like Postman.
- Example query: graphql
query {
users(first: 10) {
id
balance
transactionCount
}
transactions(first: 10, orderBy: timestamp, orderDirection: desc) {
id
user {
id
}
amount
timestamp
}
}
- Validate Results:
- Compare balance and transactionCount with expected values from your transfer calls.
- Check for missing or incorrect data (e.g., due to handler errors).
- Performance Testing:
- Test query speed for large datasets (e.g., generate 1000 Transfer events).
- Optimize schema (e.g., add @index to timestamp) or mappings (e.g., batch updates) if slow, as discussed in query performance.
7. Unit Testing Mappings
- Matchstick (The Graph’s Testing Framework):
- Install: bash
npm install @graphprotocol/matchstick
- Write tests for handlers: typescript
// tests/mapping.test.ts
import { assert, test, clearStore } from 'matchstick-as';
import { Address, BigInt } from 'graph-ts';
import { handleTransfer } from '../src/mapping';
import { createTransferEvent } from './utils';
test('handleTransfer updates user balance and creates transaction', () => {
// Clear store for clean state
clearStore();
// Create mock event
let event = createTransferEvent(
Address.fromString('0xSender'),
Address.fromString('0xRecipient'),
BigInt.fromI32(100)
);
// Call handler
handleTransfer(event);
// Assert user
assert.fieldEquals('User', '0xRecipient', 'balance', '100');
assert.fieldEquals('User', '0xRecipient', 'transactionCount', '1');
// Assert transaction
let txId = event.transaction.hash.toHex() + '-' + event.logIndex.toString();
assert.fieldEquals('Transaction', txId, 'amount', '100');
assert.fieldEquals('Transaction', txId, 'user', '0xRecipient');
});
- Helper (tests/utils.ts): typescript
import { newMockEvent } from 'matchstick-as';
import { ethereum, Address, BigInt } from 'graph-ts';
import { Transfer } from '../generated/ERC20/ERC20';
export function createTransferEvent(from: Address, to: Address, value: BigInt): Transfer {
let mockEvent = newMockEvent();
let transferEvent = new Transfer(
mockEvent.address,
mockEvent.logIndex,
mockEvent.transactionLogIndex,
mockEvent.logType,
mockEvent.block,
mockEvent.transaction,
mockEvent.parameters,
mockEvent.receipt
);
transferEvent.parameters = [
new ethereum.EventParam('from', ethereum.Value.fromAddress(from)),
new ethereum.EventParam('to', ethereum.Value.fromAddress(to)),
new ethereum.EventParam('value', ethereum.Value.fromUnsignedBigInt(value)),
];
return transferEvent;
}
- Run tests: bash
graph test
- Benefits:
- Tests handlers in isolation without a blockchain.
- Catches logic errors (e.g., incorrect balance updates).
- Uses logging assertions (e.g., check log.info outputs indirectly via entity states).
8. Debugging with Logs and Errors
- Leverage Logging (from your earlier question):
- Check log.info/log.error in Graph Node logs to trace handler execution.
- Example: If User.balance is wrong, inspect: bash
docker logs graph-node | grep "Handling Transfer"
- Look for warnings (e.g., Skipping zero-value transfer).
- Error Handling:
- Ensure handlers skip invalid events gracefully (e.g., return on null entities).
- Test edge cases locally (e.g., zero-value transfers, missing users): javascript
await contract.transfer('0xRecipient', 0); // Test zero-value case
- Verify logs catch these cases without crashing indexing.
- Fix and Iterate:
- Update mappings (e.g., add null checks).
- Redeploy: graph deploy –node http://localhost:8020/ my-subgraph.
- Requery and check logs.
9. Query Performance Testing
- Simulate Load:
- Generate many events (e.g., 1000 Transfers via a Hardhat script).
- Query large datasets: graphql
query {
transactions(first: 100, where: { amount_gt: 50 }, orderBy: timestamp) {
id
amount
}
}
Measure response time in GraphiQL or with curl.
- Optimize:
- Add indexes (e.g., timestamp: BigInt! @index in schema.graphql).
- Minimize handler writes (e.g., batch save() calls).
- Rebuild and redeploy to test improvements.
- Logs for Performance:
- Add timing logs: typescript
let start = event.block.timestamp;
// Process event
valence = user.balance.plus(event.params.value);
user.save();
log.info('Handler took {} ms', [
event.block.timestamp.minus(start).toString(),
]);
- Check for bottlenecks in indexing.
Example Testing Workflow
- Start Hardhat: Run npx hardhat node.
- Deploy Contract: Deploy ERC20 and note address.
- Update Subgraph: Set contract address in subgraph.yaml.
- Start Graph Node: Run docker-compose up in graph-node/docker.
- Deploy Subgraph: Run graph create and graph deploy.
- Trigger Events:
- Call transfer(0xRecipient, 100) via Hardhat.
- Check Graph Node logs for:
Handling Transfer: from 0xSender, to 0xRecipient, value 100
Saved user 0xRecipient and tx 0xHash-0
- Query Results:
- Run: graphql
query {
users(id: "0xRecipient") {
balance
transactionCount
}
}
- Expect: { “balance”: “100”, “transactionCount”: “1” }.
- Unit Test:
- Run graph test to verify handleTransfer updates User and Transaction.
- Debug Issues:
- If balance is wrong, check logs for null users or skipped events.
- Fix mappings (e.g., add toUser.save()) and redeploy.
- Performance Test:
- Generate 1000 transfers.
- Query transactions(first: 100) and optimize if slow.
Common Issues and Fixes
- Indexing Fails:
- Log Error: Check for NullReferenceError or schema mismatches.
- Fix: Add null checks, validate subgraph.yaml ABIs.
- Queries Return Empty:
- Log Check: Verify handlers save entities (e.g., log.info(‘Saved user …’)).
- Fix: Ensure entity.save() is called, check entity IDs.
- Slow Queries:
- Log Timing: Add performance logs to handlers.
- Fix: Index fields, reduce handler complexity, test with smaller first values.
Tie-In to Previous Questions
- Logging: Use log.info/log.error to trace test execution (e.g., confirm handleTransfer processes events).
- Error Handling: Test edge cases (e.g., zero-value transfers) locally to ensure handlers skip gracefully.
- Query Performance: Validate indexing speed and query response times locally to catch bottlenecks early (e.g., missing indexes).
If you hit a specific issue (e.g., deployment errors, query failures, or a code snippet to test), share it, and I can provide targeted fixes. Want to focus on a particular test case, like mocking an event or optimizing a query locally?
Example: Optimize a DeFi dashboard query
Since you’re focused on testing subgraphs locally, debugging mappings, logging, error handling, and improving query performance in the context of Chapter 7 (likely for The Graph protocol), let’s dive into an example of optimizing a GraphQL query for a DeFi dashboard. This will build on our previous discussions by demonstrating how to refine a query, leverage local testing, and ensure mappings support efficient data retrieval. I’ll assume the DeFi dashboard displays user balances, transaction histories, and liquidity pool stats, typical for decentralized finance apps. The example will include schema design, mappings with logging/error handling, query optimization, and local testing steps.
Scenario
The DeFi dashboard needs to display:
- A user’s total balance across a token (e.g., an ERC20).
- Their recent transactions (e.g., transfers).
- Liquidity pool metrics (e.g., total staked value for a pool). The initial query is slow and overfetches data, impacting dashboard performance. We’ll optimize it, ensure mappings support the query efficiently, and test locally to verify improvements.
Initial Setup
Schema (schema.graphql)
graphql
type User @entity {
id: ID!
balance: BigInt! @index
transactionCount: BigInt!
transactions: [Transaction!]! @derivedFrom(field: "user")
}
type Transaction @entity {
id: ID!
user: User!
amount: BigInt!
timestamp: BigInt! @index
from: Bytes!
to: Bytes!
}
type LiquidityPool @entity {
id: ID!
totalStaked: BigInt! @index
userCount: BigInt!
}
Mapping (src/mapping.ts)
Handles Transfer events for the token and Stake events for the liquidity pool. Includes logging and error handling (per your earlier focus).
typescript
import { log, BigInt, Address } from 'graph-ts';
import { Transfer } from '../generated/Token/Token';
import { Stake } from '../generated/Pool/Pool';
import { User, Transaction, LiquidityPool } from '../generated/schema';
export function handleTransfer(event: Transfer): void {
log.info('Handling Transfer: from {}, to {}, value {}', [
event.params.from.toHex(),
event.params.to.toHex(),
event.params.value.toString(),
]);
if (event.params.value.isZero()) {
log.warning('Skipping zero-value transfer in tx {}', [event.transaction.hash.toHex()]);
return;
}
// Update sender
let fromUser = User.load(event.params.from.toHex());
if (fromUser == null) {
log.info('Creating sender {}', [event.params.from.toHex()]);
fromUser = new User(event.params.from.toHex());
fromUser.balance = BigInt.fromI32(0);
fromUser.transactionCount = BigInt.fromI32(0);
}
fromUser.balance = fromUser.balance.minus(event.params.value);
fromUser.transactionCount = fromUser.transactionCount.plus(BigInt.fromI32(1));
if (fromUser.balance.lt(BigInt.fromI32(0))) {
log.error('Negative balance for user {}: {}', [
fromUser.id,
fromUser.balance.toString(),
]);
fromUser.balance = BigInt.fromI32(0); // Fallback
}
fromUser.save();
// Update recipient
let toUser = User.load(event.params.to.toHex());
if (toUser == null) {
log.info('Creating recipient {}', [event.params.to.toHex()]);
toUser = new User(event.params.to.toHex());
toUser.balance = BigInt.fromI32(0);
toUser.transactionCount = BigInt.fromI32(0);
}
toUser.balance = toUser.balance.plus(event.params.value);
toUser.transactionCount = toUser.transactionCount.plus(BigInt.fromI32(1));
toUser.save();
// Record transaction
let txId = event.transaction.hash.toHex() + '-' + event.logIndex.toString();
let transaction = new Transaction(txId);
transaction.user = toUser.id;
transaction.amount = event.params.value;
transaction.timestamp = event.block.timestamp;
transaction.from = event.params.from;
transaction.to = event.params.to;
transaction.save();
log.info('Saved tx {}, user balances: {} (from), {} (to)', [
txId,
fromUser.balance.toString(),
toUser.balance.toString(),
]);
}
export function handleStake(event: Stake): void {
log.info('Handling Stake: user {}, amount {}', [
event.params.user.toHex(),
event.params.amount.toString(),
]);
let pool = LiquidityPool.load(event.address.toHex());
if (pool == null) {
log.info('Creating pool {}', [event.address.toHex()]);
pool = new LiquidityPool(event.address.toHex());
pool.totalStaked = BigInt.fromI32(0);
pool.userCount = BigInt.fromI32(0);
}
let user = User.load(event.params.user.toHex());
if (user == null) {
log.error('User {} not found for stake', [event.params.user.toHex()]);
return;
}
pool.totalStaked = pool.totalStaked.plus(event.params.amount);
pool.userCount = pool.userCount.plus(BigInt.fromI32(1));
pool.save();
log.info('Updated pool {}: totalStaked {}, userCount {}', [
pool.id,
pool.totalStaked.toString(),
pool.userCount.toString(),
]);
}
Subgraph Manifest (subgraph.yaml)
yaml
specVersion: 0.0.5
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum
name: Token
network: localhost
source:
address: "0xTokenAddress"
abi: Token
startBlock: 0
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- User
- Transaction
abis:
- name: Token
file: ./abis/Token.json
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
file: ./src/mapping.ts
- kind: ethereum
name: Pool
network: localhost
source:
address: "0xPoolAddress"
abi: Pool
startBlock: 0
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- LiquidityPool
abis:
- name: Pool
file: ./abis/Pool.json
eventHandlers:
- event: Stake(indexed address,uint256)
handler: handleStake
file: ./src/mapping.ts
Initial (Inefficient) Query
The DeFi dashboard uses this query, which is slow and overfetches data:
graphql
query GetDashboard($userId: ID!) {
users(where: { id: $userId }) {
id
balance
transactionCount
transactions(first: 1000, orderBy: timestamp) {
id
amount
timestamp
from
to
}
}
liquidityPools(first: 100) {
id
totalStaked
userCount
}
}
Issues:
- Fetches up to 1000 transactions, causing high database load.
- No orderDirection for transactions, leading to inconsistent sorting.
- Retrieves all liquidityPools (up to 100), but the dashboard may only need one or a few.
- from and to fields in Transaction are Bytes, which may require conversion in the frontend, slowing rendering.
- No filtering on liquidityPools, potentially returning irrelevant data.
- Query performance suffers if timestamp or balance lacks indexing.
Optimized Query
Here’s the optimized version, tailored for the dashboard’s needs (e.g., show one user’s data and one main pool):
graphql
query GetDashboard($userId: ID!, $poolId: ID!) {
users(where: { id: $userId }) {
id
balance
transactionCount
transactions(first: 10, orderBy: timestamp, orderDirection: desc) {
id
amount
timestamp
}
}
liquidityPools(where: { id: $poolId }) {
id
totalStaked
userCount
}
}
Improvements:
- Reduced Transaction Fetch: Limits transactions to 10 (sufficient for a dashboard’s “recent activity” section).
- Explicit Sorting: Adds orderDirection: desc for newest transactions first.
- Single Pool: Filters liquidityPools by id instead of fetching many.
- Dropped Fields: Removes from and to (not needed for display; available via user relation if required).
- Performance: Relies on indexed fields (balance, timestamp, totalStaked) for faster filtering/sorting.
Ensure Schema and Mappings Support the Query
To make the query efficient, the schema and mappings must align:
- Indexes: Already included in the schema (balance, timestamp, totalStaked). These speed up where and orderBy.
- Derived Fields: The transactions field in User uses @derivedFrom, avoiding manual joins: graphql
transactions: [Transaction!]! @derivedFrom(field: "user")
- Efficient Mappings:
- Minimize save() calls: Each user is saved once per handleTransfer.
- Avoid redundant computations: transactionCount is incremented directly.
- Logging is selective (e.g., skips verbose debug logs in production).
- Error handling prevents crashes (e.g., skips zero-value transfers, fixes negative balances).
- Data Consistency:
- handleTransfer ensures User.balance and Transaction.amount are updated atomically.
- handleStake correctly aggregates totalStaked and userCount.
Local Testing to Verify Optimization
Following your interest in local testing, let’s test the subgraph and query locally to confirm performance and correctness.
1. Set Up Local Blockchain (Hardhat)
- Initialize Hardhat: bash
npx hardhat init
- Deploy contracts (Token.sol and Pool.sol): solidity
// contracts/Token.sol
pragma solidity ^0.8.0;
contract Token {
event Transfer(address indexed from, address indexed to, uint256 value);
function transfer(address to, uint256 value) public returns (bool) {
emit Transfer(msg.sender, to, value);
return true;
}
}
// contracts/Pool.sol
pragma solidity ^0.8.0;
contract Pool {
event Stake(address indexed user, uint256 amount);
function stake(uint256 amount) public {
emit Stake(msg.sender, amount);
}
}
- Deploy: bash
npx hardhat node
npx hardhat run scripts/deploy.js --network localhost
- javascript
// scripts/deploy.js
const { ethers } = require('hardhat');
async function main() {
const Token = await ethers.getContractFactory('Token');
const token = await Token.deploy();
await token.deployed();
console.log('Token:', token.address);
const Pool = await ethers.getContractFactory('Pool');
const pool = await Pool.deploy();
await pool.deployed();
console.log('Pool:', pool.address);
}
main();
- Note addresses (e.g., 0xTokenAddress, 0xPoolAddress).
2. Update Subgraph
- Set contract addresses in subgraph.yaml.
- Run: bash
graph codegen
graph build
3. Run Local Graph Node
- Clone and configure Graph Node: bash
git clone https://github.com/graphprotocol/graph-node
cd graph-node/docker
- Update docker-compose.yml: yaml
ethereum: 'localhost:http://host.docker.internal:8545'
- Start: bash
docker-compose up
4. Deploy Subgraph
- Create and deploy: bash
graph create --node http://localhost:8020/ defi-dashboard
graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 --version-label v0.0.1 defi-dashboard
5. Generate Test Data
- Script to emit events: javascript
// scripts/test.js
const { ethers } = require('hardhat');
async function main() {
const token = await ethers.getContractAt('Token', '0xTokenAddress');
const pool = await ethers.getContractAt('Pool', '0xPoolAddress');
const [signer] = await ethers.getSigners();
const user = '0xRecipientAddress'; // Replace with test address
// 100 transfers
for (let i = 0; i < 100; i++) {
await token.transfer(user, 100 + i);
console.log(`Transfer ${i}`);
}
// Stake in pool
await pool.stake(1000);
console.log('Staked');
}
main();
- Run: bash
npx hardhat run scripts/test.js --network localhost
6. Test Initial Query
- Query at http://localhost:8000/subgraphs/name/defi-dashboard/graphql : graphql
query GetDashboard($userId: ID!) {
users(where: { id: $userId }) {
id
balance
transactionCount
transactions(first: 1000, orderBy: timestamp) {
id
amount
timestamp
from
to
}
}
liquidityPools(first: 100) {
id
totalStaked
userCount
}
}
- Variables: { “userId”: “0xRecipientAddress” }
- Issues Observed:
- Slow response (e.g., 2 seconds) due to 1000 transactions.
- Unnecessary liquidityPools data (only one pool exists).
- Logs show handler execution:
Handling Transfer: from 0x..., to 0xRecipientAddress, value 100
7. Test Optimized Query
- Query: graphql
query GetDashboard($userId: ID!, $poolId: ID!) {
users(where: { id: $userId }) {
id
balance
transactionCount
transactions(first: 10, orderBy: timestamp, orderDirection: desc) {
id
amount
timestamp
}
}
liquidityPools(where: { id: $poolId }) {
id
totalStaked
userCount
}
}
Variables: { “userId”: “0xRecipientAddress”, “poolId”: “0xPoolAddress” }
- Results:
- Faster response (e.g., <200ms) due to fewer transactions and specific pool.Expected output: json
{
"users": [{
"id": "0xRecipientAddress",
"balance": "5050", // Sum of 100 transfers (100 + 101 + ... + 199)
"transactionCount": "100",
"transactions": [
{ "id": "...-99", "amount": "199", "timestamp": "..." },
// 9 more, newest first
]
}],
"liquidityPools": [{
"id": "0xPoolAddress",
"totalStaked": "1000",
"userCount": "1"
}]
}
- Logs Confirm:
- No errors (e.g., no Negative balance or User not found).
- Efficient handler execution.
8. Unit Test Mappings
- Use Matchstick to test handleTransfer and handleStake: typescript
// tests/mapping.test.ts
import { assert, test, clearStore } from 'matchstick-as';
import { Address, BigInt } from 'graph-ts';
import { handleTransfer, handleStake } from '../src/mapping';
import { createTransferEvent, createStakeEvent } from './utils';
test('handleTransfer updates user balance and transaction', () => {
clearStore();
let event = createTransferEvent(
Address.fromString('0xSender'),
Address.fromString('0xRecipient'),
BigInt.fromI32(100)
);
handleTransfer(event);
assert.fieldEquals('User', '0xRecipient', 'balance', '100');
assert.fieldEquals('User', '0xRecipient', 'transactionCount', '1');
assert.fieldEquals('Transaction', event.transaction.hash.toHex() + '-0', 'amount', '100');
});
test('handleStake updates liquidity pool', () => {
clearStore();
let user = new User('0xUser');
user.balance = BigInt.fromI32(1000);
user.transactionCount = BigInt.fromI32(1);
user.save();
let event = createStakeEvent(
Address.fromString('0xUser'),
BigInt.fromI32(500),
Address.fromString('0xPool')
);
handleStake(event);
assert.fieldEquals('LiquidityPool', '0xPool', 'totalStaked', '500');
assert.fieldEquals('LiquidityPool', '0xPool', 'userCount', '1');
});
Helper (tests/utils.ts): typescript
import { newMockEvent } from 'matchstick-as';
import { ethereum, Address, BigInt } from 'graph-ts';
import { Transfer } from '../generated/Token/Token';
import { Stake } from '../generated/Pool/Pool';
export function createTransferEvent(from: Address, to: Address, value: BigInt): Transfer {
let mockEvent = newMockEvent();
let transferEvent = new Transfer(
mockEvent.address,
mockEvent.logIndex,
mockEvent.transactionLogIndex,
mockEvent.logType,
mockEvent.block,
mockEvent.transaction,
mockEvent.parameters,
mockEvent.receipt
);
transferEvent.parameters = [
new ethereum.EventParam('from', ethereum.Value.fromAddress(from)),
new ethereum.EventParam('to', ethereum.Value.fromAddress(to)),
new ethereum.EventParam('value', ethereum.Value.fromUnsignedBigInt(value)),
];
return transferEvent;
}
export function createStakeEvent(user: Address, amount: BigInt, pool: Address): Stake {
let mockEvent = newMockEvent();
mockEvent.address = pool;
let stakeEvent = new Stake(
mockEvent.address,
mockEvent.logIndex,
mockEvent.transactionLogIndex,
mockEvent.logType,
mockEvent.block,
mockEvent.transaction,
mockEvent.parameters,
mockEvent.receipt
);
stakeEvent.parameters = [
new ethereum.EventParam('user', ethereum.Value.fromAddress(user)),
new ethereum.EventParam('amount', ethereum.Value.fromUnsignedBigInt(amount)),
];
return stakeEvent;
}
- Run: bash
graph test
9. Debugging and Logging
- Check Logs:
- In Graph Node: docker logs graph-node | grep “Handling“.
- Verify:
Handling Transfer: from 0x..., to 0xRecipientAddress, value 100
Saved tx ..., user balances: 0 (from), 100 (to)
Handling Stake: user 0x..., amount 1000
Updated pool 0xPoolAddress: totalStaked 1000, userCount 1
- Catch Errors:
- If User not found in handleStake, ensure User exists (via prior Transfer).
- If slow indexing, reduce logging (e.g., comment out log.debug).
- Fix Issues:
- Example: If balance is wrong, trace via logs and update handleTransfer.
10. Performance Validation
- Measure Query Time:
- Initial query: ~2s (1000 transactions).
- Optimized query: ~150ms (10 transactions, single pool).
- Optimize Further (if needed):
- Add index to Transaction.user if filtering by user is slow: graphql
type Transaction @entity {
id: ID!
user: User! @index
amount: BigInt!
timestamp: BigInt! @index
}
- Batch updates in handleTransfer (e.g., save multiple transactions at once).
- Retest:
- Redeploy: graph deploy –node http://localhost:8020/ defi-dashboard.
- Requery and confirm speed.
Additional Optimizations (Optional)
- Caching: Use Apollo Client in the dashboard to cache query results for repeated views.
- Pagination: For dynamic transaction lists, add skip or after cursors: graphql
transactions(first: 10, where: { timestamp_lt: $lastTimestamp }, orderBy: timestamp, orderDirection: desc) {
id
amount
timestamp
}
- Precomputed Aggregates:
- Store user.totalStaked in User via handleStake to avoid querying LiquidityPool separately: typescript
user.totalStaked = user.totalStaked.plus(event.params.amount);
user.save();
- Update schema: graphql
type User @entity {
id: ID!
balance: BigInt! @index
transactionCount: BigInt!
totalStaked: BigInt!
transactions: [Transaction!]! @derivedFrom(field: "user")
}
Tie-In to Previous Questions
- Logging: The example uses log.info/log.warning to trace event processing and catch errors (e.g., zero-value transfers), aiding debugging during local tests.
- Error Handling: Skips invalid events (e.g., zero value) and handles null users/pools, ensuring robust indexing for queries.
- Query Performance: The optimized query reduces data fetch (10 vs. 1000 transactions) and uses indexes, aligning with your performance focus.
- Local Testing: The workflow mirrors your interest in local setups, using Hardhat, Graph Node, and Matchstick to validate mappings and queries.
Expected Outcome
- Dashboard Performance: Loads user data and pool stats in <200ms, suitable for real-time UI.
- Correctness: Balances, transaction counts, and pool metrics match blockchain events.
- Reliability: No indexing crashes, thanks to error handling.
- Debuggability: Logs pinpoint any issues (e.g., missing users).
If you have a specific query, contract, or performance bottleneck (e.g., a slow response time or complex entity), share it, and I can refine the example further. Want to tweak this for a different DeFi feature (e.g., swaps, yields) or test a specific edge case?