The Graphハンズオン:Subgraphを作成してブロックチェーンデータを効率的に取得しよう
こんにちは、Komlock LabのWeb3エンジニアの阿部です!
本記事では、ハンズオンを通してThe Graphを理解していただくことをゴールとしています。
簡単なDAppsの開発(スマートコントラクト開発、フロント開発)はできる前提で説明を進めます。
もしDApps開発について興味がある方はこちらの記事をご参照ください。
1. The Graphとは?
The Graphは、ブロックチェーン上のデータを効率的にインデックス化し、検索・取得を容易にする分散型プロトコルです。
Ethereumをはじめとする複数のブロックチェーンで動作し、開発者が「サブグラフ」と呼ばれるデータインデックスを作成して、GraphQLを通じてデータ検索を実行できるようにします。
The Graph利用により、DApps開発者がブロックチェーンデータを迅速かつ簡単に利用できるようになります。
簡単に言うと、チェーンのトランザクションをウォッチして、イベントをリアルタイムで解析しデータをインデックスしてくれる、かつGraphQL形式でデータを取得可能にしてくれるツールです。
用例
DApps開発をしていると、特定のユーザーの取引履歴を取得したい場面があります。
その際にジェネシスブロックから遡ると、検索に時間がかかってしまいます。
そのような時に、役に立つのがThe Graphです。The Graphはデータを効率的に検索・取得ができるようになっています。
取引履歴をインデックスしておけば、ジェネシスブロックから遡るのに比べ、高速で取引履歴の取得が可能になります。
もっと詳しく知りたい方は、公式サイトをご確認ください。
2. ハンズオン準備:必要なツールと環境構築
本ハンズオンのゴール
- トークンの送金をThe Graphを利用してインデックス化できている
- インデックス化したデータをThe Graphを利用して取得できている
必要なツール
- 以下のツールをインストールしてください
- 以下を参考にAlchemyでAPI Keyを取得してください。
https://note.com/standenglish/n/n8b5f7712a151
スマートコントラクトの準備
以下の手順でERC20トークンコントラクトを作成・デプロイしてください。
詰まった時は以下のgithubレポジトリを参考にしてください。
-
Hardhatプロジェクトをセットアップ
mkdir my-token && cd my-token npx hardhat init ✔ What do you want to do? · Create a JavaScript project ✔ Hardhat project root: · /Users/takumaabe/workspace/my-token ✔ Do you want to add a .gitignore? (Y/n) · y ✔ Do you want to install this sample project's dependencies with npm (hardhat @nomicfoundation/hardhat-toolbox)? (Y/n) · y
不要なSolidityファイルを削除してください
rm contracts/Lock.sol rm -rf ignition rm -rf test
ワークスペースを分割してください
mkdir packages mv contracts packages/contracts mv hardhat.config.js packages/contracts/hardhat.config.js
package.jsonを以下のように修正してください
{ "name": "mytoken", "private": true, "workspaces": { "packages": [ "packages/*" ] }, }
packages/contracts/package.jsonを以下のように修正してください
{ "name": "contract", "version": "1.0.0", "private": true, "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "1.0.6", "@nomicfoundation/hardhat-network-helpers": "1.0.8", "@nomicfoundation/hardhat-toolbox": "2.0.2", "@nomiclabs/hardhat-ethers": "2.2.2", "@nomiclabs/hardhat-etherscan": "3.1.7", "@openzeppelin/contracts": "^5.1.0", "@typechain/ethers-v5": "10.2.0", "@typechain/hardhat": "6.1.5", "chai": "4.3.7", "ethers": "^5.7.2", "hardhat": "^2.22.17" "hardhat-gas-reporter": "1.0.9", "solidity-coverage": "0.8.2", "typechain": "8.1.1", "dotenv": "16.1.3" } }
-
ERC20トークンコントラクトを追加:
以下packages/contracts/contracts/MyToken.sol
を追加してください// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MyToken is ERC20 { constructor() ERC20("MyToken", "MTK") { _mint(msg.sender, 1000000 * 10 ** decimals()); } }
-
コントラクトをsepoliaネットワークにデプロイし、アドレスとABIをメモする
-
packages/contracts/hardhat.config.jsにnetworksを追加してください
require("@nomicfoundation/hardhat-toolbox"); require("dotenv").config(); const { PRIVATE_KEY, STAGING_ALCHEMY_KEY } = process.env; module.exports = { solidity: "0.8.20", networks: { sepolia: { url: STAGING_ALCHEMY_KEY || "", accounts: PRIVATE_KEY ? [PRIVATE_KEY] : ["0".repeat(64)], }, }, };
-
packages/contracts/.envを追加してくだしあ
PRIVATE_KEY=YOUR_PRIVATE_KEY STAGING_ALCHEMY_KEY=YOUR_STAGING_ALCHEMY_KEY
-
packages/contracts/package.jsonに以下を追加してください
"scripts": { "deploy": "npx hardhat run scripts/deploy.js --network sepolia" }
-
contracts/scripts/deploy.jsを追加してください
async function main() { const MyToken = await ethers.getContractFactory("MyToken"); console.log("Deploying MyToken..."); const myToken = await MyToken.deploy(); await myToken.deployed(); console.log("MyToken deployed to:", myToken.address); } const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.error(error); process.exit(1); } }; runMain();
-
以下のコマンドを実行してください
cd packages/contracts yarn run deploy
実行後に表示されるデプロイされたスマートコントラクトのアドレスをメモしてください。
-
3. Subgraphの作成
Subgraphプロジェクトの初期化
以下のコマンドで新しいSubgraphプロジェクトを作成してください
cd <my-tokenのrootディレクトリ>/packages
graph init my-token
? Network … Ethereum Sepolia Testnet · sepolia · https://sepolia.etherscan.io
✔ Network · Ethereum Sepolia Testnet · sepolia · https://sepolia.etherscan.io
✔ Source · Smart Contract · ethereum
✔ Subgraph slug · my-token
✔ Directory to create the subgraph in · my-token
✔ Contract address · YOUR_CONTRACT_ADDRESS
✖ Failed to fetch ABI: Error: NOTOK - Contract source code not verified
✔ Do you want to retry? (Y/n) · false
✔ Fetching start block from contract API...
✔ Fetching contract name from contract API...
✔ ABI file (path) · /xxxxx/my-token/packages/contracts/artifacts/contracts/MyToken.sol/MyToken.json
✔ Start block · 7385602
✔ Contract name · MyToken
✔ Index contract events as entities (Y/n) · true
Generate subgraph
Write subgraph to directory
✔ Create subgraph scaffold
✔ Initialize networks config
✔ Initialize subgraph repository
✔ Install dependencies with yarn
✔ Generate ABI and schema types with yarn codegen
✔ Add another contract? (y/N) · false
これにより、以下のディレクトリ構造が生成されます。
my-token/
├── subgraph.yaml
├── schema.graphql
├── src/
│ └── my-token.ts
└── package.json
Schema定義の修正
schema.graphql
に以下のスキーマを記載します。
スキーマに記載されたエンティティは、DBで言うところのテーブルに相当します。
type Transfer @entity(immutable: true) {
id: Bytes!
from: Bytes! # address
to: Bytes! # address
value: BigInt! # uint256
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
イベントハンドラー設定の確認
subgraph.yamlにイベントハンドラーの設定が記載されています。
specVersion: 1.0.0
indexerHints:
prune: auto
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum
name: MyToken
network: sepolia
source:
address: "xxxxxx"
abi: MyToken
startBlock: 7385602
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Approval
- Transfer
abis:
- name: MyToken
file: ./abis/MyToken.json
eventHandlers:
- event: Approval(indexed address,indexed address,uint256)
handler: handleApproval
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
file: ./src/my-token.ts
こちらはgraph initにて、自動でコードが生成されています。
データマッピング:イベント処理ロジックの確認
src/my-token.ts
に以下のコードが記載されています。
import {
Approval as ApprovalEvent,
Transfer as TransferEvent
} from "../generated/MyToken/MyToken"
import { Approval, Transfer } from "../generated/schema"
export function handleApproval(event: ApprovalEvent): void {
let entity = new Approval(
event.transaction.hash.concatI32(event.logIndex.toI32())
)
entity.owner = event.params.owner
entity.spender = event.params.spender
entity.value = event.params.value
entity.blockNumber = event.block.number
entity.blockTimestamp = event.block.timestamp
entity.transactionHash = event.transaction.hash
entity.save()
}
export function handleTransfer(event: TransferEvent): void {
let entity = new Transfer(
event.transaction.hash.concatI32(event.logIndex.toI32())
)
entity.from = event.params.from
entity.to = event.params.to
entity.value = event.params.value
entity.blockNumber = event.block.number
entity.blockTimestamp = event.block.timestamp
entity.transactionHash = event.transaction.hash
entity.save()
}
今回重要なのはhandleTransferです。
こちらのコードによって、送金履歴がThe Graphを通じて、イベントがインデックス化されます。
4. Subgraphのデプロイ
Subgraph Studioにログインし、サブグラフを作成してください
1.2. 作成したサブグラフ詳細画面に遷移してください
3. AUTH & DEPLOYにしたがって、サブグラフをデプロイしてください
6. フロントエンド実装
以下は、client
というディレクトリを作成し、その中でReactを使ったクライアントアプリケーションを構築する手順です。このアプリケーションでは、Apollo Clientを使用してGraphQLクエリを実行し、The Graphのサブグラフから取引履歴を取得して表示します。
1. ディレクトリの作成
まず、プロジェクトのルートディレクトリにclient
という新しいディレクトリを作成します。
mkdir client
cd client
2. Reactアプリケーションの初期化
以下のコマンドでReactアプリケーションを作成してください
npx create-react-app .
3. Apollo ClientとGraphQLのインストール
Apollo ClientとGraphQLライブラリをインストールしてください
yarn add @apollo/client graphql
5. Apollo Clientの設定ファイル
GraphQLエンドポイントに接続するために、Apollo Clientを設定してください
src/apolloClient.js
コード例: import { ApolloClient, InMemoryCache } from "@apollo/client";
const client = new ApolloClient({
uri: "https://api.studio.thegraph.com/query/USERID/mytoken/latest", // thegraphのサブグラフのエンドポイント
cache: new InMemoryCache(),
});
export default client;
-
uri
: サブグラフのGraphQLエンドポイント(The Graph Studioで確認可能)
https://thegraph.com/studio/subgraph/my-token/endpoints
6. ApolloProviderでアプリ全体をラップ
Reactアプリ全体でApollo Clientを使用できるように、ApolloProvider
でラップしてください
src/index.js
コード例: import { ApolloProvider } from "@apollo/client";
import React from 'react';
import ReactDOM from 'react-dom/client';
import client from "./apolloClient";
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
</React.StrictMode>
);
7. 送金、データ取得Reactコンポーネントを作成
- TransferHistory.js
import { gql, useQuery } from "@apollo/client";
import React from "react";
const TRANSFER_HISTORY_QUERY = gql`
query GetTransferHistory($recipient: Bytes!) {
transfers(where: { to: $recipient }) {
id
from
to
value
}
}
`;
const TransferHistory = ({ recipient }) => {
const { loading, error, data } = useQuery(TRANSFER_HISTORY_QUERY, {
variables: { recipient },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h2>トランスファー履歴</h2>
<ul>
{data.transfers.map((transfer) => (
<li key={transfer.id}>
From: {transfer.from} | To: {transfer.to} | Value:{" "}
{parseFloat(transfer.value) / Math.pow(10, 18)} MTK
</li>
))}
</ul>
</div>
);
};
export default TransferHistory;
- TransferForm.js
import { ethers } from "ethers";
import React, { useState } from "react";
import MyTokenABI from "../abis/MyToken.json";
const TransferForm = ({ contractAddress }) => {
const [recipient, setRecipient] = useState("");
const [amount, setAmount] = useState("");
const handleTransfer = async () => {
if (!window.ethereum) return alert("MetaMaskがインストールされていません");
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const contract = new ethers.Contract(contractAddress, MyTokenABI, signer);
try {
const tx = await contract.transfer(recipient, ethers.utils.parseUnits(amount, 18));
await tx.wait();
alert("送金が成功しました");
} catch (error) {
console.error(error);
alert("送金に失敗しました");
}
};
return (
<div>
<h2>トークン送金</h2>
<input
type="text"
placeholder="受取人アドレス"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
/>
<input
type="text"
placeholder="送金額"
value={amount}
onChange={(e) => setAmount(e.target.value)}
/>
<button onClick={handleTransfer}>送金</button>
</div>
);
};
export default TransferForm;
- TokenBalance.js
import { ethers } from "ethers";
import React, { useEffect, useState } from "react";
import MyTokenABI from "../abis/MyToken.json"; // ABIファイル
const TokenBalance = ({ contractAddress }) => {
const [balance, setBalance] = useState(0);
const [account, setAccount] = useState("");
useEffect(() => {
const fetchBalance = async () => {
if (!window.ethereum) return alert("MetaMaskがインストールされていません");
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const signer = provider.getSigner();
const userAddress = await signer.getAddress();
setAccount(userAddress);
const contract = new ethers.Contract(contractAddress, MyTokenABI, signer);
const balance = await contract.balanceOf(userAddress);
setBalance(ethers.utils.formatUnits(balance, 18));
};
fetchBalance();
}, [contractAddress]);
return (
<div>
<h2>トークン残高</h2>
<p>アカウント: {account}</p>
<p>残高: {balance} MTK</p>
</div>
);
};
export default TokenBalance;
8. メインコンポーネントに組み込む
packages/client/src/App.js
import { ApolloProvider } from "@apollo/client";
import React, { useEffect, useState } from "react";
import client from "./apolloClient";
import TokenBalance from "./components/TokenBalance";
import TransferForm from "./components/TransferForm";
import TransferHistory from "./components/TransferHistory";
const CONTRACT_ADDRESS = "YOUR CONTRACT_ADDRESS";
function App() {
const [account, setAccount] = useState("");
// MetaMaskのアカウントを取得
const connectWallet = async () => {
try {
const { ethereum } = window;
if (!ethereum) {
console.error("Get MetaMask!");
return;
}
const accounts = await ethereum.request({
method: "eth_requestAccounts",
});
console.log("Connected: ", accounts[0]);
setAccount(accounts[0]);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
connectWallet();
}, []);
return (
<ApolloProvider client={client}>
<div className="App">
<h1>MyToken Dashboard</h1>
<TokenBalance contractAddress={CONTRACT_ADDRESS} />
<TransferForm contractAddress={CONTRACT_ADDRESS} />
<TransferHistory recipient={account} />
</div>
</ApolloProvider>
);
}
export default App;
9. packages/client/src/abis/MyToken.jsonを作成
mkdir packages/client/src/abis
cp packages/contracts/artifacts/contracts/MyToken.sol/MyToken.json packages/client/src/abis/MyToken.json
10. アプリケーションの実行
1. 開発サーバーの起動
以下のコマンドで開発サーバーを起動してください
yarn start
2. ブラウザで確認
ブラウザでhttp://localhost:3000にアクセスし、取引履歴が表示されることを確認してください。
「トークン送金」の
- 受取人アドレス: 自ウォレットのアドレス
- 送金額: 10000000以下の数
を入力の後「送金」ボタンを押下してください。
数分後、取引履歴に入力した内容が表示されることを確認してください。
以上がTheGraphハンズオンになります!
お疲れさまでした!
こちらのハンズオンが皆さんのスキルアップの一助になれば幸いです!
もっとスキルアップをしたいという方は、是非是非Komlockが運営している、
Komlock Discordにご参加ください!
Web3の最新情報についてやり取りをしたり、Web3イベントに関しての共有を行なっています。
Discussion