web3.jsでローカル環境にデプロイしたコントラクトをNext.js(Typescript)で使ってみる
本記事の対象者
- Web3技術に興味がある
- Next.jsにWeb3技術を導入したい
- 独自通貨を作成してみたい
本記事のリポジトリ
ぜひ参考にしてください!
Next.jsの環境構築
## Next.jsのプロジェクトを作成
$ npx create-next-app . -e with-tailwindcss
## npmモジュールを生成
$ npm i
## ローカルサーバー起動
$ npm run dev
下記画像のように表示されれば完了
本記事では prettier を導入しています。導入したい方は以下のファイルを参考に設定してください!
{
"printWidth": 120,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"jsxBracketSameLine": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}
Truffleの環境構築
Truffle(トリュフ)とは
- Solidityの開発用フレー厶ワーク
- スマートコントラクトのビルド(コンパイル)、テスト、デプロイをサポート
Truffleの導入
# truffleをインストールする(既にインストールされている場合は不要)
$ npm install -g truffle
# truffleのバージョンを確認する
$ truffle
Truffle v5.5.17 - a development framework for Ethereum
Usage: truffle <command> [options]
Commands:
truffle build Execute build pipeline (if configuration present)
truffle compile Compile contract source files
truffle config Set user-level configuration options
truffle console Run a console with contract abstractions and commands
available
...
# 初期化を行う
$ truffle init
下記画像のようにcontracts, migrations, testのディレクトリ、truffle-config.jsファイルが作成されればOK
Ganacheによるローカル環境の構築
Ganache(ガナッシュ)とは
TruffleチームがローンチしたEthereum開発環境。ローカルで開発用のチェーンを構築できて、GUIでブロックやトランザクションを参照することができる
Ganacheのダウンロード
WorkSpaceを作成
下記手順でWorkSpaceを作成して先ほど作成したtruffleと紐付けを行う
-
「NEW WORKSPACE」押下
-
WORKSPACE NAMEに「ZENN TOKEN」と入力する
-
「ADD PROJECT」押下
-
truffle-config.js
を選択する
※truffle init
実行時に作成されるはず
-
「SAVE WORKSPACE」押下
-
下記画面が表示されればOK
スマートコントラクトを作成する
スマートコントラクトで独自通貨(ZennCoin)を作成します!スマートコントラクトで使用する言語はSolidityで拡張子は.solです。
VSCodeを使っているのであれば下記拡張機能を導入することをお勧めします!(ハイライトなど便利)
全体の流れ
① コントラクトを作成
Solidityでスマートコントラクトを実装します。
② build
作成したスマートコントラクトはそのままでは実行できません。buildしてJSONファイルに変換する必要があります。buildはmigrateファイルを元にして行われるためmigrateファイルを作成する必要があります。
③ deploy
buildで作成されたJSONファイルをGanacheの開発用チェーンにdeployする
① コントラクトを作成
- 通貨用のERC20プロトコルを使用するためにopenzeppelinをインストールする
$ npm i @openzeppelin/contracts
- contractファイルを作成
$ truffle create contract ZennCoin
- 下記コードに書き換え
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract ZennCoin is ERC20 {
constructor(uint256 initialSupply) ERC20("ZENC", "ZEC") {
_mint(msg.sender, initialSupply);
}
}
-
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
でERC20プロトコルのインポート -
contract ZENNToken is ERC20
でERC20プロトコルを継承することによってERC20プロトコルに準拠した通貨を作成することができる
② build
- buildに必要なmigrateファイルを作成
$ truffle create migration zennCoin
- 下記コードに書き換え
const zennCoin = artifacts.require('ZennCoin');
module.exports = function (deployer) {
const initSupply = 10000000000;
deployer.deploy(zennCoin, initSupply);
};
- ファイル名の最初の数字は作成時に割り当てられるので異なっていても問題ありません
-
const zennCoin = artifacts.require('ZennCoin');
で作成したスマートコントラクトを紐づけている -
const initSupply = 10000000000;
でコインの供給量を指定することができます
- build
下記のようにCompiled successfullyとなってbuild/contractsにjsonファイルが作成されていればOK
$ truffle build
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/ZennCoin.sol
> Compiling @openzeppelin/contracts/token/ERC20/ERC20.sol
> Compiling @openzeppelin/contracts/token/ERC20/IERC20.sol
> Compiling @openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol
> Compiling @openzeppelin/contracts/utils/Context.sol
> Artifacts written to /Users/sugayutokyo/Developer/workplace/private/next-zenn-web3/build/contracts
> Compiled successfully using:
- solc: 0.8.14+commit.80d49f37.Emscripten.clang
③ deploy
- deploy先を設定する
デプロイ先をGanacheの開発用チェーンに設定するためにtruffle-config.jsを変更します
networksの中のdevelopmentのコメントアウトを外し、hostとportをGanacheのRPC SERVERの値と同じにします。
networks: {
// Useful for testing. The `development` name is special - truffle uses it by default
// if it's defined here and no other network is specified at the command line.
// You should run a client (like ganache, geth, or parity) in a separate terminal
// tab if you use this network and you must also set the `host`, `port` and `network_id`
// options below to some value.
//
- // development: {
- // host: "127.0.0.1", // Localhost (default: none)
- // port: 8545, // Standard Ethereum port (default: none)
- // network_id: "*", // Any network (default: none)
- // },
+ development: {
+ host: "127.0.0.1", // Localhost (default: none)
+ port: 7545, // Standard Ethereum port (default: none)
+ network_id: "*", // Any network (default: none)
+ },
//
// An additional network, but with some advanced options…
// advanced: {
// port: 8777, // Custom port
// network_id: 1342, // Custom network
// gas: 8500000, // Gas sent with each transaction (default: ~6700000)
// gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei)
// from: <address>, // Account to send transactions from (default: accounts[0])
// websocket: true // Enable EventEmitter interface for web3 (default: false)
// },
//
- deploy前のGanacheの開発用チェーンの状態を確認
- アカウント
- トランザクション
- コントラクト
- deploy
$ truffle deploy
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Starting migrations...
======================
> Network name: 'development'
> Network id: 5777
> Block gas limit: 6721975 (0x6691b7)
1_initial_migration.js
======================
Deploying 'Migrations'
----------------------
> transaction hash: 0x8ba26437e4520ad95dfd187efde063c4f7512c0ea0833bdec09d6b685aa05b31
> Blocks: 0 Seconds: 0
> contract address: 0xd18a0D7149717D50316d921B145E1E40A76D70d6
> block number: 1
> block timestamp: 1655124819
> account: 0x7D4d7a0da0e8e1Dc90a86fDB82882a94190d89D6
> balance: 99.99502292
> gas used: 248854 (0x3cc16)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.00497708 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00497708 ETH
1655110571_zenn_coin.js
=======================
Deploying 'ZennCoin'
--------------------
> transaction hash: 0x5c132c121ee05deabe1db9b35da0cf3b3516881f99c4ec9145fb339c94f67d20
> Blocks: 0 Seconds: 0
> contract address: 0x932546ed94E905216e34c417602a93bC0DEc07D5
> block number: 3
> block timestamp: 1655124820
> account: 0x7D4d7a0da0e8e1Dc90a86fDB82882a94190d89D6
> balance: 99.9707734
> gas used: 1169963 (0x11da2b)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.02339926 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.02339926 ETH
Summary
=======
> Total deployments: 2
> Final cost: 0.02837634 ETH
- deploy成功後のGanacheの開発用チェーンの状態を確認
- アカウント
ETHからガス代分減っている。今回のdeployにおけるガス代はtruffle deploy
時のFinal cost: 0.02837634 ETHなので、100ETH - 約0.028 = 99.97ETHになっている。
- トランザクション
トランザクションの履歴が確認できる
- コントラクト
ZennCoinのスマートコントラクトがデプロイされていることが確認できる
Next.jsで実際にスマートコントラクトを使ってみる
スマートコントラクトをNext.jsで使用する流れ
Application Binary Interface(ABI)はコントラクトに接続するインターフェースです。Solidityファイルをbuildして作成されるJSONファイル内にABIが存在し、ABIを経由してコントラクトを操作することができます。本記事ではNext.jsからABIを経由してコントラクトを操作します。
準備
web3.jsをインストール
$ npm i web3
TypeChainを使って型ファイルの生成
ABIはSolidityファイルをbuildして作成されるJSONファイルに存在するため、Solidityファイルを変更するたびに型情報が変更になる可能性があります。ABIの型情報はJSONファイルのABIから生成してあげる必要があり、typechainというライブラリによって実現できます。
- ABIの型生成のために下記ライブラリをインストール
$ npm install -D typechain @typechain/web3-v1
$ npm install web3-utils
- typechainを実行するためにscriptsに下記内容を追加
{
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
- "start": "next start"
+ "start": "next start",
+ "typechain": "typechain --target web3-v1 --out-dir types/abi './build/contracts/**/*.json'"
},
...
- typechainを実行し、下記画像のようにtypes/abi配下にtypeのファイルが作成されることを確認
$ npm run typechain
- index.jsを下記コードに書き換えて実際に型付けがうまくいくかどうか確認
import type { NextPage } from 'next';
import Web3 from 'web3';
import { AbiItem } from 'web3-utils';
import ZCContract from '../build/contracts/ZennCoin.json';
import { ZennCoin } from '../types/abi/ZennCoin';
// プロバイダの設定
const web3 = new Web3(new Web3.providers.HttpProvider(`http://127.0.0.1:7545`));
// コントラクトのアドレス
const address = '0x8F4D574EFe77e00af32C54d2A0D07F7C53cb56bF';
const ABI = ZCContract.abi as any as AbiItem;
// コントラクトのインスタンス
const contract = new web3.eth.Contract(ABI, address) as unknown as ZennCoin;
const Home: NextPage = () => {
return <div></div>;
};
export default Home;
- buildで生成されたJSONファイルをインポートする
import ZCContract from '../build/contracts/ZennCoin.json';
- インポートしたJSONファイルからabiを抽出
const ABI = ZCContract.abi as any as AbiItem;
- 以下のコードはhttp://127.0.0.1:7545を指定することでGanacheの開発用チェーンを指定しています。テストネットやメインネットを使用したい時にはhttp://127.0.0.1:7545の部分をそれぞれ変更すればOK
const web3 = new Web3(new Web3.providers.HttpProvider(`http://127.0.0.1:7545`));
- コントラクトのアドレスは以下コードで指定している
const address = '0x8F4D574EFe77e00af32C54d2A0D07F7C53cb56bF';
コントラクトのアドレスはGanacheのCONTRACTSタブのZennCoinのADDRESSをコピーする
- 以下コードでコントラクトのインスタンスを生成している。コントラクトに関する操作はこのインスタンスを通して行う
const contract = new web3.eth.Contract(ABI, address) as unknown as ZennCoin;
- 下記のようにcontractで使用できるメソッドが予測候補に出てきたら成功
Next.jsからABIを経由してコントラクトを操作
まずはアカウント情報を取得してみる
- pages/index.tsxを下記のように修正
...
const Home: NextPage = () => {
+ (async () => {
+ const accountsWeb3 = await web3.eth.getAccounts();
+ console.log(accountsWeb3);
+ })();
return <div></div>;
};
...
- chromeでlocalhostにアクセスして下記のようにコンソールにアカウントのADDRESSが表示されることが確認できればOK
表示されているADDRESSがGanacheのものと同じであることを確認する
ETHとZennCoinの残高を取得する
- pages/index.tsxを下記のように修正
今回は2つのアカウント内でZennCoinを移動させるため、2つのアカウント(UserAとUserB)のETHとZennCoinの残高を取得します。
本記事ではWeb3技術の共有を主としているため、Reactについてのコードの解説は行いません。
import type { NextPage } from 'next';
+ import { useState } from 'react';
import Web3 from 'web3';
import { AbiItem } from 'web3-utils';
import ZCContract from '../build/contracts/ZennCoin.json';
import { ZennCoin } from '../types/abi/ZennCoin';
// プロバイダの設定
const web3 = new Web3(new Web3.providers.HttpProvider(`http://127.0.0.1:7545`));
// コントラクトのアドレス
const address = '0x8F4D574EFe77e00af32C54d2A0D07F7C53cb56bF';
const ABI = ZCContract.abi as any as AbiItem;
// コントラクトのインスタンス
const contract = new web3.eth.Contract(ABI, address) as unknown as ZennCoin;
+ const walletAddressUserA = '0x7D4d7a0da0e8e1Dc90a86fDB82882a94190d89D6';
+ const walletAddressUserB = '0x95a1D1A9fA7280E8A98c288a7bFD69EFdEFcD390';
const Home: NextPage = () => {
- (async () => {
- const accountsWeb3 = await web3.eth.getAccounts();
- console.log(accountsWeb3);
- })();
- return <div></div>;
+ const [balanceZcUserA, setBalanceZcUserA] = useState(''); // ZennCoin残高 UserA
+ const [balanceEthUserA, setBalanceEthUserA] = useState(''); // ETH残高 UserA
+ const [balanceZcUserB, setBalanceZcUserB] = useState(''); // ZennCoin残高 UserB
+ const [balanceEthUserB, setBalanceEthUserB] = useState(''); // ETH残高 UserB
+ const getBalance = async (userType: string) => {
+ if (userType === 'a') {
+ setBalanceZcUserA(await contract.methods.balanceOf(walletAddressUserA).call());
+ setBalanceEthUserA(await web3.eth.getBalance(walletAddressUserA));
+ } else if (userType === 'b') {
+ setBalanceZcUserB(await contract.methods.balanceOf(walletAddressUserB).call());
+ setBalanceEthUserB(await web3.eth.getBalance(walletAddressUserB));
+ }
+ };
+
+ return (
+ <div className="m-5">
+ <h2>UserA Info</h2>
+ {balanceZcUserA ? (
+ <table className="table-auto">
+ <thead>
+ <tr>
+ <th className="px-4 py-2">ZennCoin Balance</th>
+ <th className="px-4 py-2">ETH Balance</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td className="border px-4 py-2">{balanceZcUserA}</td>
+ <td className="border px-4 py-2">{balanceEthUserA}</td>
+ </tr>
+ </tbody>
+ </table>
+ ) : (
+ <div>「UserA 残高を取得」を押してください</div>
+ )}
+ <h2>UserB Info</h2>
+ {balanceZcUserB ? (
+ <table className="table-auto">
+ <thead>
+ <tr>
+ <th className="px-4 py-2">ZennCoin Balance</th>
+ <th className="px-4 py-2">ETH Balance</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td className="border px-4 py-2">{balanceZcUserB}</td>
+ <td className="border px-4 py-2">{balanceEthUserB}</td>
+ </tr>
+ </tbody>
+ </table>
+ ) : (
+ <div>「UserB 残高を取得」を押してください</div>
+ )}
+ <button
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
+ onClick={() => getBalance('a')}>
+ UserA 残高を取得
+ </button>
+ <button
+ className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded ml-5"
+ onClick={() => getBalance('b')}>
+ UserB 残高を取得
+ </button>
+ </div>
);
};
export default Home;
- 下記コードでwalletAddressを2つ指定している(GanacheのACCOUNTSタブの中のINDEX 0とINDEX 1のものを本記事では使用)
const walletAddressUserA = '0x7D4d7a0da0e8e1Dc90a86fDB82882a94190d89D6';
const walletAddressUserB = '0x95a1D1A9fA7280E8A98c288a7bFD69EFdEFcD390';
※2回デプロイしたのでETHの残高が少なくなっています。分かりづらくて申し訳ありません。
本記事を順番に進めているのであれば99.97と表示されるはずです!
- contractインスタンスから指定したAddressのZennCoinの残高を取得する
contract.methods.balanceOf(walletAddressUserA).call()
- web3インスタンスから指定したAddressのETHの残高を取得する
web3.eth.getBalance(walletAddressUserA)
- 動作確認する
「UserA 残高を取得」、「UserB 残高を取得」を謳歌することで下記GIFのように残高を取得することができたらOK
ZennCoinを移動させる
- pages/index.tsxを下記のように修正
const ABI = ZCContract.abi as any as AbiItem;
// コントラクトのインスタンス
const contract = new web3.eth.Contract(ABI, address) as unknown as ZennCoin;
const walletAddressUserA = '0x7D4d7a0da0e8e1Dc90a86fDB82882a94190d89D6';
const walletAddressUserB = '0x95a1D1A9fA7280E8A98c288a7bFD69EFdEFcD390';
+ const contractFromA = new web3.eth.Contract(ABI, address, { from: walletAddressUserA }) as unknown as ZennCoin;
const Home: NextPage = () => {
...
const getBalance = async (userType: string) => {
if (userType === 'a') {
setBalanceZcUserA(await contract.methods.balanceOf(walletAddressUserA).call());
setBalanceEthUserA(await web3.eth.getBalance(walletAddressUserA));
} else if (userType === 'b') {
setBalanceZcUserB(await contract.methods.balanceOf(walletAddressUserB).call());
setBalanceEthUserB(await web3.eth.getBalance(walletAddressUserB));
}
};
+ const transferZennCoin = async () => {
+ await contractFromA.methods.transfer(walletAddressUserB, 1000).send();
+ };
return (
<div className="m-5">
<h2>UserA Info</h2>
...
<h2>UserB Info</h2>
...
<button
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded ml-5"
onClick={() => getBalance('b')}>
UserB 残高を取得
</button>
+ <button
+ className="bg-orange-500 hover:bg-orange-700 text-white font-bold py-2 px-4 rounded ml-5"
+ onClick={transferZennCoin}>
+ Transfer ZennCoin From A To B
+ </button>
</div>
);
};
- 通貨を移動させるときは下記のようにfromを指定してcontractインスタンスを作成する必要がある
const contractFromA = new web3.eth.Contract(ABI, address, { from: walletAddressUserA }) as unknown as ZennCoin;
- transferメソッドの第1引数には送る相手、第2引数には移動する通貨量を指定する
contractFromA.methods.transfer(walletAddressUserB, 1000).send()
-
移動前の残高を確認する
-
「Transfer ZennCoin From A To B」を押下後「UserA 残高を取得」「UserB 残高を取得」をそれぞれ押下して下記画像のようにZennCoinが1000移動していればOK
エラーハンドリング
- Error: Invalid JSON RPC response: ""
原因: Ganacheを起動していないと起きてしまうエラー
解決策: Ganacheを起動することで解決
最後に
今回はスマートコントラクトを作成し、独自通貨の取得、移動を行いました。私が所属している株式会社UPBONDではWeb3技術を使った開発を行っています。今後もWeb3技術で開発していく中で手に入れた知見を記事にしていきます。
記事の中で間違っている箇所がありましたらコメントをいただけたら幸いです。
参考
Discussion