メインコンテンツまでスキップ

lesson-3_WEBアプリからコントラクトを呼び出そう

📒 Web アプリケーションからスマートコントラクトを呼び出す

このレッスンでは、MetaMaskの認証機能を使用して、Webアプリケーションから実際にあなたのコントラクトを呼び出す機能を実装します。

WavePortal.solに実装したgetTotalWaves関数を覚えていますか?

  function getTotalWaves() public view returns (uint256) {
console.log("We have %d total waves!", totalWaves);
return totalWaves;
}

App.jsを以下のように更新して、フロントエンドからgetTotalWaves関数へアクセスできるようにします。

/* ethers 変数を使えるようにする*/
import { ethers } from "ethers";
import React, { useEffect, useState } from "react";

import "./App.css";

const App = () => {
// ユーザーのパブリックウォレットを保存するために使用する状態変数を定義します。
const [currentAccount, setCurrentAccount] = useState("");
console.log("currentAccount: ", currentAccount);

// window.ethereumにアクセスできることを確認します。
const checkIfWalletIsConnected = async () => {
try {
const { ethereum } = window;
if (!ethereum) {
console.log("Make sure you have MetaMask!");
return;
} else {
console.log("We have the ethereum object", ethereum);
}
// ユーザーのウォレットへのアクセスが許可されているかどうかを確認します。
const accounts = await ethereum.request({ method: "eth_accounts" });
if (accounts.length !== 0) {
const account = accounts[0];
console.log("Found an authorized account:", account);
setCurrentAccount(account);
} else {
console.log("No authorized account found");
}
} catch (error) {
console.log(error);
}
};

// connectWalletメソッドを実装
const connectWallet = async () => {
try {
const { ethereum } = window;
if (!ethereum) {
alert("Get MetaMask!");
return;
}
const accounts = await ethereum.request({
method: "eth_requestAccounts",
});
console.log("Connected: ", accounts[0]);
setCurrentAccount(accounts[0]);
} catch (error) {
console.log(error);
}
};

// waveの回数をカウントする関数を実装
const wave = async () => {
try {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const wavePortalContract = new ethers.Contract(
contractAddress,
contractABI,
signer
);
let count = await wavePortalContract.getTotalWaves();
console.log("Retrieved total wave count...", count.toNumber());
console.log("Signer:", signer);
} else {
console.log("Ethereum object doesn't exist!");
}
} catch (error) {
console.log(error);
}
};

// WEBページがロードされたときに下記の関数を実行します。
useEffect(() => {
checkIfWalletIsConnected();
}, []);
return (
<div className="mainContainer">
<div className="dataContainer">
<div className="header">
<span role="img" aria-label="hand-wave">
👋
</span>{" "}
WELCOME!
</div>
<div className="bio">
イーサリアムウォレットを接続して、「
<span role="img" aria-label="hand-wave">
👋
</span>
(wave)」を送ってください
<span role="img" aria-label="shine">

</span>
</div>
{/* waveボタンにwave関数を連動させる。*/}
<button className="waveButton" onClick={wave}>
Wave at Me
</button>
{/* ウォレットコネクトのボタンを実装 */}
{!currentAccount && (
<button className="waveButton" onClick={connectWallet}>
Connect Wallet
</button>
)}
{currentAccount && (
<button className="waveButton" onClick={connectWallet}>
Wallet Connected
</button>
)}
</div>
</div>
);
};

export default App;

ここで実装した新しい機能は下記の3つです。

1 . ethers 変数を使えるようにする

import { ethers } from "ethers";

ethersのさまざまなクラスや関数は、ethersproject が提供するサブパッケージからインポートできます。これは、Webアプリケーションからコントラクトを呼び出す際に必須となるので、覚えておきましょう。

2 . wave の回数をカウントする関数を実装する

const wave = async () => {
try {
// ユーザーがMetaMaskを持っているか確認
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const wavePortalContract = new ethers.Contract(
contractAddress,
contractABI,
signer
);
let count = await wavePortalContract.getTotalWaves();
console.log("Retrieved total wave count...", count.toNumber());
console.log("Signer:", signer);
} else {
console.log("Ethereum object doesn't exist!");
}
} catch (error) {
console.log(error);
}
};

追加されたコードを見ながら、新しい概念について学びましょう。

I. provider

const provider = new ethers.providers.Web3Provider(ethereum);

ここでは、provider (= MetaMask) を設定しています。 providerを介して、ユーザーはブロックチェーン上に存在するイーサリアムノードに接続することができます。 MetaMask が提供するイーサリアムノードを使用して、デプロイされたコントラクトからデータを送受信するために上記の実装を行いました。

ethersのライブラリによりproviderのインスタンスを新規作成しています。

II. signer

const signer = provider.getSigner();

signerは、ユーザーのウォレットアドレスを抽象化したものです。

providerを作成し、provider.getSigner()を呼び出すだけで、ユーザーはウォレットアドレスを使用してトランザクションに署名し、そのデータをイーサリアムネットワークに送信することができます。

provider.getSigner()は新しいsignerインスタンスを返すので、それを使って署名付きトランザクションを送信することができます。

III. コントラクトインスタンス

const wavePortalContract = new ethers.Contract(
contractAddress,
contractABI,
signer
);

ここで、コントラクトへの接続を行っています。

コントラクトの新しいインスタンスを作成するには、以下 3 つの変数をethers.Contract関数に渡す必要があります。

  1. コントラクトのデプロイ先のアドレス(ローカル、テストネット、またはイーサリアムメインネット)
  2. コントラクトの ABI
  3. provider、もしくはsigner

コントラクトインスタンスでは、コントラクトに格納されているすべての関数を呼び出すことができます。

もしこのコントラクトインスタンスにproviderを渡すと、そのインスタンスは読み取り専用の機能しか実行できなくなります

一方、signerを渡すと、そのインスタンスは読み取りと書き込みの両方の機能を実行できるようになります

※ ABI についてはこのレッスンの終盤にて詳しく説明します。

3 . wave ボタンに wave 関数を連動させる

<button className="waveButton" onClick="{wave}">Wave at Me</button>

onClickプロップをnullからwaveに更新して、wave()関数をwaveButtonに接続しています。

🧪 テストを実行する

今回のレッスンでは実装する機能が多いので、追加する機能3つに対してテストを行います。

App.jsを更新したら、ターミナル上でyarn client startを実行してください。

ローカルサーバーを介して表示されているWebアプリケーションから右クリック → Inspectを選択し、Consoleの出力結果を確認してみましょう。

下記のようなエラーが表示されていれば、テストは成功です。

これからcontractAddresscontractABIを設定していきます。

🏠 contractAddressの設定

Sepolia Test Networkにコントラクトをデプロイしたとき、下記がターミナルに出力されていたことを覚えてますか?

Deploying contracts with account:  0x821d451FB0D9c5de6F818d700B801a29587C3dCa
Account balance: 324443375262705541
Contract deployed to: 0x3610145E4c6C801bBf2F926DFd8FDd2cE1103493

App.jscontractAddressを設定するために、Contract deployed toの出力結果(0x..)が必要です。

Contract deployed toに続く出力結果をどこかにメモしていた場合は、このままレッスンを進めましょう。

再度この結果を出力する場合は、ルートディレクトリにいることを確認してターミナル上で下記を実行してください。

yarn contract deploy

コントラクトのデプロイ先のアドレスを取得できたら、App.jscontractAddressという新規の変数を追加しましょう。Contract deployed toの出力結果(0x..)を設定していきます。

const [currentAccount, setCurrentAccount] = useState('')の直下にcontractAddressを作成しましょう。以下のようになります。

const [currentAccount, setCurrentAccount] = useState("");
/*
* デプロイされたコントラクトのアドレスを保持する変数を作成
*/
const contractAddress = "あなたの WavePortal の address を貼り付けてください";

App.jsを更新したら、ローカルサーバーにホストされているWebアプリケーションからConsoleを確認してみましょう。

contractAddressに関するエラーが消えていれば、成功です。

📂 ABI ファイルを取得する

ABI(Application Binary Interface)はコントラクトの取り扱い説明書のようなものです。

Webアプリケーションがコントラクトと通信するために必要な情報が、ABIファイルに含まれています。

コントラクト一つ一つにユニークなABIファイルが紐づいており、その中には下記の情報が含まれています。

  1. そのコントラクトに使用されている関数の名前
  2. それぞれの関数にアクセスするため必要なパラメータとその型
  3. 関数の実行結果に対して返るデータ型の種類

ABIファイルは、コントラクトがコンパイルされた時に生成され、artifactsディレクトリに自動的に格納されます。

ターミナルでpackages/contractディレクトリに移動し、lsを実行しましょう。

artifactsディレクトリの存在を確認してください。

ABIファイルの中身は、WavePortal.jsonというファイルに格納されています。

下記を実行して、ABIファイルをコピーしましょう。

  1. ターミナル上でpackages/contractにいることを確認する(もしくは移動する)。

  2. ターミナル上で下記を実行し、WavePortal.jsonを開きましょう。※ ファインダーから直接開くことも可能です。

    code artifacts/contracts/WavePortal.sol/WavePortal.json
  3. VS CodeでWavePortal.jsonファイルが開かれるので、中身をすべてコピーしましょう。※ VS Codeのファインダーを使って、直接WavePortal.jsonを開くことも可能です。

次に、下記を実行して、ABIファイルをWebアプリケーションから呼び出せるようにしましょう。

  1. ターミナル上でclient/srcに移動する。

  2. srcディレクトリの中にutilsディレクトリを作成して、その中にWavePortal.jsonファイルを作成する。

    mkdir utils
    touch utils/WavePortal.json
  3. 下記を実行して、WavePortal.jsonファイルをVS Codeで開く。

    code client/src/utils/WavePortal.json
  4. 先ほどコピーしたcontract/artifacts/contracts/WavePortal.sol/WavePortal.jsonの中身を新しく作成したclient/src/utils/WavePortal.jsonの中に貼り付けてください。

ABIファイルの準備ができたので、App.jsにインポートしましょう。

下記のようにApp.jsを更新します。

/* ethers 変数を使えるようにする*/
import { ethers } from "ethers";
import React, { useEffect, useState } from "react";

import "./App.css";

/* ABIファイルを含むWavePortal.jsonファイルをインポートする*/
import abi from "./utils/WavePortal.json";

const App = () => {
/*
* ユーザーのパブリックウォレットを保存するために使用する状態変数を定義します。
*/
const [currentAccount, setCurrentAccount] = useState("");
console.log("currentAccount: ", currentAccount);
/*
* デプロイされたコントラクトのアドレスを保持する変数を作成
*/
const contractAddress = "あなたのコントラクトアドレスを貼り付けてください";
/*
* ABIの内容を参照する変数を作成
*/
const contractABI = abi.abi;

/*
* window.ethereumにアクセスできることを確認します。
*/
const checkIfWalletIsConnected = async () => {
try {
const { ethereum } = window;
if (!ethereum) {
console.log("Make sure you have MetaMask!");
return;
} else {
console.log("We have the ethereum object", ethereum);
}
/*
* ユーザーのウォレットへのアクセスが許可されているかどうかを確認します。
*/
const accounts = await ethereum.request({ method: "eth_accounts" });
if (accounts.length !== 0) {
const account = accounts[0];
console.log("Found an authorized account:", account);
setCurrentAccount(account);
} else {
console.log("No authorized account found");
}
} catch (error) {
console.log(error);
}
};

/*
* connectWalletメソッドを実装
*/
const connectWallet = async () => {
try {
const { ethereum } = window;
if (!ethereum) {
alert("Get MetaMask!");
return;
}
const accounts = await ethereum.request({
method: "eth_requestAccounts",
});
console.log("Connected: ", accounts[0]);
setCurrentAccount(accounts[0]);
} catch (error) {
console.log(error);
}
};

/*
* waveの回数をカウントする関数を実装
*/
const wave = async () => {
try {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
/*
* ABIを参照
*/
const wavePortalContract = new ethers.Contract(
contractAddress,
contractABI,
signer
);
let count = await wavePortalContract.getTotalWaves();
console.log("Retrieved total wave count...", count.toNumber());
/*
* コントラクトに👋(wave)を書き込む。
*/
const waveTxn = await wavePortalContract.wave();
console.log("Mining...", waveTxn.hash);
await waveTxn.wait();
console.log("Mined -- ", waveTxn.hash);
count = await wavePortalContract.getTotalWaves();
console.log("Retrieved total wave count...", count.toNumber());
} else {
console.log("Ethereum object doesn't exist!");
}
} catch (error) {
console.log(error);
}
};

/*
* WEBページがロードされたときに下記の関数を実行します。
*/
useEffect(() => {
checkIfWalletIsConnected();
}, []);

return (
<div className="mainContainer">
<div className="dataContainer">
<div className="header">
<span role="img" aria-label="hand-wave">
👋
</span>{" "}
WELCOME!
</div>
<div className="bio">
イーサリアムウォレットを接続して、「
<span role="img" aria-label="hand-wave">
👋
</span>
(wave)」を送ってください
<span role="img" aria-label="shine">

</span>
</div>
{/*
* waveボタンにwave関数を連動させる。
*/}
<button className="waveButton" onClick={wave}>
Wave at Me
</button>
{/*
* ウォレットコネクトのボタンを実装
*/}
{!currentAccount && (
<button className="waveButton" onClick={connectWallet}>
Connect Wallet
</button>
)}
{currentAccount && (
<button className="waveButton" onClick={connectWallet}>
Wallet Connected
</button>
)}
</div>
</div>
);
};

export default App;

コントラクトアドレスをご自身のものに更新するのをお忘れなく!

const contractAddress = "あなたのコントラクトアドレスを貼り付けてください";

新しく実装されいる機能は下記の3つです。

1 . ABI ファイルを含む WavePortal.json ファイルをインポートする

import abi from "./utils/WavePortal.json";

2 . ABI の内容を参照する変数を作成

const contractABI = abi.abi;

ABIの参照先を確認しましょう。wave関数の中に実装されています。

const wave = async () => {
try {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
/*
* ABIをここで参照
*/
const wavePortalContract = new ethers.Contract(
contractAddress,
contractABI,
signer
);
let count = await wavePortalContract.getTotalWaves();
console.log("Retrieved total wave count...", count.toNumber());
} else {
console.log("Ethereum object doesn't exist!");
}
} catch (error) {
console.log(error);
}
};

ABIファイルをApp.jsに追加すると、フロントエンドでWaveボタンがクリックされたとき、ブロックチェーン上のコントラクトから正式にデータを読み取ることができます

3 . データをブロックチェーンに書き込む

コントラクトにデータを書き込むためのコードを実装しました。

const wave = async () => {
try {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const wavePortalContract = new ethers.Contract(
contractAddress,
contractABI,
signer
);
let count = await wavePortalContract.getTotalWaves();
console.log("Retrieved total wave count...", count.toNumber());
/*
* コントラクトに👋(wave)を書き込む。ここから...
*/
const waveTxn = await wavePortalContract.wave();
console.log("Mining...", waveTxn.hash);
await waveTxn.wait();
console.log("Mined -- ", waveTxn.hash);
count = await wavePortalContract.getTotalWaves();
console.log("Retrieved total wave count...", count.toNumber());
/*-- ここまで --*/
} else {
console.log("Ethereum object doesn't exist!");
}
} catch (error) {
console.log(error);
}
};

コントラクトにデータを書き込むコードは、データを読み込むコードに似ています。

主な違いは、コントラクトに新しいデータを書き込むときは、マイナーに通知が送られ、そのトランザクションの承認が求められることです。

データを読み込むときは、そのようなことをする必要はありません。 よって、ブロックチェーンからのデータの読み取りは無料です。

🚀 テストを実行する

ターミナル上で、下記を実行しましょう。

yarn client start

ローカルサーバー上で表示されているWebアプリケーションでInspectを実行し、以下を試してみましょう。

1 . Connect Walletをボタンを押して、WebアプリケーションにあなたのMetaMaskのウォレットアドレスを接続する。

2 . Wave at Meボタンを押して、実際にブロックチェーン上にあなたの「👋(wave)」が反映されているか確認する。

いつものようにローカルサーバーにホストされているWebアプリケーションをInspectし、Consoleを確認しましょう。

例)Wave at Meボタンを2回押した際に出力されたConsoleの結果。

それぞれのWaveがカウントされ、承認されていることが確認できたら、次のステップに進みましょう。

ターミナルを閉じるときは、以下のコマンドが使えます ✍️

  • Mac: ctrl + c
  • Windows: ctrl + shift + w

🌱 Etherscan でトランザクションを確認する

あなたのConsoleに出力されている以下のアドレスをそれぞれコピーして、Etherscan に貼り付けてみましょう。

  • Connected: 0x.. ← これをコピーしてEtherscanに貼り付ける

    🎉 あなたのSepolia Test Network上のトランザクションの履歴が参照できます。

  • Mined -- 0x.. ← これをコピーしてEtherscanに貼り付ける

    🎉 あなたのWebアプリケーションを介してSepolia Test Network上に書き込まれた「👋(wave)」に対するトランザクションの履歴が参照できます。

🙋‍♂️ 質問する

ここまでの作業で何かわからないことがある場合は、Discordの#ethereumで質問をしてください。

ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨

1. 質問が関連しているセクション番号とレッスン番号
2. 何をしようとしていたか
3. エラー文をコピー&ペースト
4. エラー画面のスクリーンショット

おめでとうございます! セクション2が終了しました! #ethereumにあなたのEtherscanのリンクを貼り付けて、コミュニティで進捗を祝いましょう 🎉 Etherscanでトランザクションの確認をしたら、次のレッスンに進んでください 😊