Web3.0入門 ブロックチェーンメッセージングアプリを試作する (Polygon)

## 対象の読者
- Web3.0を始めたいけど何から手をつけていいか分からない
- 簡単なWeb2.0アプリケーションであれば実装できる

## 成果物例

![seikabutsu_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/ee6d3cb9-4502-c411-ae59-68615c1456b7.png)

![seikabutsu_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/97b0983b-5adb-927c-3452-574423bc9a87.png)


コメントを送信し、そのコメントをブロックチェーンに記録するアプリケーションとなります。
ブロックチェーンからコメントを取得して表示し、コメント追加時に自身のMATIC(通貨)を消費して(GAS代)ブロックチェーンに記録します。
（テスト用MATICは無料で獲得できるので心配はいりません。）

## 前提条件
当記事では、以下のアーキテクチャにより解説を進めます。

- 開発プラットフォーム
  - Alchemy（[公式サイト](https://www.alchemy.com/)）
- ブロックチェーン
  - Polygon(テストネットワーク)（[polygonscan(Mumbai)](https://mumbai.polygonscan.com/)）
- フレームワーク
  - Hardhat（[公式サイト](https://hardhat.org/)）
  - React

## 事前準備

### Alchemyの登録

#### 1.Alchemyの無料プランに登録

https://www.alchemy.com/

Alchemy公式サイトより、サインアップします。
途中、「Create your first app」の画面になりますが、NETWORKは **Polygon Mumbai** を選択してください。

![alchemy_create_app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/808cc179-a548-1085-82ee-ffd4ae5c3d37.png)

#### 2. APIキーの表示
ダッシュボードに遷移したら登録完了です。
APIキーを今後使用するのであらかじめ控えておきます。

![alchemy_api_key.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/afb0ae99-9828-787d-759f-bbdc2f6669e7.png)

### MetaMaskインストール

#### 1.MetaMaskをインストール

https://metamask.io/download/

Chrome, Firefox, Edge, Braveのいずれかのブラウザが対応しています。
MetaMaskで自身のウォレットを作成します。

![metamask_download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/7e69c9e3-1779-9d5e-748c-a458862f36f4.png)

#### 2.テストネットワークを表示

MetaMaskの設定で、「テストネットワークを表示」をオンにします。

![metamask_show_config_test_network.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/2d8c4c2f-3d47-d477-f57f-69171ae27935.png)

#### 3.テストネットワークを追加

以下の内容でテストネットワークを追加します。

|項目|入力内容|
|---|---|
|ネットワーク名|(任意)|
|新しいRPC URL|先ほどAlchemyのAPIキーを表示した際の「HTTP」のURL|
|チェーンID|80001(固定)|
|通貨記号|MATIC(固定)|
|ブロックエクスプローラーのURL|https://mumbai.polygonscan.com/|

![metamask_add_test_network.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/14330526-7d42-6c04-e1bb-198486e8dcc0.png)

#### 4.秘密鍵の取得

自身のウォレットの秘密鍵を取得します。
スマートコントラクトのデプロイ時にMATIC(通貨)を消費するため、アプリケーションの環境変数に後程転記します。
（テスト用MATICは無料で獲得できるので心配はいりません。）

![metamask_private_key_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/8fa3a37c-777d-8903-194f-482b6cd59ec1.png)

### MATICの獲得

以下にアクセスします。

https://mumbaifaucet.com/

自身のMetaMask(ウォレット)のアドレスを入力し、1MATICを獲得します。
（秘密鍵ではなくアドレスです。）

## 実践
### Hardhatインストール

ここからは実装作業に移ります。
スマートコントラクトのデプロイにHardhatを使用します。

#### 1.プロジェクトの作成

`npm`もしくは`yarn`コマンドが使えることを確認してください。

`hardhat`をインストールします。

```
$ yarn add -D hardhat
```

次に、hardhatの初期化を実行します。
`What do you want to do?`は、Typescriptのテンプレートを選択しています。

```
$ npx hardhat
888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

👷 Welcome to Hardhat v2.9.7 👷‍

✔ What do you want to do? · Create an advanced sample project that uses TypeScript
✔ Hardhat project root: · /Users/ryoheitakagi/ghq/github.com/ryohei-takagi/web3-chat
✔ Do you want to add a .gitignore? (Y/n) · y
✔ Do you want to install this sample project's dependencies with yarn (@nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-etherscan dotenv eslint eslint-config-prettier eslint-config-standard eslint-plugin-import eslint-plugin-node eslint-plugin-prettier eslint-plugin-promise hardhat-gas-reporter prettier prettier-plugin-solidity solhint solidity-coverage @typechain/ethers-v5 @typechain/hardhat @typescript-eslint/eslint-plugin @typescript-eslint/parser @types/chai @types/node @types/mocha ts-node typechain typescript)? (Y/n) · y



yarn add --dev @nomiclabs/hardhat-waffle@^2.0.0 ethereum-waffle@^3.0.0 chai@^4.2.0 @nomiclabs/hardhat-ethers@^2.0.0 ethers@^5.0.0 @nomiclabs/hardhat-etherscan@^3.0.0 dotenv@^16.0.0 eslint@^7.29.0 eslint-config-prettier@^8.3.0 eslint-config-standard@^16.0.3 eslint-plugin-import@^2.23.4 eslint-plugin-node@^11.1.0 eslint-plugin-prettier@^3.4.0 eslint-plugin-promise@^5.1.0 hardhat-gas-reporter@^1.0.4 prettier@^2.3.2 prettier-plugin-solidity@^1.0.0-beta.13 solhint@^3.3.6 solidity-coverage@^0.7.16 @typechain/ethers-v5@^7.0.1 @typechain/hardhat@^2.3.0 @typescript-eslint/eslint-plugin@^4.29.1 @typescript-eslint/parser@^4.29.1 @types/chai@^4.2.21 @types/node@^12.0.0 @types/mocha@^9.0.0 ts-node@^10.1.0 typechain@^5.1.2 typescript@^4.5.2
```

各種ライブラリがインストールされ、`package.json`が以下のようになっていることを確認します。

```package.json
{
  "name": "web3-chat",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ryohei-takagi/web3-chat.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/ryohei-takagi/web3-chat/issues"
  },
  "homepage": "https://github.com/ryohei-takagi/web3-chat#readme",
  "devDependencies": {
    "@nomiclabs/hardhat-ethers": "^2.0.0",
    "@nomiclabs/hardhat-etherscan": "^3.0.0",
    "@nomiclabs/hardhat-waffle": "^2.0.0",
    "@typechain/ethers-v5": "^7.0.1",
    "@typechain/hardhat": "^2.3.0",
    "@types/chai": "^4.2.21",
    "@types/mocha": "^9.0.0",
    "@types/node": "^12.0.0",
    "@typescript-eslint/eslint-plugin": "^4.29.1",
    "@typescript-eslint/parser": "^4.29.1",
    "chai": "^4.2.0",
    "dotenv": "^16.0.0",
    "eslint": "^7.29.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-config-standard": "^16.0.3",
    "eslint-plugin-import": "^2.23.4",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-prettier": "^3.4.0",
    "eslint-plugin-promise": "^5.1.0",
    "ethereum-waffle": "^3.0.0",
    "ethers": "^5.0.0",
    "hardhat": "^2.9.7",
    "hardhat-gas-reporter": "^1.0.4",
    "prettier": "^2.3.2",
    "prettier-plugin-solidity": "^1.0.0-beta.13",
    "solhint": "^3.3.6",
    "solidity-coverage": "^0.7.16",
    "ts-node": "^10.1.0",
    "typechain": "^5.1.2",
    "typescript": "^4.5.2"
  }
}
```

デフォルトの状態でテストが通るか確認してみます。

```
$ npx hardhat test
Generating typings for: 2 artifacts in dir: typechain for target: ethers-v5
Successfully generated 5 typings!
Compiled 2 Solidity files successfully


  Greeter
Deploying a Greeter with greeting: Hello, world!
Changing greeting from 'Hello, world!' to 'Hola, mundo!'
    ✔ Should return the new greeting once it's changed (1449ms)


  1 passing (1s)

```

テストが通りました。
次に、`hardhat.config.ts`の修正に移ります。

#### 2.hardhat.config.tsの修正

以下のコードに注目してください。

```hardhat.config.ts
// 省略

const config: HardhatUserConfig = {
  solidity: "0.8.4",
  networks: {
    ropsten: {
      url: process.env.ROPSTEN_URL || "",
      accounts:
        process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
    },
  },
  gasReporter: {
    enabled: process.env.REPORT_GAS !== undefined,
    currency: "USD",
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY,
  },
};

// 省略
```

`ropsten`のネットワーク定義が既に存在します。
ここに、Polygonテストネットワークの定義を追加しましょう。

```
mumbai: {
  url: POLYGON_MUMBAI_ALCHEMY_URL,
  accounts: [`0x${PRIVATE_KEY}`]
},
```

`.env`に以下を追加します。

```.env
PRIVATE_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
POLYGON_MUMBAI_ALCHEMY_URL=https://polygon-mumbai.g.alchemy.com/v2/XXXXXXXXXXXXXXXXXXXX
```

`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`の部分は、各自置き換えてください。
`PRIVATE_KEY`は、自身のMetaMaskウォレットの秘密鍵です。
`POLYGON_MUMBAI_ALCHEMY_URL`は、AlchemyのAPIキーを表示した際の「HTTP」のURLです。

`hardhat.config.ts`は以下のようになります。

```hardhat.config.ts
import * as dotenv from "dotenv";

import { HardhatUserConfig, task } from "hardhat/config";
import "@nomiclabs/hardhat-etherscan";
import "@nomiclabs/hardhat-waffle";
import "@typechain/hardhat";
import "hardhat-gas-reporter";
import "solidity-coverage";

dotenv.config();

const { PRIVATE_KEY, POLYGON_MUMBAI_ALCHEMY_URL } = process.env;

// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

const config: HardhatUserConfig = {
  solidity: "0.8.4",
  networks: {
    ropsten: {
      url: process.env.ROPSTEN_URL || "",
      accounts:
        process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
    },
    mumbai: {
      url: POLYGON_MUMBAI_ALCHEMY_URL,
      accounts: [`0x${PRIVATE_KEY}`]
    },
  },
  gasReporter: {
    enabled: process.env.REPORT_GAS !== undefined,
    currency: "USD",
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY,
  },
};

export default config;
```

#### 3.スマートコントラクト定義の作成

デフォルトでは、`contracts/Greeter.sol`が存在しています。
当記事では、メッセージングアプリケーションを試作するため、新しく定義を作成します。

```Comments.sol
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "hardhat/console.sol";

contract Comments {
    struct Comment {
        uint32 id;
        address creator_address;
        string creator;
        string message;
        uint created_at;
    }

    uint32 private idCounter;
    Comment[] private comments;

    event CommentAdded(Comment comment);

    function getComments() public view returns(Comment[] memory) {
        return comments;
    }

    function addComment(string calldata creator, string calldata message) public {
        Comment memory comment = Comment({
            id: idCounter,
            creator_address: msg.sender,
            creator: creator,
            message: message,
            created_at: block.timestamp
        });
        comments.push(comment);
        idCounter++;
        emit CommentAdded(comment);
    }
}
```

`Comments.sol`を作成しました。

以下の2つのfunctionに注目してください。

```
function getComments() public view returns(Comment[] memory)

function addComment(string calldata creator, string calldata message) public
```

コメントの取得と、コメントの追加をおこないます。
コメントを追加した際に、ブロックチェーンにコメントが記録されるようになります。

また、以下の記述に注目してください。

```
emit CommentAdded(comment);
```

コメント追加後に、クライアントへイベントを発行することができます。
このイベントで、ブロックチェーンに記録されたことを発火し、これをトリガーに完了メッセージなどを表示したり後続の処理をクライアントで実装することができます。

#### 4.テストの作成

スマートコントラクトのデプロイ後に障害や問題があると修正が困難になる可能性があります。
事前に入念なテストを作成することを推奨します。

`test/index.ts`に以下のテストを追加します。

```test/index.ts
import { expect } from "chai";
import { ethers } from "hardhat";

describe("Comments", function () {
  it("Should add and fetch successfully", async function () {
    const Comments = await ethers.getContractFactory("Comments");
    const comments = await Comments.deploy();
    await comments.deployed();

    expect(await comments.getComments()).to.be.lengthOf(0);

    const tx1 = await comments.addComment("太郎", "Web3はいいぞ");
    await tx1.wait();

    const res1 = await comments.getComments();
    expect(res1).to.be.lengthOf(1);
    expect(res1[0].id).to.equal(0);
    expect(res1[0].message).to.equal("Web3はいいぞ");
    expect(res1[0].creator).to.equal("太郎");

    const tx2 = await comments.addComment("花子", "Web3は最高だ！");
    await tx2.wait();

    const res2 = await comments.getComments();
    expect(res2).to.be.lengthOf(2);
    expect(res2[1].id).to.equal(1);
    expect(res2[1].creator).to.equal("花子");
    expect(res2[1].message).to.equal("Web3は最高だ！");

    expect(res2[0].id).to.equal(0);
    expect(res2[0].message).to.equal("Web3はいいぞ");
    expect(res2[0].creator).to.equal("太郎");
  });
});
```

以下の手順でテストをしていることがコード上から読み取ることができます。

1. スマートコントラクトのデプロイ
2. コメントを取得して0件であることを確認
3. コメントを追加 `太郎「Web3はいいぞ」`
4. コメントを取得して1件存在することを確認
5. そのコメントは`太郎「Web3はいいぞ」`であることを確認
6. コメントを追加 `花子「Web3は最高だ！」`
7. コメントを取得して2件存在することを確認
8. そのコメントは`太郎「Web3はいいぞ」`, `花子「Web3は最高だ！」`であることを確認

```
$ npx hardhat test
```

を実行し、テストが通れば完了です。

#### 5.デプロイ

スマートコントラクトをPolygonテストネットワーク(Mumbai)にデプロイしてみましょう。
その前に、localhostにデプロイして確認することができます。

```
$ npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Account #0: 0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (10000 ETH)
Private Key: 0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Account #1: 0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (10000 ETH)
Private Key: 0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

...(省略)

```

```
$ npx hardhat run --network localhost scripts/deploy.ts 
No need to generate any newer typings.
Contract deployed to: 0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

実際にPolygonテストネットワーク(Mumbai)にデプロイする場合は、`--network mumbai`とします。
`hardhat.config.ts`で追加した以下の内容です。

```
mumbai: {
  url: POLYGON_MUMBAI_ALCHEMY_URL,
  accounts: [`0x${PRIVATE_KEY}`]
},
```

```
$ npx hardhat run --network mumbai scripts/deploy.ts
No need to generate any newer typings.
Contract deployed to: 0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

mumbaiへのデプロイが完了した後、以下のアドレスに注目してください。

```
Contract deployed to: 0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

`0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`は、あなたのスマートコントラクトのアドレスです。

以下のサイトにアクセスし、正常にデプロイが完了しているか確認しましょう。
（アドレスを検索窓に入力し、確認します。）

https://mumbai.polygonscan.com

`https://mumbai.polygonscan.com/address/{アドレス}`でも同様に確認ができます。

![polygon_contract_creation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/c72187ac-d5dc-c5a5-0ae2-54dda2401aa1.png)

### クライアントの作成

#### 1.Reactのインストール

当記事では、以下のコマンドでテンプレートを作成しています。

```
$ npx create-react-app client --template typescript
```

Reactの詳しい解説は当記事では割愛しますのでご了承ください。

AlchemyのSDKにより実装を進めるので、以下のコマンドを実行しておきます。

```
$ yarn add @alch/alchemy-web3
```

#### 2.APIの繋ぎこみ

以下のドキュメントを確認してください。

https://docs.alchemy.com/alchemy/tutorials/hello-world-smart-contract/part-4#part-4-marrying-web2-and-web3-connecting-your-smart-contract-to-a-frontend-project

このドキュメントをもとに作れば問題ありません。当記事では要点のみ解説していきます。

#### 3.contract-abi.jsonの配置

まず、`contract-abi.json`を配置してください。

```contract-abi.json
[
  {
    "anonymous": false,
    "inputs": [
      {
        "components": [
          {
            "internalType": "uint32",
            "name": "id",
            "type": "uint32"
          },
          {
            "internalType": "address",
            "name": "creator_address",
            "type": "address"
          },
          {
            "internalType": "string",
            "name": "creator",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "message",
            "type": "string"
          },
          {
            "internalType": "uint256",
            "name": "created_at",
            "type": "uint256"
          }
        ],
        "indexed": false,
        "internalType": "struct Comments.Comment",
        "name": "comment",
        "type": "tuple"
      }
    ],
    "name": "CommentAdded",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "creator",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "message",
        "type": "string"
      }
    ],
    "name": "addComment",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getComments",
    "outputs": [
      {
        "components": [
          {
            "internalType": "uint32",
            "name": "id",
            "type": "uint32"
          },
          {
            "internalType": "address",
            "name": "creator_address",
            "type": "address"
          },
          {
            "internalType": "string",
            "name": "creator",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "message",
            "type": "string"
          },
          {
            "internalType": "uint256",
            "name": "created_at",
            "type": "uint256"
          }
        ],
        "internalType": "struct Comments.Comment[]",
        "name": "",
        "type": "tuple[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]
```

コントラクトをデプロイした際に`artifacts/contracts/Comments.sol/Comments.json`が作成されているはずです。
（`.gitignore`に記録されているためバージョン管理されません。）

そのJSONの以下の部分をコピーしたものになります。

```
"abi": [

〜省略〜

]
```

#### 4.util/interact.tsの作成

前述のドキュメントをもとに`util/interact.ts`を配置し、実装を進めていきます。

まずは完成例をご確認ください。

```util/interact.ts
import {Comment} from '../types/comment'

const alchemyWssUrl = process.env.REACT_APP_ALCHEMY_WSS_URL
const { createAlchemyWeb3 } = require("@alch/alchemy-web3")
const web3 = createAlchemyWeb3(alchemyWssUrl)

const contractABI = require('../contract-abi.json')
const contractAddress = process.env.REACT_APP_CONTRACT_ADDRESS

export const commentsContract = new web3.eth.Contract(
  contractABI,
  contractAddress
)

export const loadCurrentComments: () => Promise<Comment[]> = async () => {
  const comments: Promise<Comment[]> = await commentsContract.methods.getComments().call()
  return comments
}

export const connectWallet: () => Promise<{ address: string, status: string, isError: boolean }> = async () => {
  if (window.ethereum) {
    try {
      const addressArray = await window.ethereum.request<string[]>({
        method: "eth_requestAccounts",
      })

      if (addressArray && addressArray.length > 0) {
        return {
          address: addressArray[0] ?? "",
          status: "👆🏽 さぁ、メッセージを送りましょう。",
          isError: false,
        }
      } else {
        return {
          address: "",
          status: "アドレスが取得できませんでした。",
          isError: true,
        }
      }

    } catch (err: unknown) {
      return {
        address: "",
        status: "❌ " + (err instanceof Error ? err.message : "Internal Server Error."),
        isError: true,
      }
    }
  } else {
    return {
      address: "",
      status: "🦊 MetaMaskをインストールしてください。",
      isError: true,
    }
  }
}

export const getCurrentWalletConnected: () => Promise<{ address: string, status: string, isError: boolean }> = async () => {
  if (window.ethereum) {
    try {
      const addressArray = await window.ethereum.request<string[]>({
        method: "eth_accounts",
      })

      if (addressArray && addressArray.length > 0) {
        return {
          address: addressArray[0] ?? "",
          status: "👆🏽 さぁ、メッセージを送りましょう。",
          isError: false,
        }
      } else {
        return {
          address: "",
          status: "🦊 MetaMaskの接続設定をおこなってください。",
          isError: true,
        }
      }
    } catch (err: unknown) {
      return {
        address: "",
        status: "❌ " + (err instanceof Error ? err.message : "Internal Server Error."),
        isError: true,
      }
    }
  } else {
    return {
      address: "",
      status: "🦊 MetaMaskをインストールしてください。",
      isError: true,
    }
  }
}

export const addComment: (address: string, creator: string, message: string) => Promise<{ status: string, isError: boolean }> = async (address: string, creator: string, message: string) => {
  if (!window.ethereum) {
    return {
      status: "🦊 MetaMaskをインストールしてください。",
      isError: true,
    }
  }

  if (!address) {
    return {
      status: "🦊 MetaMaskの接続設定をおこなってください。",
      isError: true,
    }
  }

  if (creator.trim() === "") {
    return {
      status: "名前を入力してください。",
      isError: true,
    }
  }

  if (message.trim() === "") {
    return {
      status: "メッセージを入力してください。",
      isError: true,
    }
  }

  const transactionParameters = {
    to: contractAddress,
    from: address,
    data: commentsContract.methods.addComment(creator, message).encodeABI(),
  }

  try {
    const txHash = await window.ethereum.request({
      method: "eth_sendTransaction",
      params: [transactionParameters],
    })

    return {
      status: `メッセージの発行リクエストを送信しました。 TxHash=[${txHash}]`,
      isError: false,
    }
  } catch (err: unknown) {
    return {
      status: "❌ " + (err instanceof Error ? err.message : "Internal Server Error."),
      isError: true,
    }
  }
}
```

少々雑ではありますが、ウォレットの接続と、コメントの取得、コメントの追加をそれぞれAlchemyのAPIを通じて実装していることが読み取れます。

なお、環境変数`REACT_APP_ALCHEMY_WSS_URL`には、AlchemyのAPIキーを表示した際の「WEBSOCKETS」のURLを転記します。
（「HTTP」ではありません。）

また、`REACT_APP_CONTRACT_ADDRESS`には先ほどデプロイしたコントラクトのアドレスを記載します。

別途、React用に`.env`を用意する場合は以下のようになります。
（`dotenv-cli`ライブラリにより環境変数を読み込んでいます。）

```.env
REACT_APP_ALCHEMY_WSS_URL=wss://polygon-mumbai.g.alchemy.com/v2/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
REACT_APP_CONTRACT_ADDRESS=0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

#### 5.Viewの作成

ビューは各自自由にデザインするよいでしょう。
`React Hooks`により各種APIを繋いでる箇所を抜粋します。

```App.tsx
// 省略

function App() {
  const [address, setAddress] = useState<string>("")
  const [creator, setCreator] = useState<string>("")
  const [message, setMessage] = useState<string>("")
  const [status, setStatus] = useState<string>("")
  const [isError, setIsError] = useState<boolean>(false)
  const [isLoading, setIsLoading] = useState<boolean>(false)

  const [comments, setComments] = useState<Comment[]>([])

  useEffect(() => {
    const fetchMessage = async() => {
      setIsLoading(true)
      const comments = await loadCurrentComments()
      setComments(comments)
      setIsLoading(false)
    }
    fetchMessage()
    addSmartContractListener()

    const fetchWallet = async() => {
      const { address, status, isError } = await getCurrentWalletConnected()
      setAddress(address)
      setStatus(status)
      setIsError(isError)
    }
    fetchWallet()
    addWalletListener()
  }, [])

  const addSmartContractListener = () => {
    commentsContract.events.CommentAdded({}, (err: unknown, data: unknown) => {
      if (err) {
        setStatus("❌ " + (err instanceof Error ? err.message : "Internal Server Error."),)
      } else {
        setStatus("🎉 おめでとうございます！あなたのメッセージは正常に発行されました。")
        setMessage("")

        const fetchMessage = async() => {
          setIsLoading(true)
          const comments = await loadCurrentComments()
          setComments(comments)
          setIsLoading(false)
        }
        fetchMessage()
      }
    })
  }

  const addWalletListener = () => {
    if (window.ethereum) {
      window.ethereum.on("accountsChanged", (args: unknown) => {
        const accounts = args as string[]
        if (accounts && accounts.length > 0) {
          setAddress(accounts[0])
          setStatus("👆🏽 さぁ、メッセージを送りましょう。")
          setIsError(false)
        } else {
          setAddress("")
          setStatus("🦊 MetaMaskの接続設定をおこなってください。")
          setIsError(true)
        }
      })
    } else {
      setStatus("🦊 MetaMaskをインストールしてください。")
      setIsError(true)
    }
  }

  const connectWalletPressed = async() => {
    const { address, status, isError } = await connectWallet()
    setAddress(address)
    setStatus(status)
    setIsError(isError)
  }

  const onSubmit = async() => {
    setIsLoading(true)
    const { status, isError } = await addComment(address, creator, message)
    setIsLoading(false)
    setStatus(status)
    setIsError(isError)
  }

// 省略
```

以下の関数に注目してください。

```ts
commentsContract.events.CommentAdded({}, (err: unknown, data: unknown) => {

}
```

この関数は、スマートコントラクトの定義で

```contracts/Comments.sol
event CommentAdded(Comment comment);
```

を定義した部分です。

このイベントをトリガーに以下の完了メッセージを送信するよう実装しました。
もちろん、その他様々な後続処理を実装することができます。

```ts
setStatus("🎉 おめでとうございます！あなたのメッセージは正常に発行されました。")
```

## 参考文献

以下の記事を参考にさせていただきました。ありがとうございます！

https://www.pointer.gg/tutorials/create-a-web3-forum-with-polygon