スマートコントラクトをDappsに接続する
この記事では、スマートコントラクトをDappsに接続して実行するまでの方法を解説します。
前回の記事の続きとなります。
Alchemyのチュートリアルに沿って、進めていきます。
1. プロジェクトの準備
Alchemyのチュートリアルに用意されているコードを使うため、
Gitリポジトリをクローンします。
https://github.com/alchemyplatform/hello-world-part-four-tutorial
$ git clone https://github.com/alchemyplatform/hello-world-part-four-tutorial.git
フォルダー移動して、VSCodeを開きます。
$ cd hello-world-part-four-tutorial
$ code.
プロジェクトルートには、2種類のフォルダーがあります。
作業するのはstarter-filesで、completedは行き詰まった場合の参照として利用します。
completed - 完成版が保存されている
starter-files - 作業用のフォルダー
フロントエンドはReactで作られています。
Reactで記述するコードは、srcフォルダーに保存していきます。
最初にReactを起動していきます。
フォルダーを移動します。
$ cd starter-files
Gitにあるプロジェクトの依存関係が非推奨になっているためpackage.jsonを更新して依存関係を修正する必要があります。
package.jsonを以下に更新します。
{
"name": "hello-world-part-four",
"version": "0.1.0",
"private": true,
"dependencies": {
"@alch/alchemy-web3": "^1.4.7",
"@testing-library/jest-dom": "6.1.0",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"alchemy-sdk": "^2.10.0",
"browserify-zlib": "^0.2.0",
"crypto-browserify": "^3.12.0",
"dotenv": "16.3.1",
"http-browserify": "^1.7.0",
"https-browserify": "^1.0.0",
"node-polyfill-webpack-plugin": "^2.0.1",
"os-browserify": "^0.3.0",
"path": "^0.12.7",
"path-browserify": "^1.0.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-scripts": "^5.0.1",
"stream": "^0.0.2",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"web-vitals": "3.4.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": [
">0.2%",
"not dead",
"not op_mini all"
]
}
プロジェクトの依存関係をインストールします。
$ npm install
webpack.config.jsファイルの設定を変更し、新しくインストールされたいくつかの依存関係を追加する必要があります。
node_modules ディレクトリ内にwebpack.config.jsを開きます。
$ code node_modules/react-scripts/config/webpack.config.js
resolveオブジェクトのfallbackプロパティに新しいモジュールを追加します。
追加する箇所は、320行目です。
fallback: { "http": require.resolve("stream-http"),
"https": require.resolve("https-browserify"), "zlib": require.resolve("browserify-zlib") },
追加後の画像になります。
モジュールを追加したらフロントエンドを起動します。
$ npm start
http://localhost:3000/にアクセスするとフロントエンドが表示されました。
「Connect Wallet」や「Update」などの機能はこれから実装していくため、まだ機能しません。
2. テンプレートコードの解説
HelloWorld.jsを開きます。
$ code src/HelloWorld.js
コード上に解説のコメントを記述しています。
// HelloWorld.js
// プロジェクトに必要な機能をインポート
import React from "react";
import { useEffect, useState } from "react";
import {
helloWorldContract,
connectWallet,
updateMessage,
loadCurrentMessage,
getCurrentWalletConnected,
} from "./util/interact.js";
import alchemylogo from "./alchemylogo.svg";
const HelloWorld = () => {
// ①状態変数を定義
const [walletAddress, setWallet] = useState("");
const [status, setStatus] = useState("");
const [message, setMessage] = useState("No connection to the network.");
const [newMessage, setNewMessage] = useState("");
//②画面を描画するとき1度だけ実行
useEffect(async () => {
}, []);
//③関数を定義
function addSmartContractListener() { //TODO: 後から機能を実装
}
function addWalletListener() { //TODO: 後から機能を実装
}
const connectWalletPressed = async () => { //TODO: 後から機能を実装
};
const onUpdatePressed = async () => { //TODO: 後から機能を実装
};
// ④コンポーネントのUI
return (
<div id="container">
<img id="logo" src={alchemylogo}></img>
<button id="walletButton" onClick={connectWalletPressed}>
{walletAddress.length > 0 ? (
"Connected: " +
String(walletAddress).substring(0, 6) +
"..." +
String(walletAddress).substring(38)
) : (
<span>Connect Wallet</span>
)}
</button>
<h2 style={{ paddingTop: "50px" }}>Current Message:</h2>
<p>{message}</p>
<h2 style={{ paddingTop: "18px" }}>New Message:</h2>
<div>
<input
type="text"
placeholder="Update the message in your smart contract."
onChange={(e) => setNewMessage(e.target.value)}
value={newMessage}
/>
<p id="status">{status}</p>
<button id="publish" onClick={onUpdatePressed}>
Update
</button>
</div>
</div>
);
};
export default HelloWorld;
①の状態変数はそれぞれ以下の役割です。
walletAddress - ユーザーのウォレットアドレスを保存する
status - アプリの状態をユーザーに案内するためのメッセージを保存する
message - スマートコントラクトの現在のメッセージを保存する
newMessage - スマートコントラクトに書き込まれる新しいメッセージを格納する
// ①状態変数を定義
const [walletAddress, setWallet] = useState("");
const [status, setStatus] = useState("");
const [message, setMessage] = useState("No connection to the network.");
const [newMessage, setNewMessage] = useState("");
②はReactの基本機能で、画面の読み込みや状態変数を更新すると再描画します。
これによりユーザーのアクションで更新された変数や処理結果を更新するたびに画面の表示が変わります。
//②画面を描画するとき1度だけ実行
useEffect(async () => {
}, []);
③の関数はそれぞれ以下の役割です。
daddSmartContractListener - コントラクトのUpdatedMessagesを監視し、メッセージが変更されたときにUIを更新します。
addWalletListener - ユーザーがウォレットを切断したり、アドレスを切り替えると、ユーザーのメタマスクの変化を検出します。
connectWalletPressed - ユーザーのメタマスクをアプリに接続します。
onUpdatePressed - ユーザーがスマートコントラクトに保存されているメッセージを更新するときに呼び出します。
//③関数を定義
function addSmartContractListener() { //TODO: 後から機能を実装
}
function addWalletListener() { //TODO: 後から機能を実装
}
const connectWalletPressed = async () => { //TODO: 後から機能を実装
};
const onUpdatePressed = async () => { //TODO: 後から機能を実装
};
④フロントエンドを表示するためのUIです。
状態変数や関数の実行で、状態が変わるとUIも連動して表示が変わります。
// ④コンポーネントのUI
return (
<div id="container">
<img id="logo" src={alchemylogo}></img>
<button id="walletButton" onClick={connectWalletPressed}>
{walletAddress.length > 0 ? (
"Connected: " +
String(walletAddress).substring(0, 6) +
"..." +
String(walletAddress).substring(38)
) : (
<span>Connect Wallet</span>
)}
</button>
<h2 style={{ paddingTop: "50px" }}>Current Message:</h2>
<p>{message}</p>
<h2 style={{ paddingTop: "18px" }}>New Message:</h2>
<div>
<input
type="text"
placeholder="Update the message in your smart contract."
onChange={(e) => setNewMessage(e.target.value)}
value={newMessage}
/>
<p id="status">{status}</p>
<button id="publish" onClick={onUpdatePressed}>
Update
</button>
</div>
</div>
);
};
次にinteract.jsファイルを開きます。
今回のReactプロジェクトは、MVCパターンで作られています。
先ほどのHelloWorld.jsは、ModelとViewになります。
interact.jsは、Controllerのロジックを管理するためのファイルです。
//export const helloWorldContract;
export const loadCurrentMessage = async () => {
};
export const connectWallet = async () => {
};
const getCurrentWalletConnected = async () => {
};
export const updateMessage = async (message) => {
};
それぞれの関数の役割です。
loadCurrentMessage - スマートコントラクトに保存されている現在のメッセージを読み込む
connectWallet - ユーザーのMetamaskをアプリに接続する
getCurrentWalletConnected - ページの読み込み時にメタマスクが接続されているかどうかを確認し、それに応じて UI を更新する
newMessage - スマート コントラクトに保存されているメッセージを更新する
3. スマートコントラクトの接続
フロントエンドから、スマートコントラクトを実行するために次の手順を行う必要があります。
EthereumチェーンにAPIを接続する
コントラクトのインスタンスを作成する
コントラクトの関数を呼び出す
データが変更されたときに更新を監視する
アプリにAPIを接続するために、AlchemyのAPIキーが必要になります。
必要なライブラリをnpmでインストールします。
$ npm install @alch/alchemy-web3
次にWebsockets APIキーを取得します。
取得したAPIキーをinteract.jsのalchemyKeyに貼り付けます。
またalchemyのインスタンスを作成するコードを追加します。
// interact.js
const alchemyKey = "wss://eth-sepolia.g.alchemy.com/v2/<YOUR-API-KEY>"
const { createAlchemyWeb3 } = require("@alch/alchemy-web3");
const web3 = createAlchemyWeb3(alchemyKey);
//export const helloWorldContract;
Hello Worldスマートコントラクトを読み込むために、コントラクトアドレスとABIが必要です。
前回の記事で、公開したコントラクトアドレスをEtherscanから確認します。
https://sepolia.etherscan.io/address/0x024CEc0E8aD1E71cC69c0a095B00377174DeD23F
Contractを選択します。
Contract ABIをコピーします。
Contract ABI は、コントラクトが呼び出す関数や変数などのコントラクトを実行するための情報が含まれています。
フロントエンドは、配置したAPIを使ってコントラクトを実行します。
Contract ABI をコピーしたら、 srcのcontract-abi.jsonという JSON ファイルに貼り付けて保存します。
APIファイルの読み込みとコントラクトアドレスを定義するためのコードを追加します。
// interact.js
const alchemyKey = "wss://eth-sepolia.g.alchemy.com/v2/<YOUR-API-KEY>"
const { createAlchemyWeb3 } = require("@alch/alchemy-web3");
const web3 = createAlchemyWeb3(alchemyKey);
const contractABI = require('../contract-abi.json')
const contractAddress = <Your-Contract-Address>;
コメントアウトされているhelloWorldContractにコントラクトを読み込む処理を追加します。
export const helloWorldContract = new web3.eth.Contract(
contractABI,
contractAddress
);
ここまでのコードです。
const alchemyKey = "wss://eth-sepolia.g.alchemy.com/v2/<YOUR-API-KEY>"
const { createAlchemyWeb3 } = require("@alch/alchemy-web3");
const web3 = createAlchemyWeb3(alchemyKey);
const contractABI = require('../contract-abi.json')
const contractAddress = <Your-Contract-Address>;
export const helloWorldContract = new web3.eth.Contract(
contractABI,
contractAddress
);
4. コントラクトのメッセージを読み込む
次にinteract.jsにloadCurrentMessageの処理を追加していきます。
この関数は、コントラクトのmessage関数を実行し取得した文字列を変数に格納して返却します。
export const loadCurrentMessage = async () => {
const message = await helloWorldContract.methods.message().call();
return message;
};
取得した文字列をフロントエンドに表示する処理を追加します。
HelloWorld.jsのuseEffectを次のように更新します。
// HelloWorld.js
//画面を描画するとき1度だけ実行
useEffect(() => {
async function fetchMessage() {
const message = await loadCurrentMessage();
setMessage(message);
}
fetchMessage();
}, []);
useEffectに追加することで、画面を表示するときloadCurrentMessage関数を実行しコントラクトに保存された値を取得して表示します。
フロントエンドを起動してみます。
$ npm run start
コントラクトに保存されているメッセージを取得しUIに反映されました。
次はaddSmartContractListener関数を実装していきます。
この関数は、コントラクトの文字が変更されたとき画面のUIを更新するための処理になります。
HelloWorld.jsのaddSmartContractListenerを以下のコードに更新します。
function addSmartContractListener() {
helloWorldContract.events.UpdatedMessages({}, (error, data) => {
if (error) {
setStatus("😥 " + error.message);
} else {
setMessage(data.returnValues[1]);
setNewMessage("");
setStatus("🎉 Your message has been updated!");
}
});
}
useEffectにaddSmartContractListener()を追加します。
これによりフロントは、スマートコントラクトの文字列が更新されると検知してUIを更新してくれます。
useEffect(() => {
async function fetchMessage() {
const message = await loadCurrentMessage();
setMessage(message);
}
fetchMessage();
addSmartContractListener();
}, []);
5. フロントからメタマスクを接続する
コントラクトの関数を実行するには、メタマスクを経由して実行する必要があります。
そのためフロントからメタマスクを接続する処理を追加していきます。
connectWalletを次のように変更します。
コメントにコードの解説を加えています。
export const connectWallet = async () => {
// メタマスクがあることを確認し、存在したら処理を実行
if (window.ethereum) {
try {
// ethereumが用意したAPIを実行
// eth_requestAccountsでメタマスクを接続するためのリクエストを追加
const addressArray = await window.ethereum.request({
method: "eth_requestAccounts",
});
// 成功したらステータスと取得したアドレスの1番目を返却
const obj = {
status: "👆🏽 Write a message in the text-field above.",
address: addressArray[0],
};
return obj;
} catch (err) {
// 失敗したらエラーを返却
return {
address: "",
status: "😥 " + err.message,
};
}
} else {
// メタマスクがなければ、ダウンロードを案内するUIを返却
return {
address: "",
status: (
<span>
<p>
{" "}
🦊{" "}
<a target="_blank" href={`https://metamask.io/download`}>
You must install Metamask, a virtual Ethereum wallet, in your
browser.
</a>
</p>
</span>
),
};
}
};
UIコンポーネントにconnectWallet関数を追加します。
const connectWalletPressed = async () => {
const walletResponse = await connectWallet();
setStatus(walletResponse.status);
setWallet(walletResponse.address);
};
フロントエンドから確認してみます。
メタマスクのネットワークをSepoliaに変更します。
フロントエンドのConnect Walletを押します。
メタマスクのアクセス許可を求められて、許可するとメタマスクとフロントエンドを接続できます。
キャンセルすると接続できませんでしたのメッセージを表示します。
接続した状態で、リロードするとボタンが「Connect Wallet」に戻ります。
しかしボタンを再度押すと接続状態に変わります。
これは画面の読み込み時に、メタマスクの接続をフロントが検知できていないためです。
画面の初期読み込みやリロードしたら、メタマスクとの接続を確認して画面に反映する処理を追加します。
interact.jsの getCurrentWalletConnected関数を次のように更新します。
export const getCurrentWalletConnected = async () => {
if (window.ethereum) {
try {
// eth_accountsは、アプリ接続されているメタマスクのアドレスをリストで返します。
const addressArray = await window.ethereum.request({
method: "eth_accounts",
});
// リストにアカウントが含まれれば、アドレスとステータスを返します。
if (addressArray.length > 0) {
return {
address: addressArray[0],
status: "👆🏽 Write a message in the text-field above.",
};
} else {
return {
address: "",
status: "🦊 Connect to Metamask using the top right button.",
};
}
} catch (err) {
return {
address: "",
status: "😥 " + err.message,
};
}
} else {
return {
address: "",
status: (
<span>
<p>
{" "}
🦊{" "}
<a target="_blank" href={`https://metamask.io/download`}>
You must install Metamask, a virtual Ethereum wallet, in your
browser.
</a>
</p>
</span>
),
};
}
};
この関数を実行するには、 HelloWorld.jsコンポーネントのuseEffect関数で呼び出します。
画面の読み込み時に、メタマスクの接続状態を確認して画面を更新します。
useEffect(() => {
async function fetchMessage() {
const message = await loadCurrentMessage();
setMessage(message);
}
fetchMessage();
addSmartContractListener();
async function fetchWallet() {
const {address, status} = await getCurrentWalletConnected();
setWallet(address);
setStatus(status);
}
fetchWallet();
}, []);
メタマスクを接続してブラウザを更新すると、今度はConnectedになることを確認できました。
メタマスクを接続する最後のステップとして、ユーザーがメタマスクを切断したり、アカウントを切り替えるとUIに反映する処理を追加します。
addWalletListener関数を更新します。
// HelloWorld.js
function addWalletListener() {
if (window.ethereum) {
window.ethereum.on("accountsChanged", (accounts) => {
if (accounts.length > 0) {
setWallet(accounts[0]);
setStatus("👆🏽 Write a message in the text-field above.");
} else {
setWallet("");
setStatus("🦊 Connect to Metamask using the top right button.");
}
});
} else {
setStatus(
<p>
{" "}
🦊{" "}
<a target="_blank" href={`https://metamask.io/download`}>
You must install Metamask, a virtual Ethereum wallet, in your
browser.
</a>
</p>
);
}
}
useEffectにaddWalletListener関数を追加して、UIに検知できるようにします。
useEffect(() => {
async function fetchMessage() {
const message = await loadCurrentMessage();
setMessage(message);
}
fetchMessage();
addSmartContractListener();
async function fetchWallet() {
const {address, status} = await getCurrentWalletConnected();
setWallet(address)
setStatus(status);
}
fetchWallet();
addWalletListener();
}, []);
アカウントを切り替えると、フロントに反映されました。
6. コントラクトの書き込み処理を追加
最後にコントラクトに書き込むための処理を追加します。
メタマスクが存在しない、接続していないときにメタマスクの案内メッセージを返します。
また入力した文字が空だったら、バリデーションエラーを返します。
バリデーションが通ったら、コントラクトの書き込み関数を実行するためのトランザクションに書き込み処理を入れます。
// interact.js
export const updateMessage = async (address, message) => {
if (!window.ethereum || address === null) {
return {
status:
"💡 Connect your Metamask wallet to update the message on the blockchain.",
};
}
if (message.trim() === "") {
return {
status: "❌ Your message cannot be an empty string.",
};
}
//トランザクションのパラメータを定義
const transactionParameters = {
to: contractAddress, //コントラクトアドレス
from: address, 接続したウオレットアドレス.
data: helloWorldContract.methods.update(message).encodeABI(),
};
//トランザクションにサインイン
try {
const txHash = await window.ethereum.request({
method: "eth_sendTransaction",
params: [transactionParameters],
});
return {
status: (
<span>
✅{" "}
<a target="_blank" href={`https://sepolia.etherscan.io/tx/${txHash}`}>
View the status of your transaction on Etherscan!
</a>
<br />
ℹ️ Once the transaction is verified by the network, the message will
be updated automatically.
</span>
),
};
} catch (error) {
return {
status: "😥 " + error.message,
};
}
};
バリデーションチェックの下に実行するコードを追加します。
フロントエンドに追加します。
const onUpdatePressed = async () => {
const { status } = await updateMessage(walletAddress, newMessage);
setStatus(status);
};
これで完成です。
未入力でupdateボタンを押すと、エラーメッセージをフロントに表示します。
新しいメッセージを入力して、アップデートします。
メタマスクからトランザクションを実行するための許可画面が表示され、許可するとメッセージが更新されました。
Etherscanを確認するとログを確認すると、トランザクションが確認できました!