indexing について
dApp 開発者向けのオンチェーンデータとインデックス作成のガイドです。イベント設計、履歴クエリ、The Graph や Dune などの選択肢を説明します。
主な機能
- 01イベントを活動履歴データの基盤として利用
- 02The Graph のサブグラフと GraphQL クエリの解説
- 03複数のオンチェーンインデックス方式の比較
- 04現在状態の読み取りと一括呼び出し
- 05WebSocket によるリアルタイムイベント購読
活用シーン
- 01dApp のアクティビティフィードと取引履歴の構築
- 02ランキングやオンチェーン分析ダッシュボードの作成
- 03NFT の所有者と移転履歴のインデックス化
- 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 ブロックを超えるスキャンが必要になるあらゆるクエリ
仕組み
- スキーマを定義する — クエリ対象のエンティティを指定します
- マッピングを記述する — イベントをエンティティに処理する 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();
// 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 | シンプルなイベントログのクエリ | レート制限があり、大量処理には不向き |
| Ponder | TypeScript を優先したインデックス作成 | ローカル優先で、単一アプリでの利用なら 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 またはカスタムサブグラフ |