はじめに
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/
コメント