mcpField
I
indexing

indexing

github.com/austintgriffithデータ・データベース30239 回インストール3 日前に更新
リポジトリ

indexing について

dApp 開発者向けのオンチェーンデータとインデックス作成のガイドです。イベント設計、履歴クエリ、The Graph や Dune などの選択肢を説明します。

主な機能

  1. 01イベントを活動履歴データの基盤として利用
  2. 02The Graph のサブグラフと GraphQL クエリの解説
  3. 03複数のオンチェーンインデックス方式の比較
  4. 04現在状態の読み取りと一括呼び出し
  5. 05WebSocket によるリアルタイムイベント購読

活用シーン

  1. 01dApp のアクティビティフィードと取引履歴の構築
  2. 02ランキングやオンチェーン分析ダッシュボードの作成
  3. 03NFT の所有者と移転履歴のインデックス化
  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 +データ 1 バイトごとに 8 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 コレクションブラウザー(所有者、転送履歴など)
  • 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();

  // 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シンプルなイベントログのクエリレート制限があり、大量処理には不向き
PonderTypeScript を優先したインデックス作成ローカル優先で、単一アプリでの利用なら The Graph よりシンプル
Direct 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
拡張プロバイダー API

一般的なクエリでは、プロバイダー 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 による一括読み取り

複数の読み取りを 1 回の 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 またはカスタムサブグラフ

コメント