目次
はじめに
Solidityで作成したスマートコントラクトは、それだけではユーザーが直接操作することは難しいです。分散型アプリケーション(DApp)として機能させるためには、ウェブサイトなどのフロントエンドと接続する必要があります。これにより、ユーザーは使い慣れたインターフェースを通じて、ブロックチェーン上のスマートコントラクトと対話できるようになります。
このステップでは、人気のフロントエンドフレームワークであるReactとVueを使って、デプロイ済みのスマートコントラクトに接続する方法を学びます。
準備するもの
フロントエンドとスマートコントラクトを接続するために、以下の準備が必要です。
- Node.js と npm/yarn: JavaScriptの実行環境とパッケージマネージャー。
- React または Vue の基本的な知識: コンポーネント、ステート、プロップスなどの基本概念を理解していること。
- デプロイ済みのスマートコントラクト: ブロックチェーンネットワーク(ローカル、テストネット、メインネット)にデプロイされていること。
- スマートコントラクトのABI(Application Binary Interface): コントラクトの関数やイベントの定義情報が記載されたJSONファイル。これは通常、コンパイル時に生成されます。
- スマートコントラクトのアドレス: コントラクトがデプロイされたネットワーク上のアドレス。
- ウォレット拡張機能: MetaMaskなどのブラウザ拡張機能ウォレット。ユーザーがトランザクションに署名したり、ネットワークに接続したりするために必要です。
接続の要:Web3ライブラリ (ethers.js / web3.js)
フロントエンド(JavaScript)からEthereumブロックチェーンやスマートコントラクトと通信するには、専用のライブラリが必要です。最も広く使われているのが ethers.js と web3.js です。
現在、ethers.js がよりモダンで、軽量、TypeScriptとの親和性も高いなどの理由から人気を集めています。このチュートリアルでは主に ethers.js を使用して解説します。
- 人気度: web3.jsは歴史が長く、多くのプロジェクトで使用されていますが、新しいプロジェクトではethers.jsが好まれる傾向があります。
- サイズ: ethers.jsはweb3.jsよりも軽量です。
- API設計: ethers.jsはよりシンプルで直感的と評価されることが多いです。
- 機能: ENS(Ethereum Name Service)のネイティブサポートなど、ethers.jsには便利な機能が組み込まれています。
- ライセンス: ethers.jsはMITライセンス、web3.jsはLGPLライセンスです。
どちらのライブラリも活発に開発されており、基本的な機能は網羅していますが、プロジェクトの要件や好みに合わせて選択すると良いでしょう。
プロジェクトのセットアップとライブラリのインストール
まず、ReactまたはVueのプロジェクトを作成し、ethers.jsをインストールします。
Reactの場合
# Create React App を使用してプロジェクトを作成
npx create-react-app my-dapp
cd my-dapp
# ethers.js をインストール
npm install ethers
# または
yarn add ethers
Vueの場合
# Vue CLI を使用してプロジェクトを作成 (Vue 3 推奨)
npm init vue@latest my-dapp
# または
yarn create vue my-dapp
# (プロンプトに従って設定を選択)
cd my-dapp
npm install
# または
yarn
# ethers.js をインストール
npm install ethers
# または
yarn add ethers
Ethereumへの接続
フロントエンドからEthereumネットワークに接続するには、「プロバイダー」が必要です。プロバイダーは、ブロックチェーンへの読み取りアクセスを提供します。MetaMaskなどのウォレット拡張機能がブラウザにインストールされている場合、それがプロバイダーとして機能します。
import { ethers } from "ethers";
async function connectWallet() { // MetaMask がインストールされているか確認 if (typeof window.ethereum !== 'undefined') { console.log('MetaMask is installed!'); try { // プロバイダーを取得 (MetaMask経由) const provider = new ethers.BrowserProvider(window.ethereum); // アカウントへのアクセスを要求 // eth_requestAccounts を使用すると、ユーザーに接続許可を求めるプロンプトが表示されます。 const accounts = await provider.send("eth_requestAccounts", []); const signer = await provider.getSigner(); // 書き込み操作用のサイナーを取得 const address = await signer.getAddress(); console.log("Connected account:", address); // ここで接続されたアカウント情報などをStateに保存する // ネットワーク変更やアカウント変更を検知するリスナーを設定することも推奨されます window.ethereum.on('accountsChanged', (newAccounts) => { console.log('Accounts changed:', newAccounts); // アカウント変更時の処理 }); window.ethereum.on('chainChanged', (chainId) => { console.log('Chain changed:', chainId); // ネットワーク変更時の処理 (ページのリロードなど) window.location.reload(); }); return { provider, signer, address }; } catch (error) { console.error("User denied account access or other error:", error); // エラー処理 return null; } } else { console.log('Please install MetaMask!'); alert('MetaMaskをインストールしてください。'); return null; }
}
// 例: ボタンクリックでウォレット接続を実行
// const connection = await connectWallet();
// if (connection) {
// // 接続成功後の処理
// }
window.ethereum
は、MetaMaskなどのEthereum互換ブラウザ拡張機能によって注入されるグローバルAPIです。これを通じて、ユーザーのウォレットと対話します。 ethers.BrowserProvider
はブラウザ環境でのプロバイダーを抽象化します。 provider.send("eth_requestAccounts", [])
は、ユーザーにウォレット接続の許可を求めます。 provider.getSigner()
は、トランザクション署名など、書き込み操作に必要な「サイナー」オブジェクトを取得します。
スマートコントラクトの読み込み
接続が確立したら、対話したいスマートコントラクトのインスタンスを作成します。これには、コントラクトのアドレスとABIが必要です。
import { ethers } from "ethers";
import YourContractABI from './YourContractABI.json'; // ABIファイルをインポート
// 定数として定義 (環境変数から読み込むのがより安全)
const contractAddress = "0xYourDeployedContractAddress"; // デプロイしたコントラクトのアドレスに置き換える
function getContractInstance(providerOrSigner) { // providerOrSigner には読み取り専用なら provider、書き込みも行うなら signer を渡す if (!contractAddress || !YourContractABI) { console.error("Contract address or ABI is missing."); return null; } try { const contract = new ethers.Contract(contractAddress, YourContractABI.abi, providerOrSigner); return contract; } catch (error) { console.error("Failed to create contract instance:", error); return null; }
}
// --- 使い方 ---
// 読み取り専用のインスタンスを取得する場合 (例: connectWallet() から provider を取得)
// const provider = new ethers.BrowserProvider(window.ethereum); // 接続済みの場合
// const readOnlyContract = getContractInstance(provider);
// 読み書き可能なインスタンスを取得する場合 (例: connectWallet() から signer を取得)
// const signer = await provider.getSigner(); // 接続済みの場合
// const readWriteContract = getContractInstance(signer);
ABIファイル(例: YourContractABI.json
)は、通常HardhatやTruffleなどの開発フレームワークでコンパイル時に artifacts
ディレクトリなどに生成されます。これをフロントエンドプロジェクトにコピーしてインポートします。 ethers.Contract
コンストラクタに、コントラクトアドレス、ABI、そしてプロバイダー(読み取り専用)またはサイナー(読み書き可能)を渡すことで、コントラクトオブジェクトが作成されます。
スマートコントラクトとの対話
コントラクトインスタンスがあれば、関数を呼び出したり、イベントを購読したりできます。
データの読み取り (Read Operations)
Solidityコントラクトの view
や pure
でマークされた関数(状態を変更しない読み取り専用関数)を呼び出すのは簡単です。ガス代はかかりません。
// readOnlyContract または readWriteContract インスタンスを使用
async function readMessage(contract) { if (!contract) return; try { // コントラクトの 'getMessage' 関数 (view関数と仮定) を呼び出す const message = await contract.getMessage(); console.log("Message from contract:", message); // 取得したデータをUIに表示するなどの処理 return message; } catch (error) { console.error("Error reading message:", error); }
}
// 呼び出し例
// const message = await readMessage(readOnlyContract);
データの書き込み (Write Operations)
ブロックチェーンの状態を変更する関数(例: 値を設定する、トークンを送るなど)を呼び出すには、トランザクションを送信する必要があります。これにはサイナー(接続されたウォレット)が必要で、ガス代が発生します。
// readWriteContract インスタンス (signerに接続されているもの) を使用
async function updateMessage(contract, newMessage) { if (!contract) return; try { // コントラクトの 'setMessage' 関数 (状態変更関数と仮定) を呼び出す console.log(`Updating message to: ${newMessage}...`); const tx = await contract.setMessage(newMessage); console.log("Transaction sent:", tx.hash); // トランザクションがブロックに含まれるのを待つ (任意だが推奨) // UIでローディング表示を開始 const receipt = await tx.wait(); console.log("Transaction confirmed:", receipt); // UIでローディング表示を終了し、成功メッセージを表示 alert("メッセージが更新されました!"); } catch (error) { console.error("Error updating message:", error); // UIでエラーメッセージを表示 alert(`エラーが発生しました: ${error.message || error}`); }
}
// 呼び出し例 (ユーザーが入力したメッセージで更新)
// const userInput = "新しいメッセージ";
// await updateMessage(readWriteContract, userInput);
書き込み操作は非同期で行われ、ネットワークの状況によって時間がかかることがあります。tx.wait()
を使うことで、トランザクションがブロックチェーンに取り込まれる(承認される)のを待つことができます。ユーザーには、トランザクション送信中であることや、完了したことを明確にフィードバックすることが重要です。
イベントの購読 (Listening to Events)
スマートコントラクトは、特定の出来事が発生したときにイベントを発行(emit)できます。フロントエンドでは、これらのイベントを購読して、リアルタイムで通知を受け取ることができます。
// readOnlyContract または readWriteContract インスタンスを使用
function listenToEvents(contract) { if (!contract) return; console.log("Listening for MessageUpdated events..."); // 'MessageUpdated' という名前のイベントをリッスン contract.on("MessageUpdated", (oldMessage, newMessage, event) => { console.log("Event received: MessageUpdated"); console.log(" - Old Message:", oldMessage); console.log(" - New Message:", newMessage); console.log(" - Event details:", event); // イベントの詳細情報 (ブロック番号、トランザクションハッシュなど) // イベント受信時の処理 (UIの更新など) alert(`メッセージが更新されました: ${newMessage}`); }); // 必要に応じてリスナーを解除する処理も実装する // (例: コンポーネントのアンマウント時) // contract.off("MessageUpdated", listenerCallback); // または // contract.removeAllListeners("MessageUpdated");
}
// 呼び出し例
// listenToEvents(readOnlyContract);
contract.on("EventName", callback)
を使うことで、特定のイベントが発生した際にコールバック関数が実行されるようになります。これは、他のユーザーが行った操作の結果をリアルタイムでUIに反映させたい場合などに非常に便利です。
Reactでの実装例
Reactコンポーネント内で上記のロジックを組み合わせる例です。
import React, { useState, useEffect, useCallback } from 'react';
import { ethers } from 'ethers';
import YourContractABI from './YourContractABI.json'; // ABIファイルをインポート
const contractAddress = "0xYourDeployedContractAddress"; // ここにコントラクトアドレス
function DApp() { const [provider, setProvider] = useState(null); const [signer, setSigner] = useState(null); const [contract, setContract] = useState(null); const [account, setAccount] = useState(null); const [message, setMessage] = useState(''); const [newMessage, setNewMessage] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const connectWallet = useCallback(async () => { setError(''); if (typeof window.ethereum !== 'undefined') { try { const web3Provider = new ethers.BrowserProvider(window.ethereum); const accounts = await web3Provider.send("eth_requestAccounts", []); const web3Signer = await web3Provider.getSigner(); const currentAccount = accounts[0]; setProvider(web3Provider); setSigner(web3Signer); setAccount(currentAccount); const contractInstance = new ethers.Contract(contractAddress, YourContractABI.abi, web3Signer); setContract(contractInstance); } catch (err) { console.error(err); setError('ウォレット接続に失敗しました。'); } } else { setError('MetaMaskをインストールしてください。'); } }, []); const fetchMessage = useCallback(async () => { if (contract) { try { const currentMessage = await contract.getMessage(); setMessage(currentMessage); } catch (err) { console.error(err); setError('メッセージの読み取りに失敗しました。'); } } }, [contract]); const handleUpdateMessage = async (e) => { e.preventDefault(); if (contract && newMessage) { setLoading(true); setError(''); try { const tx = await contract.setMessage(newMessage); await tx.wait(); setNewMessage(''); // 入力フィールドをクリア await fetchMessage(); // メッセージを再読み込み alert('メッセージを更新しました!'); } catch (err) { console.error(err); setError(`メッセージの更新に失敗しました: ${err.message || err}`); } finally { setLoading(false); } } }; // 初期レンダリング時にメッセージを取得 useEffect(() => { if (contract) { fetchMessage(); // イベントリスナーを設定 const listener = (oldMsg, newMsg, event) => { console.log('MessageUpdated event detected:', newMsg); setMessage(newMsg); // UIを更新 }; contract.on("MessageUpdated", listener); // クリーンアップ関数でリスナーを解除 return () => { contract.off("MessageUpdated", listener); }; } }, [contract, fetchMessage]); // アカウント変更時の処理 useEffect(() => { if (window.ethereum) { const handleAccountsChanged = (accounts) => { if (accounts.length === 0) { // MetaMask is locked or the user has not connected any accounts console.log('Please connect to MetaMask.'); setAccount(null); setSigner(null); setContract(null); } else if (accounts[0] !== account) { setAccount(accounts[0]); // 必要に応じてSignerとContractを再初期化 if (provider) { provider.getSigner().then(newSigner => { setSigner(newSigner); const newContract = new ethers.Contract(contractAddress, YourContractABI.abi, newSigner); setContract(newContract); }); } } }; window.ethereum.on('accountsChanged', handleAccountsChanged); // クリーンアップ return () => window.ethereum.removeListener('accountsChanged', handleAccountsChanged); } }, [account, provider]); // 依存配列にaccountとproviderを追加 return ( My DApp
{error && {error}} {account ? ( 接続中のアカウント: {account}
現在のメッセージ
{message || '読み込み中...'}
メッセージを更新
) : ( )} );
}
export default DApp;
この例では、useState
で状態を管理し、useEffect
で初期データの読み込みやイベントリスナーの設定、アカウント変更の監視を行っています。useCallback
は関数の再生成を防ぎ、パフォーマンスを最適化します。
Vueでの実装例
Vue 3 の Composition API を使用した例です。
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { ethers } from 'ethers';
import YourContractABI from './YourContractABI.json'; // ABIファイルをインポート
const contractAddress = "0xYourDeployedContractAddress"; // ここにコントラクトアドレス
const provider = ref(null);
const signer = ref(null);
const contract = ref(null);
const account = ref(null);
const message = ref('');
const newMessage = ref('');
const loading = ref(false);
const error = ref('');
const isConnected = computed(() => !!account.value);
const connectWallet = async () => { error.value = ''; if (typeof window.ethereum !== 'undefined') { try { const web3Provider = new ethers.BrowserProvider(window.ethereum); const accounts = await web3Provider.send("eth_requestAccounts", []); const web3Signer = await web3Provider.getSigner(); const currentAccount = accounts[0]; provider.value = web3Provider; signer.value = web3Signer; account.value = currentAccount; const contractInstance = new ethers.Contract(contractAddress, YourContractABI.abi, web3Signer); contract.value = contractInstance; await fetchMessage(); // 接続後にメッセージを取得 listenForEvents(); // イベントリスナーを設定 } catch (err) { console.error(err); error.value = 'ウォレット接続に失敗しました。'; } } else { error.value = 'MetaMaskをインストールしてください。'; }
};
const fetchMessage = async () => { if (contract.value) { try { const currentMessage = await contract.value.getMessage(); message.value = currentMessage; } catch (err) { console.error(err); error.value = 'メッセージの読み取りに失敗しました。'; } }
};
const handleUpdateMessage = async () => { if (contract.value && newMessage.value) { loading.value = true; error.value = ''; try { const tx = await contract.value.setMessage(newMessage.value); await tx.wait(); newMessage.value = ''; // 入力フィールドをクリア await fetchMessage(); // メッセージを再読み込み alert('メッセージを更新しました!'); } catch (err) { console.error(err); error.value = `メッセージの更新に失敗しました: ${err.message || err}`; } finally { loading.value = false; } }
};
const handleAccountsChanged = async (accounts) => { if (accounts.length === 0) { console.log('Please connect to MetaMask.'); account.value = null; signer.value = null; contract.value = null; } else if (accounts[0] !== account.value) { account.value = accounts[0]; if (provider.value) { try { const newSigner = await provider.value.getSigner(); signer.value = newSigner; contract.value = new ethers.Contract(contractAddress, YourContractABI.abi, newSigner); await fetchMessage(); listenForEvents(); // 再度リスナー設定が必要な場合 } catch (err) { console.error("Failed to re-initialize signer/contract:", err); // エラー処理 } } }
};
const handleChainChanged = () => { window.location.reload();
};
const eventListener = (oldMsg, newMsg, event) => { console.log('MessageUpdated event detected:', newMsg); message.value = newMsg; // UIを更新
};
const listenForEvents = () => { if (contract.value) { // 既存のリスナーがあれば解除 contract.value.removeAllListeners("MessageUpdated"); // 新しいリスナーを設定 contract.value.on("MessageUpdated", eventListener); }
};
onMounted(() => { if (window.ethereum) { window.ethereum.on('accountsChanged', handleAccountsChanged); window.ethereum.on('chainChanged', handleChainChanged); // ページ読み込み時に自動接続を試みる場合 // connectWallet(); }
});
onUnmounted(() => { if (window.ethereum) { window.ethereum.removeListener('accountsChanged', handleAccountsChanged); window.ethereum.removeListener('chainChanged', handleChainChanged); } if (contract.value) { contract.value.removeAllListeners("MessageUpdated"); }
});
</script>
<template> <div class="container p-4"> <h1 class="title is-3">My DApp (Vue)</h1> <div v-if="error" class="notification is-danger">{{ error }}</div> <div v-if="isConnected"> <p class="mb-3">接続中のアカウント: <strong class="has-text-info">{{ account }}</strong></p> <div class="box"> <h2 class="title is-4">現在のメッセージ</h2> <p class="is-size-5">{{ message || '読み込み中...' }}</p> </div> <div class="box"> <h2 class="title is-4">メッセージを更新</h2> <form @submit.prevent="handleUpdateMessage"> <div class="field"> <label class="label" for="newMessageVue">新しいメッセージ:</label> <div class="control"> <input id="newMessageVue" class="input" type="text" v-model="newMessage" :disabled="loading" required /> </div> </div> <div class="control"> <button type="submit" class="button is-primary" :class="{ 'is-loading': loading }" :disabled="loading || !newMessage" > 更新する </button> </div> </form> </div> </div> <div v-else> <button class="button is-link" @click="connectWallet"> ウォレットを接続 </button> </div> </div>
</template>
Vue 3 の Composition API では、ref
でリアクティブな状態を定義し、computed
で算出プロパティを作成します。onMounted
や onUnmounted
ライフサイクルフック内でイベントリスナーの登録・解除を行います。
重要な考慮事項
- エラーハンドリング: ユーザーがトランザクションを拒否した場合、ネットワークエラーが発生した場合など、様々なエラーケースを考慮し、ユーザーに分かりやすくフィードバックする必要があります。
- ユーザーエクスペリエンス (UX): トランザクションの処理には時間がかかるため、ローディング状態を明確に示し、処理が完了したら通知するなど、ユーザーが状況を理解できるように工夫が必要です。
- セキュリティ: フロントエンドのコードでは秘密鍵を扱わないようにしてください。トランザクションの署名は常にMetaMaskなどのユーザーのウォレットに委譲します。コントラクトアドレスやABIは公開情報ですが、機密情報(APIキーなど)はコードに直接埋め込まず、環境変数などを使って管理しましょう。
- ネットワークの切り替え: ユーザーがMetaMaskで異なるネットワーク(例: MainnetからSepoliaテストネットへ)に切り替えた場合に、DAppが適切に対応できるようにする必要があります。通常はページをリロードして再接続を促すのが一般的です。
まとめ
このステップでは、ReactまたはVueフレームワークと ethers.js ライブラリを使って、フロントエンドアプリケーションをEthereumスマートコントラクトに接続する方法を学びました。
- Web3ライブラリ (ethers.js) を導入する。
- MetaMaskなどのウォレットプロバイダーに接続し、ユーザーアカウント情報を取得する。
- コントラクトアドレスとABIを使って、スマートコントラクトのインスタンスを作成する。
- コントラクトインスタンスを通じて、読み取り関数や書き込み関数を呼び出す。
- コントラクトから発行されるイベントを購読し、リアルタイムな更新に対応する。
これで、ユーザーがWebインターフェースを通じてスマートコントラクトと対話できる、本格的なDAppの基礎が完成しました。次のステップでは、より具体的なDAppの例として、トークンやNFTを扱うコントラクトの作成に挑戦してみましょう!
参考情報
- ethers.js Documentation: https://docs.ethers.org/v6/
- web3.js Documentation: https://web3js.readthedocs.io/
- React Documentation: https://react.dev/
- Vue.js Documentation: https://vuejs.org/
- MetaMask Documentation: https://docs.metamask.io/guide/