indexing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOnchain Data & Indexing
链上数据与索引
What You Probably Got Wrong
你可能踩过的误区
You try to query historical state via RPC calls. You can't cheaply read past state. reads current state. Reading state at a historical block requires an archive node (expensive, slow). For historical data, you need an indexer.
eth_callYou loop through blocks looking for events. Scanning millions of blocks with 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.
eth_getLogsYou 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你遍历区块查找事件。用扫描数百万个区块的时间复杂度是O(n)——会超时、触发速率限制,或消耗大量RPC额度。请使用已处理过所有区块的索引器。
eth_getLogs你将查询结果存储在链上。排行榜、活动流、分析数据——这些都属于链下范畴。在链下计算、索引事件。如果需要链上承诺,只需存储哈希值即可。
你不了解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 — , , , . Don't index large values or values you won't filter on.
sellerbuyertokenContractlistingId每次状态变更都应发射事件。这不仅是最佳实践——更是你的前端、索引器和区块浏览器了解链上情况的方式。
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——如、、、。不要为大值或无需过滤的值添加索引。
sellerbuyertokenContractlistingIdReading 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
工作原理
- Define a schema — what entities you want to query
- Write mappings — TypeScript handlers that process events into entities
- Deploy — subgraph indexes all historical events and stays synced
- 定义Schema——确定要查询的实体
- 编写映射——用TypeScript处理器将事件转换为实体
- 部署——子图会索引所有历史事件并保持同步
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
undefinedbash
undefinedInstall
安装
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
替代索引方案
| Solution | Best for | Tradeoffs |
|---|---|---|
| The Graph | Production dApp backends, decentralized | GraphQL API, requires subgraph development |
| Dune Analytics | Dashboards, analytics, ad-hoc queries | SQL interface, great visualization, not for app backends |
| Alchemy/QuickNode APIs | Quick token/NFT queries | |
| Etherscan/Blockscout APIs | Simple event log queries | Rate-limited, not for high-volume |
| Ponder | TypeScript-first indexing | Local-first, simpler than The Graph for single-app use |
| Direct RPC | Real-time current state only | Only for current state reads, not historical |
| 方案 | 最佳适用场景 | 权衡点 |
|---|---|---|
| The Graph | 生产级dApp后端、去中心化场景 | 提供GraphQL API,需要开发子图 |
| Dune Analytics | 仪表盘、分析、临时查询 | SQL界面、可视化能力强,不适合作为应用后端 |
| Alchemy/QuickNode APIs | 快速查询代币/NFT信息 | |
| 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
undefinedEnhanced 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
undefinedReading 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 fourtypescript
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 need | How to get it |
|---|---|
| Activity feed for a dApp | Emit events → index with The Graph → query via GraphQL |
| Token balances for a user | Alchemy |
| NFT collection browser | The Graph subgraph or Alchemy |
| Price history | Dune Analytics or DEX subgraphs |
| Real-time new events | WebSocket subscription via viem |
| Historical transaction list | The Graph or Alchemy |
| Dashboard / analytics | Dune Analytics (SQL + charts) |
| Protocol TVL tracking | DeFiLlama 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 |
| NFT集合浏览器 | The Graph子图或Alchemy |
| 价格历史 | Dune Analytics或DEX子图 |
| 实时新事件 | 通过viem订阅WebSocket |
| 历史交易列表 | The Graph或Alchemy |
| 仪表盘/分析 | Dune Analytics(SQL+图表) |
| 协议TVL追踪 | DeFiLlama API或自定义子图 |