name: 索引 description: 如何读取和查询链上数据 — 事件、The Graph、索引模式。为什么不能直接循环区块,以及应该使用什么替代方法。
链上数据与索引
你可能犯的错误
你尝试通过 RPC 调用查询历史状态。 你不能廉价地读取过去的状态。eth_call 读取当前状态。读取历史区块的状态需要一个归档节点(昂贵、慢)。对于历史数据,你需要一个索引器。
你循环遍历区块查找事件。 使用 eth_getLogs 扫描数百万个区块是 O(n) 复杂度 — 它会超时、被限速,或在 RPC 积分上花费巨大。使用已经处理了每个区块的索引器。
你将查询结果存储在链上。 排行榜、活动动态、分析 — 这些应放在链下。在链下计算,索引事件在链下。如果你需要链上承诺,存储一个哈希值。
你不知道 The Graph。 The Graph 将你的合约事件转换为可查询的 GraphQL API。这是每个严肃的 dApp 读取历史数据的方式。Etherscan 使用索引器。Uniswap 使用索引器。你也应该如此。
你将事件视为可选的。 事件是读取历史链上活动的主要方式。如果你的合约不发出事件,没有人可以基于它构建前端、仪表板或分析。设计合约时以事件为先。
事件是你的 API
Solidity 事件发射便宜(约 375 gas 基础 + 375 每个索引主题 + 8 gas 每字节数据),并且在链下免费读取。它们存储在交易收据中,不在合约存储中,因此不消耗存储 gas。
以事件为先设计合约
每个状态变更都应发出一个事件。这不仅仅是好实践 — 这是你的前端、索引器和区块浏览器知道发生了什么的方式。
// ✅ 好 — 每个操作都发出可查询的事件
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。不要索引大值或你不会过滤的值。
直接读取事件(小规模)
对于最近的事件或低交易量合约,你可以通过 RPC 直接读取事件:
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',
});
这适用于: 最后几千个区块、低交易量合约、实时监控。 这不适用于: 历史查询、高交易量合约、任何扫描超过约 10K 区块的操作。
The Graph(子图)
The Graph 是一个去中心化索引协议。你定义如何处理事件,部署子图,并获取一个提供历史数据的 GraphQL API。
何时使用 The Graph
- 任何需要历史数据的 dApp(活动动态、交易历史)
- 排行榜、排名、分析仪表板
- NFT 收藏浏览器(谁拥有什么、转移历史)
- DeFi 仪表板(持仓历史、盈亏跟踪)
- 任何需要扫描超过约 10K 区块的查询
工作原理
- 定义模式 — 你想查询的实体
- 编写映射 — 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();
// 创建或更新令牌实体
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();
}
查询子图:
{
tokens(where: { owner: "0xAlice..." }, first: 100) {
tokenId
mintedAt
transfers(orderBy: timestamp, orderDirection: desc, first: 5) {
from
to
timestamp
}
}
}
部署子图
# 安装
npm install -g @graphprotocol/graph-cli
# 从合约 ABI 初始化
graph init --studio my-subgraph
# 从模式生成类型
graph codegen
# 构建
graph build
# 部署到 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 查询解码的链上数据。最适合分析和仪表板,不适用于应用后端。
-- 你的市场上前 10 买家(过去 30 天)
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
增强的提供商 API
对于常见查询,提供商 API 比构建子图更快:
// 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'],
});
读取当前状态(非历史)
对于当前余额、许可和合约状态,直接 RPC 读取没问题。不需要索引器。
单次读取
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],
});
批量读取使用 Multicall
对于一次 RPC 调用中的多个读取,使用 Multicall3(在每个链上部署在相同地址):
// 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 调用而不是四次
实时更新
对于实时更新,通过 WebSocket 订阅新事件:
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(`销售:列表 ${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 或自定义子图 |