lesson-4_スマートコントラクトにデータを保存しよう
📦 データを保存しよう
このレッスンでは、ユーザーがあなたに送った「👋(wave)」の数をデータとして保存する方法を学びます。
ブロックチェーンは、AWSのようなクラウド上にデータを保存できるサーバーのようなものです。
しかし、誰もそのデータを所有していません。
世界中に、ブロックチェーン上にデータを保存する作業を行う「マイナー」と呼ばれる人々が存在します。この作業に対して、私たちは代金を支払います。
その代金は、通称ガス代と呼ばれます。
イーサリアムのブロックチェーン上にデータを書き込む場合、私たちは代金として$ETH
を「マイナー」に支払います。
それでは、「👋(wave)」を保存するために、WavePortal.sol
を更新していきましょう。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "hardhat/console.sol";
contract WavePortal {
uint256 private _totalWaves;
constructor() {
console.log("Here is my first smart contract!");
}
function wave() public {
_totalWaves += 1;
console.log("%s has waved!", msg.sender);
}
function getTotalWaves() public view returns (uint256) {
console.log("We have %d total waves!", _totalWaves);
return _totalWaves;
}
}
新しく追加されたコードの理解を深めましょう。
uint256 private _totalWaves;
自動的に0
に初期化される_totalWaves
変数が追加されました。この変数は「状態変数」と呼ばれ、WavePortal
コントラクトのストレージに永続的に保存されます。
- uint256 は、非常に大きな数を扱うことができる「符号なし整数のデータ型」を意味します。
🎁 Solidity のアクセス修飾子について
function wave() public {
_totalWaves += 1;
console.log("%s has waved!", msg.sender);
}
ここで、「👋(wave)」の回数を記録するwave()
関数が追加されました。まず、public
について見ていきます。
これは、Solidityのアクセス修飾子の1つです。
Solidity含めさまざまな言語において、各関数のアクセス権限について指定する必要があり ます。その指定を行うのが、アクセス修飾子です。
Solidityには、4つのアクセス修飾子が存在します。
-
public
:public
で定義された関数や変数は、それらが定義されているコントラクト、そのコントラクトが継承された別のコントラクト、それらコントラクトの外部と、基本的にどこからでも呼び出すことができます。Solidityでは、アクセス修飾子がついてない関数を、自動的にpublic
として扱います。 -
private
:private
で定義された関数や変数は、それらが定義されたコントラクトでのみ呼び出すことができます。 -
internal
:internal
で定義された関数や変数は、それらが定義されたコントラクトと、そのコントラクトが継承された別のコントラクトの両方から呼び出すことができます。Solidityでは、アクセス修飾子がついてない変数を、自動的にinternal
として扱います。 -
external
:external
で定義された関数や変数は、外部からのみ呼び出すことができます。
以下に、Solidityのアクセス修飾子とアクセス権限についてまとめています。
これからSolidityのアクセス修飾子は頻繁に登場するので、まずは大まかな理解ができれば大丈夫です。
🔍 msg.sender
について
function wave() public {
_totalWaves += 1;
console.log("%s has waved!", msg.sender);
}
wave()
関数の中にmsg.sender
が登場するのをお気付きでしょうか?
msg.sender
に入る値は、ずばり、関数を呼び出した人(=あなたに「👋(wave)」を送った人)のウォレットアドレスです。
これは、ユーザー認証のようなものです。
- スマートコントラクトに含まれる関数を呼び出すには、ユーザーは有効なウォレットを接続する必要があります。
msg.sender
では、誰が関数を呼び出したかを正確に把握し、ユーザー認証を行っています。
🖋 Solidity の関数修飾子について
Solidityには、関数(function)に対してのみ使用される修飾子(=関数修飾子)が存在します。
Solidity開発では関数修飾子を意識しておかないとデータを記録する際のコスト(=ガス代)が跳ね上がってしまうので注意が必要です。
ここでポイントとなるのは、ブロックチェーンに値を書き込むにはガス代を払う必要があること、そしてブロックチェーンから値を参照するだけなら、ガス代を払う必要がないことです。
ここでは、主要な2つの関数修飾子を紹介します。
view
:view
関数は、読み取り専用の関数であり、呼び出した後に関数の中で定義された状態変数が変更されないようにします。pure
:pure
関数は、関数の中で定義された状態変数を読み込んだり変更したりせず、関数に渡されたパラメータや関数に存在するローカル変数のみを使用して値を返します。
以下に、Solidityの関数修飾子pure
とview
についてまとめています。
ここまで理解してほしいのは、pure
やview
関数を使用すれば、ガス代を削減できるということです。
同時に、ブロックチェーン上にデータを書き込まないことで、処理速度も向上します。
WavePortal.sol
に追加された下記の関数を見ていきましょう。
function wave() public {
_totalWaves += 1;
console.log("%s has waved!", msg.sender);
}
wave()
関数には関数修飾子がついていないことをお気付きでしょうか。
- 同一のユーザーが送った「👋(wave)」の回数が
_totalWaves += 1
によってカウントされ、ブロックチェーン上にデータが書き込まれます。 - また、この関数が呼び出されると、
console.log("%s has waved!", msg.sender)
によって、あなたに「👋(wave)」を送ったユーザーのアドレスがターミナル上に表示されます。
それでは、下記のコードも見ていきましょう。
function getTotalWaves() public view returns (uint256) {
console.log("We have %d total waves!", _totalWaves);
return _totalWaves;
}
一方、view
という関数修飾子がついたgetTotalWaves()
関数は、ユーザーがあなたに送った「👋(wave)」の総数の参照のみを行います。
✅ run.js
を更新して関数を呼び出す
次に、run.js
を以下のように更新していきます。
const main = async () => {
const [owner, randomPerson] = await hre.ethers.getSigners();
const waveContractFactory = await hre.ethers.getContractFactory("WavePortal");
const waveContract = await waveContractFactory.deploy();
const wavePortal = await waveContract.deployed();
console.log("Contract deployed to:", wavePortal.address);
console.log("Contract deployed by:", owner.address);
let waveCount;
waveCount = await waveContract.getTotalWaves();
let waveTxn = await waveContract.wave();
await waveTxn.wait();
waveCount = await waveContract.getTotalWaves();
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
WavePortal.sol
の中でpublic
と指定したwave()
関数とgetTotalWaves()
関数は、ブロックチェーン上で呼び出すことができるようになりました。
run.js
から、それら関数を呼び出していきます。
- 復習となりますが、
run.js
はデバッグ用のテストコードです。 run.js
は本番環境でユーザーがあなたのスマートコントラクトを呼び出すシチュエーションを想定して、コードが問題なく走るかテストするために、作られています。
public
で定義した関数はAPIのエンドポイントのようなものです。
更新された部分を1行ずつ見ていきましょう。
const [owner, randomPerson] = await hre.ethers.getSigners();
ブロックチェーンにWavePortal
コントラクトをデプロイする際、「👋(wave)」を送る側のウォレットアドレスが必要です。
hre.ethers.getSigners()
はHardhatが提供する任意のアドレスを返す関数です。
-
ここでは、コントラクト所有者(=あなた)のウォレットアドレスと、あなたに「👋(wave)」を送るユーザーのウォレットアドレスをHardhatがそれぞれ生成し、
owner
とrandomPerson
という変数に格納しています。 -
randomPerson
は、テスト環境で「👋(wave)」を送ってくるユーザーだと思ってください。
次に、下記のコードを見ていきましょう。
console.log("Contract deployed to:", wavePortal.address);
ここでは、あなたのスマートコントラクトのデプロイ先のアドレス(= wavePortal.address
)をターミナルに出力しています。
console.log("Contract deployed by:", owner.address);
ここでは、WavePortal
コントラクトをデプロイした人(=あなた)のアドレス(= owner.address
)をターミナルに出力しています。
最後に、下記のコードを見ていきましょう。
let waveCount;
waveCount = await waveContract.getTotalWaves();
let waveTxn = await waveContract.wave();
await waveTxn.wait();
waveCount = await waveContract.getTotalWaves();
ここでは、通常のAPIと同じように、関数を手動で呼び出しています。1行ずつ見ていきましょう。
let waveCount;
waveCount = await waveContract.getTotalWaves();
まず、let waveCount
でローカル変数を宣言します。
次に、waveContract.getTotalWaves()
でWavePortal.sol
に記載されたgetTotalWaves()
を呼び出し、既存の「👋(wave)」の総数を取得します。
let waveTxn = await waveContract.wave();
await waveTxn.wait();
let waveTxn = await waveContract.wave()
では、ユーザーが新しい「👋(wave)」を送ったことを承認するまで、コントラクトからの応答をフロントエンドが待機するよう設定しています。
.wave()
関数ではブロックチェーン上の書き込みが発生するので、ガス代がかかります。よって、ユーザーは取引を確認する必要があります。
MetaMaskを使っていて、取引を承認するために数秒手間どった経験はありませんか?
あなたが承認を行っている間、コードは次の処理に進まず、待機しています。
承認が終わったら、await waveTxn.wait()
が実行され、トランザクションの結果を取得します。コードが冗長に感じるかもしれませんが、大事な処理です。
waveCount = await waveContract.getTotalWaves();
ここで最後に、waveCount
をもう一度取得して、+1
されたかどうかを確認します。
🧙♀️ テストを実行しよう
ルートディレクトリにいることを確認して、ターミナルで下記を実行してみましょう。
yarn contract run:script