链上数据索引Skill indexing

这个技能用于读取和查询区块链上的数据,包括事件处理、使用The Graph进行索引、以及替代直接循环区块的方法。它涉及设计事件优先的智能合约、部署子图、使用各种索引解决方案如Dune Analytics和Alchemy APIs,以及实时状态读取。关键词:区块链、链上数据、索引、The Graph、dApp、事件、智能合约、数据查询、去中心化应用。

DApp开发 0 次安装 0 次浏览 更新于 3/24/2026

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 — sellerbuyertokenContractlistingId。不要索引大值或你不会过滤的值。

直接读取事件(小规模)

对于最近的事件或低交易量合约,你可以通过 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 区块的查询

工作原理

  1. 定义模式 — 你想查询的实体
  2. 编写映射 — TypeScript 处理程序,将事件处理为实体
  3. 部署 — 子图索引所有历史事件并保持同步

示例: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 查询 getTokenBalancesgetNFTsgetAssetTransfers — 快速但集中化
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 或自定义子图