关于 indexing
面向链上应用开发者的数据与索引指南,讲解事件设计、历史数据查询和索引方案选择,覆盖 The Graph、Dune 等工具。
主要功能
- 01采用事件作为历史活动数据来源
- 02介绍 The Graph 子图与 GraphQL 查询
- 03比较多种链上索引方案
- 04说明当前状态读取与批量调用
- 05使用 WebSocket 订阅实时事件
适用场景
- 01构建应用活动动态与交易历史
- 02制作排行榜和链上分析面板
- 03为 NFT 浏览器索引持有者与转移记录
- 04选择历史数据和实时数据读取方式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ austintgriffith/ ethskills --skill indexing安装后在 Claude Code 中输入/indexing确认已加载。
详细介绍
链上数据与索引
你可能理解错的地方
你尝试通过 RPC 调用查询历史状态。 你无法低成本读取过去的状态。eth_call 读取的是当前状态。读取历史区块的状态需要归档节点(成本高、速度慢)。要查询历史数据,你需要索引器。
你循环遍历区块查找事件。 使用 eth_getLogs 扫描数百万个区块的复杂度为 O(n)——这会超时、触发速率限制,或耗尽大量 RPC 积分。使用已经处理过每个区块的索引器。
你把查询结果存储在链上。 排行榜、活动动态、分析数据——这些都应该放在链下。在链下计算,在链下索引事件。如果需要链上承诺,存储一个哈希值。
你不了解 The Graph。 The Graph 会将合约事件转换为可查询的 GraphQL API。所有成熟的 dApp 都通过它读取历史数据。Etherscan 使用索引器。Uniswap 使用索引器。你也应该使用。
你把事件当作可有可无的东西。 事件是读取链上历史活动的主要方式。如果合约不发出事件,就没人能基于它构建前端、仪表板或分析功能。设计合约时应以事件为先。
事件就是你的 API
Solidity 事件的发出成本很低(基础成本约为 375 gas + 每个索引主题 375 gas + 每字节数据 8 gas),链下读取则免费。事件存储在交易收据中,而非合约存储中,因此不会产生存储 gas 费用。
设计合约时以事件为先
每次状态变更都应发出事件。这不只是良好的实践——前端、索引器和区块浏览器都要靠它们了解发生了什么。
// ✅ 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);
}
}
索引你会用于筛选的字段。 每个事件最多有 3 个索引主题。将它们用于你会查询的地址和 ID——seller、buyer、tokenContract、listingId。不要索引大型值或不会用于筛选的值。
直接读取事件(小规模)
对于近期事件或低交易量合约,你可以直接通过 RPC 读取事件:
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',
});
适用于: 最近几千个区块、低交易量合约、实时监控。 不适用于: 历史查询、高交易量合约,以及任何需要扫描超过约 10K 个区块的情况。
The Graph(子图)
The Graph 是一个去中心化索引协议。你可以定义事件的处理方式、部署子图,并获得一个能即时提供历史数据的 GraphQL API。
何时使用 The Graph
- 任何需要历史数据的 dApp(活动动态、交易历史)
- 排行榜、排名、分析仪表板
- NFT 收藏集浏览器(谁拥有哪些 NFT、转移历史)
- DeFi 仪表板(持仓历史、PnL 跟踪)
- 任何需要扫描超过约 10K 个区块的查询
工作原理
- 定义 schema——你要查询哪些实体
- 编写映射——用于将事件处理为实体的 TypeScript 处理程序
- 部署——子图会索引所有历史事件,并保持同步
示例:NFT 收藏集子图
schema.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:
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();
}
查询子图:
{
tokens(where: { owner: "0xAlice..." }, first: 100) {
tokenId
mintedAt
transfers(orderBy: timestamp, orderDirection: desc, first: 5) {
from
to
timestamp
}
}
}
部署子图
# Install
npm install -g @graphprotocol/graph-cli
# Initialize from contract ABI
graph init --studio my-subgraph
# Generate types from schema
graph codegen
# Build
graph build
# Deploy to Subgraph Studio
graph deploy --studio my-subgraph
Subgraph Studio(studio.thegraph.com)——用于开发和测试的环境。开发期间免费。发布到去中心化网络以用于生产环境。
替代索引方案
| 方案 | 最适合 | 权衡 |
|---|---|---|
| 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
使用 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
增强型 Provider API
对于常见查询,Provider API 比构建子图更快:
// 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'],
});
读取当前状态(非历史数据)
对于当前余额、授权额度和合约状态,直接读取 RPC 即可,无需索引器。
单次读取
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],
});
使用 Multicall 批量读取
要在一次 RPC 调用中执行多次读取,请使用 Multicall3(在每条链上都部署于相同地址):
// 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
实时更新
如需实时更新,可通过 WebSocket 订阅新事件:
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}`);
}
},
});
常见模式
| 所需内容 | 获取方式 |
|---|---|
| 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 或自定义子图 |