Closed9

Truffleの学習

Ryotaro HadaRyotaro Hada

スマートコントラクト開発学習のためにTruffleを学ぶ。

https://trufflesuite.com/

関連ツールが複数提供されていて良い感じ。
一つづつ試していく。

Ryotaro HadaRyotaro Hada

Truffle

まずはメインのTruffleから。

Node.jsのインストール

Node.jsが必要なようなのでVoltaで用意したNode.js v16.15.0を使用することにする。

ついでにyarnを使用したいのでyarn v1.22.18も併せて用意。

https://volta.sh/


Truffle導入

プロジェクトルートを作成してyarn init -ypackage.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

これで準備完了。

Ryotaro HadaRyotaro Hada

Linterを入れたい

コントラクトを書く前にLinter系を入れたくなったので対応する。

Solhint

調べるとsolhintがよく出てくるので採用。

https://protofire.github.io/solhint/

yarn add -D solhintでインストール。

"solhint": "^3.3.7"

package.jsonのscriptsにも追加しておく。

"lint": "solhint **/*.sol",

設定ファイルがない場合は初期化が必要らしい。

yarn lint --initを実行すると.solhint.jsonが作成された。

中身は以下に書き換える。
とりあえずrecommendedの設定で進める。

ルール一覧
https://protofire.github.io/solhint/docs/rules.html

{
  "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-versionoffにした(本当はダメ)

{
  "extends": "solhint:recommended",
  "plugins": [],
  "rules": {
    "compiler-version": "off"
  }
}

直し方は今度調べる。

もう一回lint実行。

$ solhint **/*.sol contracts/Test.sol
✨  Done in 0.82s.

いけた。


Prettier

linterを入れるならprettierも入れたい。

これが良さそうなので導入。

https://www.npmjs.com/package/prettier-plugin-solidity

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はもう必要ないので削除。


次からはコントラクト書いていく。

Ryotaro HadaRyotaro Hada

コントラクトを書いていく

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の場合はmochachaiを使用しているらしい。

独自にcontract()を提供しており、実行時にEthereumローカルネットをクリーンアップしてくれるとのこと。さらにcontract()の第二引数に渡すコールバック関数の引数からaccountsリストを取得できるとのこと。

https://trufflesuite.com/docs/truffle/how-to/debug-test/write-tests-in-javascript/

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でテストを開始できるが、一つ大事なことを忘れていたので先にそれをやる。

Ryotaro HadaRyotaro Hada

Ganacheでローカルネットを構築

ブロックチェーン上のプログラムを書いているので開発用のネットワークがないとお話にならない。

truffleから提供されているGanache(ガナッシュ)を使用する。

GanacheはGUIとCLIを提供しているが、今回はGUIを選択、インストール。

開くと最初にこんな画面が出る(執筆時点ではもう触っているのでプロジェクトが表示されている)

QUICKSTARTを押す。

すると以下のような画面になった。

Ethereumローカルネットワークが起動、アドレスが10個生成されてそれぞれに100ETHずつ入っている。

このローカルネットに接続するにはtruffle-config.jsにネットワークの情報を記述する。

networksdevelopmentを追加して起動した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)
    },
    ...
  }
}

これでローカルネットに繋がった。

次はこのネットワークに実際にコントラクトをデプロイしてテストしてみる。

Ryotaro HadaRyotaro Hada

コントラクトをテストする

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.

テストが通ったのでこのコントラクトは問題ないことが証明された。

Ryotaro HadaRyotaro Hada

コントラクトをmigrationを通してデプロイする

migrations以下にデプロイタスクを記述したjsファイルを作成できる。

タスクが複数ある場合、順次実行されるのでファイル名の接頭辞は数字で始める必要があるとのこと。

https://trufflesuite.com/docs/truffle/how-to/contracts/run-migrations/

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()をつけて出力することで期待した値にはなるが、この辺りの仕様はいまいち理解できていない。

一応、以下リンクが参考になった。

https://www.preciouschicken.com/blog/posts/decomposing-a-bignumber-in-truffle-console/

https://stackoverflow.com/questions/65690694/convert-bn-to-number

Ryotaro HadaRyotaro Hada

Truffle for VSCode

Truffle for VSCode が提供されているので使ってみる。

https://trufflesuite.com/docs/vscode-ext/

VSCode拡張機能をインストール

まずはTruffle for VS Code をインストールする。

VSCodeの拡張機能タブからインストール。

その他、Node.jsGitが依存関係として必要らしいがもうインストールしているので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で停止できる。

このスクラップは2023/06/14にクローズされました