Truffleの学習
スマートコントラクト開発学習のためにTruffleを学ぶ。
関連ツールが複数提供されていて良い感じ。
一つづつ試していく。
Truffle
まずはメインのTruffleから。
Node.jsのインストール
Node.jsが必要なようなのでVoltaで用意したNode.js v16.15.0
を使用することにする。
ついでにyarnを使用したいのでyarn v1.22.18
も併せて用意。
Truffle導入
プロジェクトルートを作成してyarn init -y
でpackage.json
作成。
そしてyarn add -D truffle
でtruffleパッケージをインストールする。
ちなみにグローバルインストールでも良かったが、ローカル環境を汚したくないのでプロジェクト内でインストールする。
.env
もしテストネットにデプロイする場合などにAPIキーなどが要るのでdotenv
を導入して.env
も作成。
(Ganacheでローカルネットを構築できるようなので必要な場合のみで良い)
yarn add -D dotenv
でインストール。
touch .env
でファイル作成。
.gitignore
node_modules
をコミットしたくないのでtouch .gitignore
で.gitignore
を作っておく。
中身。
node_modules
.env
build
package.jsonにscripts追加
よく使うコマンドを書いておく。
"scripts": {
"truffle": "truffle",
"build": "truffle compile",
"migrate": "truffle migrate",
"console": "truffle console"
},
truffleの初期化
yarn truffle init
実行。
- contracts/
- migrations/
- test/
- truffle-config.js
上記が作成される。
準備完了
ここまでで下のようなディレクトリ構成になった。
.
├── .env
├── .gitignore
├── contracts
│ └── .gitkeep
├── migrations
│ └── .gitkeep
├── package.json
├── test
│ └── .gitkeep
├── truffle-config.js
└── yarn.lock
これで準備完了。
Linterを入れたい
コントラクトを書く前にLinter系を入れたくなったので対応する。
Solhint
調べるとsolhint
がよく出てくるので採用。
yarn add -D solhint
でインストール。
"solhint": "^3.3.7"
package.jsonのscriptsにも追加しておく。
"lint": "solhint **/*.sol",
設定ファイルがない場合は初期化が必要らしい。
yarn lint --init
を実行すると.solhint.json
が作成された。
中身は以下に書き換える。
とりあえずrecommendedの設定で進める。
ルール一覧
{
"extends": "solhint:recommended",
"plugins": [],
"rules": {}
}
試しにtouch contracts/Test.sol
でsolファイルを作成し、中身を以下にしてlintを実行する。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Test {
string public name = "str";
function readName() public view returns (string memory) {
return name;
}
}
実行結果
$ solhint **/*.sol contracts/Test.sol
contracts/Test.sol
2:1 error Compiler version ^0.8.0 does not satisfy the ^0.5.8 semver requirement compiler-version
✖ 1 problem (1 error, 0 warnings)
エラーは出たがちゃんと動いている。
ちょっと直し方がすぐに分からなかったのでcompiler-version
をoff
にした(本当はダメ)
{
"extends": "solhint:recommended",
"plugins": [],
"rules": {
"compiler-version": "off"
}
}
直し方は今度調べる。
もう一回lint実行。
$ solhint **/*.sol contracts/Test.sol
✨ Done in 0.82s.
いけた。
Prettier
linterを入れるならprettierも入れたい。
これが良さそうなので導入。
yarn add -D prettier prettier-plugin-solidity
でインストール。
package.jsonにscripts追加。
"prettier": "prettier --write \"**/*.+(js|sol)\""
touch .prettierrc
で設定ファイルを作成。
中身は適当に設定。
jsファイルも扱うことになるのでjsのフォーマット設定も入れておく。
{
"printWidth": 80,
"tabWidth": 2,
"overrides": [
{
"files": "*.sol",
"options": {
"useTabs": false,
"singleQuote": false,
"bracketSpacing": false,
"explicitTypes": "always"
}
},
{
"files": "*.js",
"options": {
"singleQuote": true,
"semi": false,
"jsxSingleQuote": true,
"arrowParens": "always",
"trailingComma": "all",
"htmlWhitespaceSensitivity": "ignore"
}
}
]
}
除外設定もしておく。
touch .prettierignore
を実行。
中身を書く。
node_modules
build
これでprettierの対象から除外するファイルを設定できた。
そしたらyarn prettier
で実行。
$ prettier --write "**/*.+(js|sol)"
contracts/Test.sol 103ms
truffle-config.js 58ms
✨ Done in 0.57s.
フォーマットに違反してないので修正は入らないが、ちゃんと.sol
と.js
がフォーマット対象になっているので問題なさそう。
contracts/Test.sol
はもう必要ないので削除。
次からはコントラクト書いていく。
コントラクトを書いていく
contracts/Counter.sol
を作成。
今回はシンプルにカウントアップ機能を持つCounterを作成する。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// カウンターを作成
contract Counter {
uint256 public count = 0; // カウンターの値
// 値を取得
function read() public view returns (uint256) {
return count;
}
// 値を1増やす
function increment() public {
count += 1;
}
// 受け取った引数を値から加算して代入
function add(uint256 value) public {
count += value;
}
// 受け取った引数を値から減算して代入
function _subtract(uint256 value) private {
count -= value;
}
}
コントラクトをコンパイルする
yarn build
でSolidityをコンパイルする。
コンパイル結果としてbuild/contracts/Counter.json
が出力される。
このjsonファイルは外部からコントラクトを叩くための情報が書かれたファイル。
abi
の値に含まれている名前を呼び出すことができるが、実際にファイルを見てみるとpublicな変数と関数のみが記載されていて、private関数の_subtract
は記載されていないことがわかる。
コントラクトをテストする
コントラクトは一度デプロイすると変更ができない。
なのでローカルでしっかりとテストする必要があるのでtest
ディレクトリ以下にファイルを作成し、Counter.sol
が期待通り動作するかテストする。
テストファイルは.js
と.sol
の拡張子を使用できるが、フロントエンドからコントラクトを叩くことが多いので.js
で記述する。逆にライブラリとして他の.sol
ファイルにインポートしている.sol
を単体テストしたい場合はテストファイルを.sol
で記述するとより実際のユースケースに近いテストができると思われる。
truffleの提供するテストツールを確認
truffleはテストツールを提供してくれている。
公式docsによると、jsの場合はmocha
とchai
を使用しているらしい。
独自にcontract()
を提供しており、実行時にEthereumローカルネットをクリーンアップしてくれるとのこと。さらにcontract()
の第二引数に渡すコールバック関数の引数からaccounts
リストを取得できるとのこと。
test/Counter.test.js
を作成。
const Counter = artifacts.require('Counter')
contract('Counter', (accounts) => {
it('initial count of 0', async () => {
const counterInstance = await Counter.deployed()
const initialCount = await counterInstance.read()
assert.equal(initialCount, 0, 'Count of 0')
})
it('The result of incrementing is 1.', async () => {
const counterInstance = await Counter.deployed()
await counterInstance.increment()
const count = await counterInstance.read()
assert.equal(count, 1, 'Count of 1')
})
it('Adding up results in 100.', async () => {
const counterInstance = await Counter.deployed()
await counterInstance.add(99) // 一つ前のテストで1がcountされているので100にするために99を指定
const count = await counterInstance.read()
assert.equal(count.toString(), 100, 'Count of 100')
})
})
yarn truffle test
でテストを開始できるが、一つ大事なことを忘れていたので先にそれをやる。
Ganacheでローカルネットを構築
ブロックチェーン上のプログラムを書いているので開発用のネットワークがないとお話にならない。
truffleから提供されているGanache
(ガナッシュ)を使用する。
GanacheはGUIとCLIを提供しているが、今回はGUIを選択、インストール。
開くと最初にこんな画面が出る(執筆時点ではもう触っているのでプロジェクトが表示されている)
QUICKSTARTを押す。
すると以下のような画面になった。
Ethereumローカルネットワークが起動、アドレスが10個生成されてそれぞれに100ETHずつ入っている。
このローカルネットに接続するにはtruffle-config.js
にネットワークの情報を記述する。
networks
にdevelopment
を追加して起動したganacheの画面から取得できる情報を入れるのだが、truffle init
で初期化したときに既にコメントアウトで書かれていたので、コメントを外せばそのままの設定で使える。
もしganache側の設定を変えていたら、もちろんtruffle-config.js
も修正する必要がある。
今回はこのまま行く。
module.exports = {
networks: {
// ↓ 追加
development: {
host: '127.0.0.1', // Localhost (default: none)
port: 7545, // Standard Ethereum port (default: none)
network_id: '*', // Any network (default: none)
},
...
}
}
これでローカルネットに繋がった。
次はこのネットワークに実際にコントラクトをデプロイしてテストしてみる。
コントラクトをテストする
yarn truffle test
を実行。
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Count of: undefined
Contract: Counter
✔ initial count of 0 (115ms)
✔ The result of incrementing is 1. (171ms)
✔ Adding up results in 100. (149ms)
3 passing (534ms)
✨ Done in 8.45s.
テストが通ったのでこのコントラクトは問題ないことが証明された。
コントラクトをmigrationを通してデプロイする
migrations
以下にデプロイタスクを記述したjsファイルを作成できる。
タスクが複数ある場合、順次実行されるのでファイル名の接頭辞は数字で始める必要があるとのこと。
migrations/1_deploy_counter.js
を作成。
const Counter = artifacts.require('Counter')
module.exports = async function (deployer) {
await deployer.deploy(Counter)
const instance = await Counter.deployed()
const value = await instance.read()
console.log('Count of: ', value.toString())
}
yarn migrate
でマイグレーション実行。
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_deploy_counter.js
===================
Replacing 'Counter'
-------------------
> transaction hash: 0x9fd5dd49586cedad3adb6465b3f561446688543267c85a2dc289dbb3ac19d9a1
> Blocks: 0 Seconds: 0
> contract address: 0x245a487C9a5da383AD23A4E5141B68fB2B6F8863
> block number: 12
> block timestamp: 1667461560
> account: 0xc6d324a5A1febAb348dC5Fb5993785E27FEb4389
> balance: 99.97519708
> gas used: 171745 (0x29ee1)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.0034349 ETH
Count of: 0
> Saving artifacts
-------------------------------------
> Total cost: 0.0034349 ETH
Summary
=======
> Total deployments: 1
> Final cost: 0.0034349 ETH
デプロイが成功し、各種情報が出力されていることがわかる。
この時ネットワークにトランザクションを送信するアドレスはGanacheで作成した10個のアドレスのうち、一番最初のアドレスが使われている。おそらくデフォルトはそうなってる。
デプロイしたコントラクトを叩いてみる。
それでは先ほどデプロイしたコントラクトを実際に叩いてみる。
truffleからはTruffleコンソールというものが提供されており、CLIからブロックチェーン上のコントラクトとやりとりできる。
yarn console
でTruffleコンソールを起動する。
するとこんな表示が出てくる。
truffle(development)>
この>
の後に続けてコントラクトとやり取りするコードを書く。jsで書けるらしい。
試しにコントラクトアドレスを取得してみる。
truffle(development)> const instance = await Counter.deployed()
truffle(development)> instance.address
'0x245a487C9a5da383AD23A4E5141B68fB2B6F8863'
取得できた。デプロイ時に出力されたコントラクトアドレスと一致している。
カウンターをincrementしてカウントの値を取得してみる。
truffle(development)> await instance.increment()
{
tx: '0x0490980014229df7f9b280437e45c096b58c6c3afec5a3f3890ae75c2895efb7',
receipt: {
transactionHash: '0x0490980014229df7f9b280437e45c096b58c6c3afec5a3f3890ae75c2895efb7',
transactionIndex: 0,
blockHash: '0x09d018f083199998a768b876e0a7e895a61bf6e7d3c2217dce84f81174350183',
blockNumber: 13,
from: '0xc6d324a5a1febab348dc5fb5993785e27feb4389',
to: '0x245a487c9a5da383ad23a4e5141b68fb2b6f8863',
gasUsed: 42267,
cumulativeGasUsed: 42267,
contractAddress: null,
logs: [],
status: true,
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
rawLogs: []
},
logs: []
}
truffle(development)> value
BN { negative: 0, words: [ 1, <1 empty item> ], length: 1, red: null }
truffle(development)> value.toString()
'1'
increment()
を実行するトランザクションを送信してカウントをincrementさせ、read()
で値を取得した結果、ちゃんと1
になっている。
しかしよくわからないのが、read()
で取得した結果をそのまま出力してもBN(BigNumber)で返されるということ。
toString()
をつけて出力することで期待した値にはなるが、この辺りの仕様はいまいち理解できていない。
一応、以下リンクが参考になった。
今回のリポジトリ貼っておく。
まだtruffleの学習は続けるのでスクラップはクローズしない。
Truffle for VSCode
Truffle for VSCode が提供されているので使ってみる。
VSCode拡張機能をインストール
まずはTruffle for VS Code をインストールする。
VSCodeの拡張機能タブからインストール。
その他、Node.js
とGit
が依存関係として必要らしいがもうインストールしているのでOK。
インストールが完了したらコマンドパレットでtruffle
と入れてみる。
すると、truffleでよく使用するであろう機能が色々出てきた。これは便利かも。
Solidityプロジェクトを作成する
手始めにNew Solidity Project
を選択し、新しいプロジェクトを作る。
すると空のプロジェクト、サンプルコードが書かれたプロジェクト、truffle box(スターターキットみたいなやつ)を使用したプロジェクトを作るかの3つの選択肢が出てきた。
とりあえずサンプルプロジェクトを作る。Create sample project
を選択。
ダイアログが開き、どのディレクトリにプロジェクトを作るか選択する。
試しに触ってるだけなので、このスクラップで使用していたリポジトリのルートに_test
というディレクトリを作ってそこにプロジェクトを作成することにする。
新しいワークスペースが開いてサンプルコードが書かれたプロジェクトが作成された。
コントラクトをコンパイルする
Build Contracts
でコントラクトをコンパイルする。
VSCodeにtruffleをインストールしているのか、下のような表記が出てきたので待機。
インストールが終わったがコンパイルされない。なんでや...。
VSCodeをリロードして再度実行するも変わらず。
別の方法でコンパイルを試す。.sol
ファイルを選択して右クリック。
表示されたダイアログの下側にBuild Contracts
があるのでクリック。
何も起きない。なぜなのか?
よくみると右下に警告のポップアップがあることに気づく(スクショは忘れた)
「必要なツールが無いからインストールしろ」とのこと。
新しいSolidityプロジェクト作成時に以下のような画面が出てくるのだが、この画面をよく見てみると必要なツールをワンクリックでインストールできそうなUIがある。
どうやらTruffleとGanacheを別途インストールしないといけないらしい。
TruffleはさっきのでインストールされているはずなのでGanacheをインストール。
再度コンパイルしてみると通った。
と思いきやbuild
の中身はなんと空...。なんでやねん!
試行錯誤したけどちょっとよくわからなかった、バグか...??
いったん飛ばします。
コントラクトのデプロイ
Ganacheを使用したローカルネットへのデプロイもクリックするだけでいけるらしい。
.sol
ファイルを右クリックしてDeploy Contracts
を選択。
Create a new network
を選択
使用するツールを選ぶ。Infura
も使えるようだが、ここはGanache
を選択。
Local
を選択。
ホストするポートを入力してEnter。ここでは7545
にしておく。
デプロイ先のネットワークを選択。
多分立ち上がってるネットワークの一覧がここに表示される。
ターミナルが起動して以下が表示される。
デプロイが完了し、詳細が表示されている。
[Truffle: Execute command] > Blocks: 0 Seconds: 0
> contract address: 0x901Cbbc21C741BFcB0a040D605fE5A3eBb5cB69f
> block number: 1
> block timestamp: 1667484479
> account: 0x632F289BFed1df67DeF23556aAA7ceFf9DD75984
> balance: 999.998058561625
> gas used: 575241 (0x8c709)
> gas price: 3.375 gwei
> value sent: 0 ETH
> total cost: 0.001941438375 ETH
[Truffle: Execute command] > Saving artifacts
-------------------------------------
> Total cost: 0.001941438375 ETH
Summary
=======
> Total deployments: 1
> Final cost: 0.001941438375 ETH
よくみるといつの間にかちゃんとビルドファイルがある。
デプロイするとファイルがちゃんと出力されるのはなぜなんだろう...。
ネットワークの停止
Ganacheサーバーはもう用済みなので停止したい。
Stop Ganache
で停止できる。