mcpField
I
indexing

indexing

github.com/austintgriffith数据与数据库30239 次安装3 天前更新
代码仓库

关于 indexing

面向链上应用开发者的数据与索引指南,讲解事件设计、历史数据查询和索引方案选择,覆盖 The Graph、Dune 等工具。

主要功能

  1. 01采用事件作为历史活动数据来源
  2. 02介绍 The Graph 子图与 GraphQL 查询
  3. 03比较多种链上索引方案
  4. 04说明当前状态读取与批量调用
  5. 05使用 WebSocket 订阅实时事件

适用场景

  1. 01构建应用活动动态与交易历史
  2. 02制作排行榜和链上分析面板
  3. 03为 NFT 浏览器索引持有者与转移记录
  4. 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 个区块的查询
工作原理
  1. 定义 schema——你要查询哪些实体
  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();

  // 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快速查询代币/NFTgetTokenBalances、getNFTs、getAssetTransfers——速度快,但服务集中化
Etherscan/Blockscout APIs简单的事件日志查询有速率限制,不适用于高流量场景
PonderTypeScript 优先的索引本地优先,适用于单个应用时比 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 或自定义子图

评论

支持 Markdown