indexing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Onchain Data & Indexing

链上数据与索引

What You Probably Got Wrong

你可能踩过的误区

You try to query historical state via RPC calls. You can't cheaply read past state.
eth_call
reads current state. Reading state at a historical block requires an archive node (expensive, slow). For historical data, you need an indexer.
You loop through blocks looking for events. Scanning millions of blocks with
eth_getLogs
is O(n) — it will timeout, get rate-limited, or cost a fortune in RPC credits. Use an indexer that has already processed every block.
You store query results onchain. Leaderboards, activity feeds, analytics — these belong offchain. Compute offchain, index events offchain. If you need an onchain commitment, store a hash.
You don't know about The Graph. The Graph turns your contract's events into a queryable GraphQL API. It's how every serious dApp reads historical data. Etherscan uses indexers. Uniswap uses indexers. So should you.
You treat events as optional. Events are THE primary way to read historical onchain activity. If your contract doesn't emit events, nobody can build a frontend, dashboard, or analytics on top of it. Design contracts event-first.

你尝试通过RPC调用查询历史状态。低成本读取历史状态是不可行的。
eth_call
仅读取当前状态。读取历史区块的状态需要使用归档节点(成本高、速度慢)。对于历史数据,你需要使用索引器。
你遍历区块查找事件。用
eth_getLogs
扫描数百万个区块的时间复杂度是O(n)——会超时、触发速率限制,或消耗大量RPC额度。请使用已处理过所有区块的索引器。
你将查询结果存储在链上。排行榜、活动流、分析数据——这些都属于链下范畴。在链下计算、索引事件。如果需要链上承诺,只需存储哈希值即可。
你不了解The Graph。The Graph能将合约事件转换为可查询的GraphQL API。这是所有成熟dApp读取历史数据的方式。Etherscan、Uniswap都在使用索引器,你也应该这么做。
你将事件视为可选项。事件是读取链上历史活动的核心方式。如果你的合约不发射事件,没人能在其之上构建前端、仪表盘或分析工具。请以事件优先的原则设计合约。

Events Are Your API

事件就是你的API

Solidity events are cheap to emit (~375 gas base + 375 per indexed topic + 8 gas per byte of data) and free to read offchain. They're stored in transaction receipts, not in contract storage, so they don't cost storage gas.
Solidity事件的发射成本很低(基础约375 gas,每个索引主题375 gas,每字节数据8 gas),且链下读取是免费的。事件存储在交易收据中,而非合约存储,因此不会产生存储gas费用。

Design Contracts Event-First

以事件优先的原则设计合约

Every state change should emit an event. This isn't just good practice — it's how your frontend, indexer, and block explorer know what happened.
solidity
// ✅ Good — every action emits a queryable event
contract Marketplace {
    event Listed(
        uint256 indexed listingId,
        address indexed seller,
        address indexed tokenContract,
        uint256 tokenId,
        uint256 price
    );
    event Sold(uint256 indexed listingId, address indexed buyer, uint256 price);
    event Cancelled(uint256 indexed listingId);

    function list(address token, uint256 tokenId, uint256 price) external {
        uint256 id = nextListingId++;
        listings[id] = Listing(msg.sender, token, tokenId, price, true);
        emit Listed(id, msg.sender, token, tokenId, price);
    }

    function buy(uint256 listingId) external payable {
        // ... transfer logic ...
        emit Sold(listingId, msg.sender, msg.value);
    }
}
Index the fields you'll filter by. You get 3 indexed topics per event. Use them for addresses and IDs that you'll query —
seller
,
buyer
,
tokenContract
,
listingId
. Don't index large values or values you won't filter on.
每次状态变更都应发射事件。这不仅是最佳实践——更是你的前端、索引器和区块浏览器了解链上情况的方式。
solidity
// ✅ 优秀示例——每个操作都会发射可查询的事件
contract Marketplace {
    event Listed(
        uint256 indexed listingId,
        address indexed seller,
        address indexed tokenContract,
        uint256 tokenId,
        uint256 price
    );
    event Sold(uint256 indexed listingId, address indexed buyer, uint256 price);
    event Cancelled(uint256 indexed listingId);

    function list(address token, uint256 tokenId, uint256 price) external {
        uint256 id = nextListingId++;
        listings[id] = Listing(msg.sender, token, tokenId, price, true);
        emit Listed(id, msg.sender, token, tokenId, price);
    }

    function buy(uint256 listingId) external payable {
        // ... 转账逻辑 ...
        emit Sold(listingId, msg.sender, msg.value);
    }
}
为需要过滤的字段添加索引。每个事件最多可设置3个索引主题。将它们用于你要查询的地址和ID——如
seller
buyer
tokenContract
listingId
。不要为大值或无需过滤的值添加索引。

Reading Events Directly (Small Scale)

直接读取事件(小规模场景)

For recent events or low-volume contracts, you can read events directly via RPC:
typescript
import { createPublicClient, http, parseAbiItem } from 'viem';

const client = createPublicClient({
  chain: mainnet,
  transport: http(),
});

// Get recent events (last 1000 blocks)
const logs = await client.getLogs({
  address: '0xYourContract',
  event: parseAbiItem('event Sold(uint256 indexed listingId, address indexed buyer, uint256 price)'),
  fromBlock: currentBlock - 1000n,
  toBlock: 'latest',
});
This works for: Last few thousand blocks, low-volume contracts, real-time monitoring. This breaks for: Historical queries, high-volume contracts, anything scanning more than ~10K blocks.

对于近期事件或低交易量合约,你可以直接通过RPC读取事件:
typescript
import { createPublicClient, http, parseAbiItem } from 'viem';

const client = createPublicClient({
  chain: mainnet,
  transport: http(),
});

// 获取近期事件(最近1000个区块)
const logs = await client.getLogs({
  address: '0xYourContract',
  event: parseAbiItem('event Sold(uint256 indexed listingId, address indexed buyer, uint256 price)'),
  fromBlock: currentBlock - 1000n,
  toBlock: 'latest',
});
适用场景:最近几千个区块、低交易量合约、实时监控。 不适用场景:历史查询、高交易量合约、任何扫描超过约1万个区块的操作。

The Graph (Subgraphs)

The Graph(子图)

The Graph is a decentralized indexing protocol. You define how to process events, deploy a subgraph, and get a GraphQL API that serves historical data instantly.
The Graph是一个去中心化索引协议。你可以定义事件处理方式、部署子图,然后获得一个能即时提供历史数据的GraphQL API。

When to Use The Graph

何时使用The Graph

  • Any dApp that needs historical data (activity feeds, transaction history)
  • Leaderboards, rankings, analytics dashboards
  • NFT collection browsers (who owns what, transfer history)
  • DeFi dashboards (position history, PnL tracking)
  • Any query that would require scanning more than ~10K blocks
  • 任何需要历史数据的dApp(活动流、交易历史)
  • 排行榜、排名、分析仪表盘
  • NFT集合浏览器(资产归属、转账历史)
  • DeFi仪表盘(仓位历史、盈亏追踪)
  • 任何需要扫描超过约1万个区块的查询

How It Works

工作原理

  1. Define a schema — what entities you want to query
  2. Write mappings — TypeScript handlers that process events into entities
  3. Deploy — subgraph indexes all historical events and stays synced
  1. 定义Schema——确定要查询的实体
  2. 编写映射——用TypeScript处理器将事件转换为实体
  3. 部署——子图会索引所有历史事件并保持同步

Example: NFT Collection Subgraph

示例:NFT集合子图

schema.graphql:
graphql
type Token @entity {
  id: ID!
  tokenId: BigInt!
  owner: Bytes!
  mintedAt: BigInt!
  transfers: [Transfer!]! @derivedFrom(field: "token")
}

type Transfer @entity {
  id: ID!
  token: Token!
  from: Bytes!
  to: Bytes!
  timestamp: BigInt!
  blockNumber: BigInt!
}
mapping.ts:
typescript
import { Transfer as TransferEvent } from './generated/MyNFT/MyNFT';
import { Token, Transfer } from './generated/schema';

export function handleTransfer(event: TransferEvent): void {
  let tokenId = event.params.tokenId.toString();

  // Create or update token entity
  let token = Token.load(tokenId);
  if (token == null) {
    token = new Token(tokenId);
    token.tokenId = event.params.tokenId;
    token.mintedAt = event.block.timestamp;
  }
  token.owner = event.params.to;
  token.save();

  // Create transfer record
  let transfer = new Transfer(
    event.transaction.hash.toHex() + '-' + event.logIndex.toString()
  );
  transfer.token = tokenId;
  transfer.from = event.params.from;
  transfer.to = event.params.to;
  transfer.timestamp = event.block.timestamp;
  transfer.blockNumber = event.block.number;
  transfer.save();
}
Query the subgraph:
graphql
{
  tokens(where: { owner: "0xAlice..." }, first: 100) {
    tokenId
    mintedAt
    transfers(orderBy: timestamp, orderDirection: desc, first: 5) {
      from
      to
      timestamp
    }
  }
}
schema.graphql:
graphql
type Token @entity {
  id: ID!
  tokenId: BigInt!
  owner: Bytes!
  mintedAt: BigInt!
  transfers: [Transfer!]! @derivedFrom(field: "token")
}

type Transfer @entity {
  id: ID!
  token: Token!
  from: Bytes!
  to: Bytes!
  timestamp: BigInt!
  blockNumber: BigInt!
}
mapping.ts:
typescript
import { Transfer as TransferEvent } from './generated/MyNFT/MyNFT';
import { Token, Transfer } from './generated/schema';

export function handleTransfer(event: TransferEvent): void {
  let tokenId = event.params.tokenId.toString();

  // 创建或更新token实体
  let token = Token.load(tokenId);
  if (token == null) {
    token = new Token(tokenId);
    token.tokenId = event.params.tokenId;
    token.mintedAt = event.block.timestamp;
  }
  token.owner = event.params.to;
  token.save();

  // 创建转账记录
  let transfer = new Transfer(
    event.transaction.hash.toHex() + '-' + event.logIndex.toString()
  );
  transfer.token = tokenId;
  transfer.from = event.params.from;
  transfer.to = event.params.to;
  transfer.timestamp = event.block.timestamp;
  transfer.blockNumber = event.block.number;
  transfer.save();
}
查询子图:
graphql
{
  tokens(where: { owner: "0xAlice..." }, first: 100) {
    tokenId
    mintedAt
    transfers(orderBy: timestamp, orderDirection: desc, first: 5) {
      from
      to
      timestamp
    }
  }
}

Deploying a Subgraph

部署子图

bash
undefined
bash
undefined

Install

安装

npm install -g @graphprotocol/graph-cli
npm install -g @graphprotocol/graph-cli

Initialize from contract ABI

从合约ABI初始化

graph init --studio my-subgraph
graph init --studio my-subgraph

Generate types from schema

根据Schema生成类型

graph codegen
graph codegen

Build

构建

graph build
graph build

Deploy to Subgraph Studio

部署到Subgraph Studio

graph deploy --studio my-subgraph

**Subgraph Studio** (studio.thegraph.com) — development and testing environment. Free during development. Publish to the decentralized network for production.

---
graph deploy --studio my-subgraph

**Subgraph Studio**(studio.thegraph.com)——开发和测试环境。开发阶段免费。生产环境可发布到去中心化网络。

---

Alternative Indexing Solutions

替代索引方案

SolutionBest forTradeoffs
The GraphProduction dApp backends, decentralizedGraphQL API, requires subgraph development
Dune AnalyticsDashboards, analytics, ad-hoc queriesSQL interface, great visualization, not for app backends
Alchemy/QuickNode APIsQuick token/NFT queries
getTokenBalances
,
getNFTs
,
getAssetTransfers
— fast but centralized
Etherscan/Blockscout APIsSimple event log queriesRate-limited, not for high-volume
PonderTypeScript-first indexingLocal-first, simpler than The Graph for single-app use
Direct RPCReal-time current state onlyOnly for current state reads, not historical
方案最佳适用场景权衡点
The Graph生产级dApp后端、去中心化场景提供GraphQL API,需要开发子图
Dune Analytics仪表盘、分析、临时查询SQL界面、可视化能力强,不适合作为应用后端
Alchemy/QuickNode APIs快速查询代币/NFT信息
getTokenBalances
getNFTs
getAssetTransfers
——速度快但中心化
Etherscan/Blockscout APIs简单事件日志查询有速率限制,不适合高流量场景
Ponder优先使用TypeScript的索引场景本地优先,单应用场景下比The Graph更简单
直接RPC调用仅实时当前状态读取仅适用于当前状态读取,不支持历史数据

Dune Analytics

Dune Analytics

Write SQL queries over decoded onchain data. Best for analytics and dashboards, not for app backends.
sql
-- Top 10 buyers on your marketplace (last 30 days)
SELECT
    buyer,
    COUNT(*) as purchases,
    SUM(price / 1e18) as total_eth_spent
FROM mycontract_ethereum.Marketplace_evt_Sold
WHERE evt_block_time > NOW() - INTERVAL '30' DAY
GROUP BY buyer
ORDER BY total_eth_spent DESC
LIMIT 10
通过SQL查询解码后的链上数据。最适合分析和仪表盘,不适合作为应用后端。
sql
undefined

Enhanced Provider APIs

你的交易平台过去30天的Top 10买家

For common queries, provider APIs are faster than building a subgraph:
typescript
// Alchemy: get all tokens held by an address
const balances = await alchemy.core.getTokenBalances(address);

// Alchemy: get all NFTs owned by an address
const nfts = await alchemy.nft.getNftsForOwner(address);

// Alchemy: get transfer history
const transfers = await alchemy.core.getAssetTransfers({
  fromAddress: address,
  category: ['erc20', 'erc721'],
});

SELECT buyer, COUNT(*) as purchases, SUM(price / 1e18) as total_eth_spent FROM mycontract_ethereum.Marketplace_evt_Sold WHERE evt_block_time > NOW() - INTERVAL '30' DAY GROUP BY buyer ORDER BY total_eth_spent DESC LIMIT 10
undefined

Reading Current State (Not Historical)

增强型Provider API

For current balances, allowances, and contract state, direct RPC reads are fine. No indexer needed.
对于常见查询,Provider API比构建子图更快:
typescript
// Alchemy: 获取地址持有的所有代币
const balances = await alchemy.core.getTokenBalances(address);

// Alchemy: 获取地址拥有的所有NFT
const nfts = await alchemy.nft.getNftsForOwner(address);

// Alchemy: 获取转账历史
const transfers = await alchemy.core.getAssetTransfers({
  fromAddress: address,
  category: ['erc20', 'erc721'],
});

Single Reads

读取当前状态(非历史数据)

typescript
import { createPublicClient, http } from 'viem';

const client = createPublicClient({ chain: mainnet, transport: http() });

// Read current balance
const balance = await client.readContract({
  address: tokenAddress,
  abi: erc20Abi,
  functionName: 'balanceOf',
  args: [userAddress],
});
对于当前余额、授权额度和合约状态,直接RPC读取即可,无需索引器。

Batch Reads with Multicall

单次读取

For multiple reads in one RPC call, use Multicall3 (deployed at the same address on every chain):
typescript
// Multicall3: 0xcA11bde05977b3631167028862bE2a173976CA11
// Same address on Ethereum, Arbitrum, Optimism, Base, Polygon, and 50+ chains

const results = await client.multicall({
  contracts: [
    { address: tokenA, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
    { address: tokenB, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
    { address: tokenC, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
    { address: vault, abi: vaultAbi, functionName: 'totalAssets' },
  ],
});
// One RPC call instead of four
typescript
import { createPublicClient, http } from 'viem';

const client = createPublicClient({ chain: mainnet, transport: http() });

// 读取当前余额
const balance = await client.readContract({
  address: tokenAddress,
  abi: erc20Abi,
  functionName: 'balanceOf',
  args: [userAddress],
});

Real-Time Updates

用Multicall批量读取

For live updates, subscribe to new events via WebSocket:
typescript
import { createPublicClient, webSocket } from 'viem';

const client = createPublicClient({
  chain: mainnet,
  transport: webSocket('wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'),
});

// Watch for new sales in real-time
const unwatch = client.watchContractEvent({
  address: marketplaceAddress,
  abi: marketplaceAbi,
  eventName: 'Sold',
  onLogs: (logs) => {
    for (const log of logs) {
      console.log(`Sale: listing ${log.args.listingId} for ${log.args.price}`);
    }
  },
});

如需在一次RPC调用中完成多次读取,可使用Multicall3(在所有链上的部署地址相同):
typescript
// Multicall3: 0xcA11bde05977b3631167028862bE2a173976CA11
// 在以太坊、Arbitrum、Optimism、Base、Polygon等50+条链上地址相同

const results = await client.multicall({
  contracts: [
    { address: tokenA, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
    { address: tokenB, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
    { address: tokenC, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
    { address: vault, abi: vaultAbi, functionName: 'totalAssets' },
  ],
});
// 一次RPC调用替代四次调用

Common Patterns

实时更新

What you needHow to get it
Activity feed for a dAppEmit events → index with The Graph → query via GraphQL
Token balances for a userAlchemy
getTokenBalances
or Multicall
NFT collection browserThe Graph subgraph or Alchemy
getNftsForContract
Price historyDune Analytics or DEX subgraphs
Real-time new eventsWebSocket subscription via viem
Historical transaction listThe Graph or Alchemy
getAssetTransfers
Dashboard / analyticsDune Analytics (SQL + charts)
Protocol TVL trackingDeFiLlama API or custom subgraph
如需实时更新,可通过WebSocket订阅新事件:
typescript
import { createPublicClient, webSocket } from 'viem';

const client = createPublicClient({
  chain: mainnet,
  transport: webSocket('wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'),
});

// 实时监控新交易
const unwatch = client.watchContractEvent({
  address: marketplaceAddress,
  abi: marketplaceAbi,
  eventName: 'Sold',
  onLogs: (logs) => {
    for (const log of logs) {
      console.log(`交易完成:Listing ${log.args.listingId},价格${log.args.price}`);
    }
  },
});

常见模式

需求实现方案
dApp活动流发射事件 → 用The Graph索引 → 通过GraphQL查询
用户代币余额Alchemy
getTokenBalances
或Multicall
NFT集合浏览器The Graph子图或Alchemy
getNftsForContract
价格历史Dune Analytics或DEX子图
实时新事件通过viem订阅WebSocket
历史交易列表The Graph或Alchemy
getAssetTransfers
仪表盘/分析Dune Analytics(SQL+图表)
协议TVL追踪DeFiLlama API或自定义子图