さて、LEARNの第1回「ETH-dApp」のSection 3!
Section 2までで、フロントエンドとバックエンドを作って、テストネットにデプロイするところまでを学んだので、Section 3でどう改良していくか楽しみ。
Lesson 1. ブロックチェーン上にデータを保存
現在のContractはwaveの数だけを格納していたが、メッセージも格納できるようにする改修を入れるとのこと。
Contractの改修(アップデート)は・・・「難しい」と聞いていたが、大きな仕様変更の場合だけなのだろうか・・・
コントラクト改修
とりあえず、最新版のコードがこれ。
contract WavePortal {
uint256 totalWaves;
event NewWave(address indexed from, uint256 timestamp, string msg);
struct Wave {
address waver;
string msg;
uint256 timestamp;
}
Wave[] waves;
constructor() {
console.log("Wave Portal ver 0.1.1");
}
function wave(string memory _msg) public {
totalWaves += 1;
console.log("%s waved w/ message %s", msg.sender, _msg);
waves.push(Wave(msg.sender, _msg, block.timestamp));
emit NewWave(msg.sender, block.timestamp, _msg);
}
function getTotalWaves() public view returns (uint256) {
console.log("We have %d total waves!", totalWaves);
return totalWaves;
}
function getAllWaves() public view returns (Wave[] memory) {
return waves;
}
}
event NewWave(address indexed from, uint256 timestamp, string msg);
変更点は、まずはevent
。
emit NewWave(msg.sender, block.timestamp, _msg);
で、対となるwave
ファンクション内のemit
。
こんな感じでイベントを定義して、フロントエンド側に送る(他のユーザが更新した情報も含めてフロントエンド側に送る)ことができる。
struct Wave {
address waver;
string msg;
uint256 timestamp;
}
次に、struct
。
構造体を定義して、内部でデータを管理できるようにしている。
→timestampっているのか???ブロックのtimestamp使えばいい気はするけど、それだとブロック確定しないとダメだったりするからズレるのかな???
コントラクト確認
const main = async () => {
const [owner, randomPerson, randomPerson2] = 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("The 1st messsage!");
await waveTxn.wait();
waveCount = await waveContract.getTotalWaves();
waveTxn = await waveContract.connect(randomPerson).wave("The 2nd message!!");
await waveTxn.wait();
waveCount = await waveContract.getTotalWaves();
waveTxn = await waveContract.connect(randomPerson2).wave("The 3rd message!!!");
await waveTxn.wait();
waveCount = await waveContract.getTotalWaves();
let allWaves = await waveContract.getAllWaves();
console.log(allWaves);
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
で、実行。
$ npx hardhat run scripts/run.js Wave Portal ver 0.1.1 Contract deployed to: xxxxx Contract deployed by: xxxxx We have 0 total waves! xxxxx waved w/ message The 1st messsage! We have 1 total waves! xxxxx waved w/ message The 2nd message!! We have 2 total waves! xxxxx waved w/ message The 3rd message!!! We have 3 total waves! [ [ 'xxxxx', 'The 1st messsage!', BigNumber { value: "1673420816" }, waver: 'xxxxx', msg: 'The 1st messsage!', timestamp: BigNumber { value: "1673420816" } ], [ 'xxxxx', 'The 2nd message!!', BigNumber { value: "1673420817" }, waver: 'xxxxx', msg: 'The 2nd message!!', timestamp: BigNumber { value: "1673420817" } ], [ 'xxxxx', 'The 3rd message!!!', BigNumber { value: "1673420818" }, waver: 'xxxxx', msg: 'The 3rd message!!!', timestamp: BigNumber { value: "1673420818" } ] ]
無事実行できて、3つのメッセージが格納できた模様。
新しいコントラクトのデプロイ
あ、やはり、
一度デプロイされたスマートコントラクトの変更はできない
っぽい。
再デプロイされた場合は、変数がリセットされるって・・・
というか、完全に別コントラクトとして新しくデプロイされるというのが正しそう。
アップデート時にはここら辺をどう保存するか、どう移行するかを考えないとダメなのんだろうな。
$ npx hardhat run scripts/deploy.js --network goerli Deploying contracts with account: xxxxx Account balance: 132313865381976031 Contract deployed to: xxxxx Contract deployed by: xxxxx
無事成功。
フロントエンド改修
コントラクトアドレス変更
上記の通り新しいコントラクトが作られたのでコントラクトアドレス部分を修正。
wave
ファンクション変更
const waveTxn = await wavePortalContract.wave(messageValue, {
gasLimit: 300000,
});
state
として定義していたmessageValue
を引数に渡して、さらに{gasLimit: 300000}
も追加して呼び出し。
ガス代の最大値を設定する処理も入れておくと、それを超えた場合に処理が中断されるらしい。
ちなみに単位はGasで、300000Gas・・・いくら???
Gasは21Gweiとのことで、1Gwei=0.000000001ETHでだいぶ安い。
テキストエリア追加
{currentAccount && (
<textarea
name="messageArea"
placeholder="メッセージはこちら"
type="text"
id="message"
value={messageValue}
onChange={(e) => setMessageValue(e.target.value)}
/>
)}
ここはシンプル。
currentAccount
があれば(=MetaMaskと接続していたら)、表示するように追記。
wave履歴取得
const getAllWaves = async () => {
const { ethereum } = window;
try {
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const wavePortalContract = new ethers.Contract(
contractAddress,
contractABI,
signer
);
const waves = await wavePortalContract.getAllWaves();
const wavesCleaned = waves.map((wave) => {
return {
address: wave.waver,
timestamp: new Date(wave.timestamp * 1000),
message: wave.message,
};
});
setAllWaves(wavesCleaned);
} else {
console.log("Ethereum object doesn't exist!");
}
} catch (error) {
console.log(error);
}
};
コントラクトを呼び出して、getAllWaves()
を呼び出して、取得したデータを適切な形に整形して(これはわざわざする必要があるのだろうか・・・ま、わかりやすいかもだけど、、、)、state
として定義したallWaves
にセット。
wave履歴表示
{currentAccount &&
allWaves
.slice(0)
.reverse()
.map((wave, index) => {
return (
<div
key={index}
style={{
backgroundColor: "#F8F8FF",
marginTop: "16px",
padding: "8px",
}}
>
<div>Address: {wave.address}</div>
<div>Time: {wave.timestamp.toString()}</div>
<div>Message: {wave.message}</div>
</div>
);
})}
ここはシンプルに(とはいえ、降順に)allWavesを表示。
※とはいえ、ここでslice(0).reverse()
するなら、wavesCleaned
作るところでやっておけばいい気もするが、、、
slice(0)ってなんだ・・・消しても当然動く。。。
emitされたイベントを処理
useEffect(() => {
let wavePortalContract;
const onNewWave = (from, timestamp, message) => {
console.log("NewWave", from, timestamp, message);
setAllWaves((prevState) => [
...prevState,
{
address: from,
timestamp: new Date(timestamp * 1000),
message: message,
},
]);
};
if (window.ethereum) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
wavePortalContract = new ethers.Contract(
contractAddress,
contractABI,
signer
);
wavePortalContract.on("NewWave", onNewWave);
}
return () => {
if (wavePortalContract) {
wavePortalContract.off("NewWave", onNewWave);
}
};
}, []);
ここも難しいことはなく、onNewWave
でemit
されたイベントの引数を使って処理を記載。
あとは、最後のメモリリーク対応。
で、Lesson 1終了。
そういえば、スマートコントラクト側のmemory
が気になったので、以下を読んでみた。
Lesson 2. ユーザにETHを送る
コントラクト改修
constructor() payable {
まずはコンストラクタを変更して、payable
追加で、送金機能追加。
function wave(string memory _msg) public {
totalWaves += 1;
console.log("%s waved w/ message %s", msg.sender, _msg);
waves.push(Wave(msg.sender, _msg, block.timestamp));
emit NewWave(msg.sender, block.timestamp, _msg);
uint256 prizeAmount = 0.0001 ether;
require(
prizeAmount <= address(this).balance,
"Trying to withdraw more money than the contract has."
);
(bool success, ) = (msg.sender).call{value: prizeAmount}("");
require(success, "Failed to withdraw money from contract.");
}
次はwave
ファンクションを修正して、waveを送ってきた人にETHを送る処理を記載。
emit
の後が、送金処理。
require
がSolidity特有の構文かな。第一引数がfalse
ならトランザクションをキャンセルする。
必須チェックみたいなもので、そこに引っ掛かったらエラーを発生させるというものっぽい。
scripts/run.js
改修
let contractBalance = await hre.ethers.provider.getBalance(
waveContract.address
);
残高を取得するコード。
const waveContract = await waveContractFactory.deploy({
value: hre.ethers.utils.parseEther("0.1"),
});
run.js内で重要なところとしては、デプロイする際に資金を提供するコード。
scripts/run.js
実行
$ npx hardhat run scripts/run.js Compiled 1 Solidity file successfully Wave Portal ver 0.1.2 with pay function. Contract deployed to: xxxxx Contract balance: 0.1 We have 0 total waves! xxxxx waved w/ message The 1st messsage! Contract balance: 0.0999 We have 1 total waves! xxxxx waved w/ message The 2nd message!! Contract balance: 0.0998 We have 2 total waves! xxxxx waved w/ message The 3rd message!!! Contract balance: 0.0997 We have 3 total waves! [ [ 'xxxxx', 'The 1st messsage!', BigNumber { value: "1673442674" }, waver: 'xxxxx', msg: 'The 1st messsage!', timestamp: BigNumber { value: "1673442674" } ], [ 'xxxxx', 'The 2nd message!!', BigNumber { value: "1673442675" }, waver: 'xxxxx', msg: 'The 2nd message!!', timestamp: BigNumber { value: "1673442675" } ], [ 'xxxxx', 'The 3rd message!!!', BigNumber { value: "1673442676" }, waver: 'xxxxx', msg: 'The 3rd message!!!', timestamp: BigNumber { value: "1673442676" } ] ]
※ethers.jsの詳細はこちら。
scripts/deploy.js
改修と実行
で、deploy.js
でも資金を提供するようにしてデプロイ・・・・・・・
エラー出たと思ったら、deploy.jsのデプロイ後のコードがバグってた・・・・
ちゃんとデプロイされていたのにエラーになっていた。
ガス代(テストネットだけど・・・)もしっかり払っていたのに複数回同じものをデプロイしていた・・・
deploy.jsのコーディングミスも考えないとダメって意外としんどいな。
兎にも角にも、Section 3 終了。
まとめ
今回は、
- ブロックチェーン上にデータを保存する
- ユーザにETHを送る
という改修を入れてみた。
ブロックチェーン上にデータを保存するには、struct
を使ってデータを定義して、その配列をContractの変数として持つことでテーブルを作ったみたいなことが可能。
で、そのデータが更新されたことをユーザに通知するためのものがevent
であり、それを通知するemit
。
ユーザにETHを送る機能ってどんな機能なんだろう・・・というのは置いておいて、コントラクト自体にETHを持たせておいて、それをユーザに配るということができるっぽい。なんともユーザにとっては嬉しいお金配りおじさん機能。
require
ファンクションを使ってしっかりとエラーチェックすることで、残高がない場合にエラーを発生させたりすることを学んだ。