Challenge UNCHAIN vol.6 – コントラクトのアップデート –

さて、LEARNの第1回「ETH-dApp」のSection 3!

Section 2までで、フロントエンドとバックエンドを作って、テストネットにデプロイするところまでを学んだので、Section 3でどう改良していくか楽しみ。

No Title

Learn / ETH-dApp このプロジェクトでは、イーサリアムネットワーク上にスマートコントラクトを実装して、スマートコントラクトとやりとりできる独自のWebアプリケーションを構築します。 プロジェクトを進めるには以下の技術が必要です。 いますべてを理解している必要はありません。 …

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);
      }
    };
  }, []);

ここも難しいことはなく、onNewWaveemitされたイベントの引数を使って処理を記載。

あとは、最後のメモリリーク対応。

で、Lesson 1終了。

そういえば、スマートコントラクト側のmemoryが気になったので、以下を読んでみた。

Storage vs Memory in Solidity – GeeksforGeeks

Storage and Memory keywords in Solidity are analogous to Computer’s hard drive and Computer’s RAM. Much like RAM, Memory in Solidity is a temporary place to store data whereas Storage holds data between function calls. The Solidity Smart Contract can use any amount of memory during the execution but once the execution stops, the Memory is completely wiped off for the next execution.

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の詳細はこちら。

Documentation

The ethers.js library aims to be a complete and compact library for interacting with the Ethereum Blockchain and its ecosystem. It was originally designed for use with ethers.io and has since expanded into a more general-purpose library.

scripts/deploy.js改修と実行

で、deploy.jsでも資金を提供するようにしてデプロイ・・・・・・・

エラー出たと思ったら、deploy.jsのデプロイ後のコードがバグってた・・・・

ちゃんとデプロイされていたのにエラーになっていた。

ガス代(テストネットだけど・・・)もしっかり払っていたのに複数回同じものをデプロイしていた・・・

deploy.jsのコーディングミスも考えないとダメって意外としんどいな。

兎にも角にも、Section 3 終了。

まとめ

今回は、

  • ブロックチェーン上にデータを保存する
  • ユーザにETHを送る

という改修を入れてみた。

ブロックチェーン上にデータを保存するには、structを使ってデータを定義して、その配列をContractの変数として持つことでテーブルを作ったみたいなことが可能。

で、そのデータが更新されたことをユーザに通知するためのものがeventであり、それを通知するemit

ユーザにETHを送る機能ってどんな機能なんだろう・・・というのは置いておいて、コントラクト自体にETHを持たせておいて、それをユーザに配るということができるっぽい。なんともユーザにとっては嬉しいお金配りおじさん機能。

requireファンクションを使ってしっかりとエラーチェックすることで、残高がない場合にエラーを発生させたりすることを学んだ。