hardhatを使ってトークンをデプロイしてWebサイトで利用するまでの一部始終

## 用意するもの

- PC(windows or mac or linux)
- VSCode + Remote Development拡張機能
- Docker
- ブラウザ(chrome等) + MetaMask

## VSCode を起動し開発用のフォルダを開く

/dev/hardhat

## Dockerfile を作成

```dockerfile:Dockerfile
FROM node:lts
```

## Reopen in container でDockerfileを指定してコンテナに入る


## hardhatプロジェクトを作成

```shell:terminal
# npx hardhat

  hardhat@2.12.1
Ok to proceed? (y) 
```

Enterで進める

```
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.12.1

? What do you want to do? … 
  Create a JavaScript project
  Create a TypeScript project
▸ Create an empty hardhat.config.js
  Quit
```
空の設定ファイルを作成を選択

```
✔ What do you want to do? · Create an empty hardhat.config.js
Config file created

You need to install hardhat locally to use it. Please run:

npm install --save-dev "hardhat@^2.12.1"


Give Hardhat a star on Github if you're enjoying it!

     https://github.com/NomicFoundation/hardhat
```

下記の2ファイルが作成された

```js:hardhat.config.js
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.17",
};
```

```json:package.json
{
  "name": "hardhat-project"
}
```

# hardhat パッケージを追加

```shell:terminal
# npm install --save-dev hardhat

added 300 packages, and audited 301 packages in 36s

61 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
```

```diff_json:package.json
{
  "name": "hardhat-project",
+ "devDependencies": {
+   "hardhat": "^2.12.1"
+ }
}
```

# 実行できるコマンドの確認

```shell:terminal
# npx hardhat

Hardhat version 2.12.1

Usage: hardhat [GLOBAL OPTIONS] <TASK> [TASK OPTIONS]

GLOBAL OPTIONS:

  --config              A Hardhat config file. 
  --emoji               Use emoji in messages. 
  --flamegraph          Generate a flamegraph of your Hardhat tasks 
  --help                Shows this message, or a task's help if its name is provided 
  --max-memory          The maximum amount of memory that Hardhat can use. 
  --network             The network to connect to. 
  --show-stack-traces   Show stack traces (always enabled on CI servers). 
  --tsconfig            A TypeScript config file. 
  --typecheck           Enable TypeScript type-checking of your scripts/tests 
  --verbose             Enables Hardhat verbose logging 
  --version             Shows hardhat's version. 


AVAILABLE TASKS:

  check         Check whatever you need
  clean         Clears the cache and deletes all artifacts
  compile       Compiles the entire project, building all artifacts
  console       Opens a hardhat console
  flatten       Flattens and prints contracts and their dependencies
  help          Prints this message
  node          Starts a JSON-RPC server on top of Hardhat Network
  run           Runs a user-defined script after compiling the project
  test          Runs mocha tests

To get help for a specific task run: npx hardhat help [task]
```

# 開発に役立つプラグインを追加

```shell:terminal
# npm install --save-dev @nomicfoundation/hardhat-toolbox

npm WARN deprecated har-validator@5.1.5: this library is no longer supported
npm WARN deprecated request-promise-native@1.0.9: request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142
npm WARN deprecated debug@3.2.6: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)
npm WARN deprecated uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
npm WARN deprecated debug@3.2.6: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)
npm WARN deprecated uuid@2.0.1: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.

added 395 packages, and audited 696 packages in 40s

113 packages are looking for funding
  run `npm fund` for details

6 high severity vulnerabilities

To address all issues, run:
  npm audit fix

Run `npm audit` for details.
```

いろいろ警告が出るが今回はこのまま進める

```diff_json:package.json
{
  "name": "hardhat-project",
  "devDependencies": {
+   "@nomicfoundation/hardhat-toolbox": "^2.0.0",
    "hardhat": "^2.12.1"
  }
}
```

## プラグインを読み込む設定を追加

```diff_javascript:hardhat.config.js
+ require("@nomicfoundation/hardhat-toolbox");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.17",
};
```


## スマートコントラクトのファイルを作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/3e5b2db0-1d8c-ef48-2547-d3a73a56e772.png)

```solidity:contracts/Token.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

contract Token {
    string public name = "My Hardhat Token";
    string public symbol = "MHT";

    uint256 public totalSupply = 10000;

    address public owner;

    mapping(address => uint256) balances;

    event Transfer(address indexed _from, address indexed _to, uint256 _value);

    constructor() {
        balances[msg.sender] = totalSupply;
        owner = msg.sender;
    }

    function transfer(address to, uint256 amount) external {
        require(balances[msg.sender] >= amount, "Not enough tokens");

        balances[msg.sender] -= amount;
        balances[to] += amount;

        emit Transfer(msg.sender, to, amount);
    }

    function balanceOf(address account) external view returns (uint256) {
        return balances[account];
    }
}
```

## スマートコントラクトをコンパイルする

```shell:terminal
# npx hardhat compile

Compiled 1 Solidity file successfully
```

## テストファイルを作成する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/3ff0ee10-6a96-780a-b8a2-c48e86982046.png)

```js:test/Token.js
const { expect } = require("chai");

describe("Token contract", function () {
  it("Deployment should assign the total supply of tokens to the owner", async function () {
    const [owner] = await ethers.getSigners();
    const Token = await ethers.getContractFactory("Token");
    const token = await Token.deploy();
    const balance = await token.balanceOf(owner.address);

    expect(await token.totalSupply()).to.equal(balance);
  });
});
```

## テストを実行する

```shell:terminal
# npx hardhat test

  Token contract
    ✔ Deployment should assign the total supply of tokens to the owner (7137ms)

  1 passing (7s)
```

## テストを追加する

```diff_javascript:test/Token.js
const { expect } = require("chai");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");

describe("Token contract", function () {
    async function deploy() {
        const [owner, other1, other2] = await ethers.getSigners();
        const Token = await ethers.getContractFactory("Token");
        const token = await Token.deploy();
        return { token, owner, other1, other2 }
    }

    it("Right name", async function () {
        const { token } = await loadFixture(deploy);

        expect(await token.name()).to.equal("My Hardhat Token");
    });

    it("Right symbol", async function () {
        const { token } = await loadFixture(deploy);

        expect(await token.symbol()).to.equal("MHT");
    });

    it("Right total supply", async function () {
        const { token } = await loadFixture(deploy);

        expect(await token.totalSupply()).to.equal(10000);
    });

    it("Right owner address", async function () {
        const { token, owner } = await loadFixture(deploy);

        expect(await token.owner()).to.equal(owner.address);
    });

    it("Deployment should assign the total supply of tokens to the owner", async function () {
        const { token, owner } = await loadFixture(deploy);
        const balance = await token.balanceOf(owner.address);

        expect(await token.totalSupply()).to.equal(balance);
    });

    it("Transfer 50 tokens from owner to other1", async function () {
        const { token, owner, other1 } = await loadFixture(deploy);

        await expect(token.transfer(other1.address, 50)).to.changeTokenBalances(token, [owner, other1], [-50, 50]);
    });

    it("Transfer 50 tokens from owner to other1 to other2", async function () {
        const { token, owner, other1, other2 } = await loadFixture(deploy);

        await expect(token.transfer(other1.address, 50)).to.changeTokenBalances(token, [owner, other1], [-50, 50]);
        await expect(token.connect(other1).transfer(other2.address, 50)).to.changeTokenBalances(token, [other1, other2], [-50, 50]);
    });

    it("should emit Transfer events", async function () {
        const { token, owner, other1, other2 } = await loadFixture(deploy);

        await expect(token.transfer(other1.address, 50)).to.emit(token, "Transfer").withArgs(owner.address, other1.address, 50);
        await expect(token.connect(other1).transfer(other2.address, 50)).to.emit(token, "Transfer").withArgs(other1.address, other2.address, 50);
    });

    it("Should fail if sender doesn't have enough tokens", async function () {
        const { token, owner, other1 } = await loadFixture(deploy);
        const initialOwnerBalance = await token.balanceOf(owner.address);

        await expect(token.connect(other1).transfer(owner.address, 1)).to.be.revertedWith("Not enough tokens");
        expect(await token.balanceOf(owner.address)).to.equal(initialOwnerBalance);
    });
});
```

## 追加したテストを実行する

```shell:terminal
# npx hardhat test

Token contract
    ✔ Right name (7440ms)
    ✔ Right symbol
    ✔ Right total supply
    ✔ Right owner address
    ✔ Deployment should assign the total supply of tokens to the owner
    ✔ Transfer 50 tokens from owner to other1 (64ms)
    ✔ Transfer 50 tokens from owner to other1 to other2 (64ms)
    ✔ should emit Transfer events
    ✔ Should fail if sender doesn't have enough tokens (57ms)


  9 passing (8s)
```

## デプロイ用のファイルを作成

```js:scripts/deploy.js
async function main() {
  const [deployer] = await ethers.getSigners();

  console.log("Deploying contracts with the account:", deployer.address);

  console.log("Account balance:", (await deployer.getBalance()).toString());

  const Token = await ethers.getContractFactory("Token");
  const token = await Token.deploy();

  console.log("Token address:", token.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
```

## ローカルでデプロイをしてみる

```shell:terminal
# npx hardhat run scripts/deploy.js

Deploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Account balance: 10000000000000000000000
Token address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
```

## goerli(ゴエリ)テストネットにデプロイしてみる

### dotenv パッケージを追加

```shell:terminal
# npm install --save-dev dotenv

added 1 package, and audited 697 packages in 4s

113 packages are looking for funding
  run `npm fund` for details

6 high severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
```

```diff_json:package.json
{
  "name": "hardhat-project",
  "devDependencies": {
   "@nomicfoundation/hardhat-toolbox": "^2.0.0",
+    "dotenv": "^16.0.3",
    "hardhat": "^2.12.1"
  }
}
```

### hardhat.config.js にネットワーク設定を追加

```diff_javascript:hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
+ require("dotenv").config();
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.17",
+  networks: {
+    goerli: {
+      url: `https://eth-goerli.alchemyapi.io/v2/${process.env.ALCHEMY_API_KEY}`,
+      accounts: [process.env.GOERLI_PRIVATE_KEY]
+    }
+  }
};
```

### .env にAPIキーとプライベートキーを記載（記載内容が流出しないよう注意する）

APIキーは https://www.alchemyapi.io/ にサインアップして取得する
プライベートキーは MetaMask を Goerliに接続して秘密鍵をエクスポートで取得する
デプロイ用の GoerliETH は https://faucets.chain.link/ 等から入手可能

```:.env
ALCHEMY_API_KEY=********************
GOERLI_PRIVATE_KEY=********************
```

### デプロイを実行

```shell:terminal
# npx hardhat run scripts/deploy.js --network goerli

Deploying contracts with the account: 0x**************************
Account balance: 100000000000000000
Token address: 0x**************************
```

### デプロイされた Token address を MetaMask でインポート

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/c891ac77-b7c7-48ff-5acf-ae51c7470c2d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/96835928-46b0-fd2f-ddc8-5e3e6293ff36.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/b3301667-1bb4-57d3-4aff-49ff29703815.png)

これで無事テストネットにデプロイされました。

# Webサイトからスマートコントラクトを使用するためのフロントエンドを作成する

## フロントエンド用のファイルを作成する

```html:site/index.html
frontend
```

## Webサーバを起動してブラウザで表示を確認する

```shell:terminal
# npx http-server site

Need to install the following packages:
  http-server@14.1.1
Ok to proceed? (y) 
Starting up http-server, serving site

http-server version: 14.1.1

http-server settings: 
CORS: disabled
Cache: 3600 seconds
Connection Timeout: 120 seconds
Directory Listings: visible
AutoIndex: visible
Serve GZIP Files: false
Serve Brotli Files: false
Default File Extension: none

Available on:
  http://127.0.0.1:8080
  http://172.17.0.2:8080
Hit CTRL-C to stop the server
```

ブラウザで http://127.0.0.1:8080 を開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/f22abe52-d8fc-dba3-de68-02d0a5de4168.png)

## スマートコントラクトのインターフェイスファイルをsiteにコピー

```shell:terminal
cp artifacts/contracts/Token.sol/Token.json site/
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/8b60cd8b-ac42-67a5-7268-44e54a80bbb7.png)

## index.html をスマートコントラクトを呼び出す処理に変更

```html:site/index.html
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js"></script>
<script>
    (async function () {
        const address = "(デプロイしたスマートコントラクトのアドレス)";
        const { abi } = await (await fetch("Token.json")).json();
        const provider = new ethers.providers.JsonRpcProvider();
        const contract = new ethers.Contract(address, abi, provider);
        document.querySelector("#totalSupply").textContent = await contract.functions.totalSupply();
    })();
</script>
Total supply: <span id="totalSupply">loading...</span>
```

## デプロイしたスマートコントラクトのアドレスを得るためにローカルのブロックチェーンネットワークを起動して Token.sol をデプロイする

```shell:terminal
# npx hardhat node
```

別ターミナルを起動して

```shell:terminal
# npx hardhat run scripts/deploy.js --network localhost

Deploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Account balance: 9999998836412500000000
Token address: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
```

site/index.html の address 変数に Token address の値をセットする

```diff_html:site/index.html
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js"></script>
<script>
    (async function () {
-        const address = "(デプロイしたスマートコントラクトのアドレス)";
+        const address = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512";
        const { abi } = await (await fetch("Token.json")).json();
        const provider = new ethers.providers.JsonRpcProvider();
        const contract = new ethers.Contract(address, abi, provider);
        document.querySelector("#totalSupply").textContent = await contract.functions.totalSupply();
    })();
</script>
Total supply: <span id="totalSupply">loading...</span>
```

※0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 はデプロイごとに変化するので注意

ブラウザリロードで結果を確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/7313dcdb-a39e-9395-b7ca-e12a083cc4e9.png)

## トークンの配布ができるようにする

index.html に数量と配布先アドレス入力欄、配布ボタンを設置

```diff_html:site/index.html
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js"></script>
<script>
    (async function () {
+        const $ = (s) => document.querySelector(s);
        const address = "(デプロイしたスマートコントラクトのアドレス)";
        const { abi } = await (await fetch("Token.json")).json();
        const provider = new ethers.providers.JsonRpcProvider();
        const contract = new ethers.Contract(address, abi, provider);
-         document.querySelector("#totalSupply").textContent = await contract.functions.totalSupply();
+         $("#totalSupply").textContent = await contract.functions.totalSupply();
+         $("form").addEventListener("submit", async e => {
+             e.preventDefault();
+             await contract.connect(provider.getSigner()).functions.transfer($("#to").value, $("#amount").value);
+         });
    })();
</script>
Total supply: <span id="totalSupply">loading...</span>
+
+ <form>
+     <label>配布先<input type="text" id="to" required /></label>
+     <label>配布額<input type="number" id="amount" value="1" required /></label>
+     <button>配布</button>
+ </form>
```

ブラウザをリロードして配布先アドレスを入力して配布してみる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/33d392c8-278a-1746-ea56-c314f4b75362.png)

ローカルネットワークのログに下記が表示される

```
  Contract call:       Token#transfer
  Transaction:         0xbec4c93fbe53293408a5644b75f4f795796c69bb02161eb75346e4d463809256
  From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
  To:                  0xe7f1725e7734ce288f8367e1bb143e90bb3f0512
  Value:               0 ETH
  Gas used:            35070 of 35070
  Block #5:            0x1b1e91ce4e726ee43ece76ddaa34b4e57228c61817f0f50614ba24a3a185e344

eth_chainId
eth_getTransactionByHash
eth_blockNumber
eth_chainId
```

## 残高が表示されるように処理を追加

```html:site/index.html
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js"></script>
<script>
    (async function () {
        const $ = (s) => document.querySelector(s);
        const address = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512";
        const { abi } = await (await fetch("Token.json")).json();
        const provider = new ethers.providers.JsonRpcProvider();
        const contract = new ethers.Contract(address, abi, provider);
        const updateBalance = async () => {
            const [ownerAddress] = await contract.functions.owner();
            $("#ownerBalance").textContent = await contract.functions.balanceOf(ownerAddress);
            $("#balance").textContent = $("#to").value ? await contract.functions.balanceOf($("#to").value) : "";
        };
        $("#totalSupply").textContent = await contract.functions.totalSupply();
        $("#to").addEventListener("change", updateBalance);
        $("form").addEventListener("submit", async e => {
            e.preventDefault();
            await contract.connect(provider.getSigner()).functions.transfer($("#to").value, $("#amount").value);
            updateBalance();
        });
        updateBalance();
    })();
</script>
Total supply: <span id="totalSupply">loading...</span>
Owner balance: <span id="ownerBalance">loading...</span>

<form>
    <label>配布先<input type="text" id="to" required /></label>
    (残高: <span id="balance"></span>)
    <label>配布額<input type="number" id="amount" value="1" required /></label>
    <button>配布</button>
</form>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/e4481b8b-3b74-6eb1-78a3-1aa4e1e698c1.png)

これでトークンが移動されているのが確認できるようになりました。