[Solidityのはじめ方] Part24: フロントエンド(React/Vue)とスマコンの接続

はじめに

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.jsweb3.js です。

現在、ethers.js がよりモダンで、軽量、TypeScriptとの親和性も高いなどの理由から人気を集めています。このチュートリアルでは主に ethers.js を使用して解説します。

ethers.js と web3.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コントラクトの viewpure でマークされた関数(状態を変更しない読み取り専用関数)を呼び出すのは簡単です。ガス代はかかりません。

// 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 || '読み込み中...'}

メッセージを更新

setNewMessage(e.target.value)} disabled={loading} required />
) : ( )}
); } 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 で算出プロパティを作成します。onMountedonUnmounted ライフサイクルフック内でイベントリスナーの登録・解除を行います。

重要な考慮事項

  • エラーハンドリング: ユーザーがトランザクションを拒否した場合、ネットワークエラーが発生した場合など、様々なエラーケースを考慮し、ユーザーに分かりやすくフィードバックする必要があります。
  • ユーザーエクスペリエンス (UX): トランザクションの処理には時間がかかるため、ローディング状態を明確に示し、処理が完了したら通知するなど、ユーザーが状況を理解できるように工夫が必要です。
  • セキュリティ: フロントエンドのコードでは秘密鍵を扱わないようにしてください。トランザクションの署名は常にMetaMaskなどのユーザーのウォレットに委譲します。コントラクトアドレスやABIは公開情報ですが、機密情報(APIキーなど)はコードに直接埋め込まず、環境変数などを使って管理しましょう。
  • ネットワークの切り替え: ユーザーがMetaMaskで異なるネットワーク(例: MainnetからSepoliaテストネットへ)に切り替えた場合に、DAppが適切に対応できるようにする必要があります。通常はページをリロードして再接続を促すのが一般的です。

まとめ

このステップでは、ReactまたはVueフレームワークと ethers.js ライブラリを使って、フロントエンドアプリケーションをEthereumスマートコントラクトに接続する方法を学びました。

  1. Web3ライブラリ (ethers.js) を導入する。
  2. MetaMaskなどのウォレットプロバイダーに接続し、ユーザーアカウント情報を取得する。
  3. コントラクトアドレスとABIを使って、スマートコントラクトのインスタンスを作成する。
  4. コントラクトインスタンスを通じて、読み取り関数や書き込み関数を呼び出す。
  5. コントラクトから発行されるイベントを購読し、リアルタイムな更新に対応する。

これで、ユーザーがWebインターフェースを通じてスマートコントラクトと対話できる、本格的なDAppの基礎が完成しました。次のステップでは、より具体的なDAppの例として、トークンやNFTを扱うコントラクトの作成に挑戦してみましょう!

参考情報

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です