用意するもの
- 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",
};
スマートコントラクトのファイルを作成
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
テストファイルを作成する
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 でインポート
これで無事テストネットにデプロイされました。
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 を開く
スマートコントラクトのインターフェイスファイルをsiteにコピー
terminal
cp artifacts/contracts/Token.sol/Token.json site/
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 はデプロイごとに変化するので注意
ブラウザリロードで結果を確認
トークンの配布ができるようにする
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>
ローカルネットワークのログに下記が表示される
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>
これでトークンが移動されているのが確認できるようになりました。