LoginSignup
1
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-10-29

用意するもの

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

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

/dev/hardhat

Dockerfile を作成

Dockerfile
FROM node:lts

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

hardhatプロジェクトを作成

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ファイルが作成された

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

hardhat パッケージを追加

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
package.json
{
  "name": "hardhat-project",
+ "devDependencies": {
+   "hardhat": "^2.12.1"
+ }
}

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

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]

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

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.

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

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

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

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

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

image.png

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];
    }
}

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

terminal
# npx hardhat compile

Compiled 1 Solidity file successfully

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

image.png

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);
  });
});

テストを実行する

terminal
# npx hardhat test

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

  1 passing (7s)

テストを追加する

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);
    });
});

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

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)

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

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);
  });

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

terminal
# npx hardhat run scripts/deploy.js

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

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

dotenv パッケージを追加

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.
package.json
{
  "name": "hardhat-project",
  "devDependencies": {
   "@nomicfoundation/hardhat-toolbox": "^2.0.0",
+    "dotenv": "^16.0.3",
    "hardhat": "^2.12.1"
  }
}

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

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=********************

デプロイを実行

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

image.png

image.png

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

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

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

site/index.html
frontend

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

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

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

terminal
cp artifacts/contracts/Token.sol/Token.json site/

image.png

index.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 をデプロイする

terminal
# npx hardhat node

別ターミナルを起動して

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 の値をセットする

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

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

index.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

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

  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

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

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

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1