TruffleSuiteで始めるDapps開発2

どうも、次世代デジタル基盤開発事業部(旧ブロックチェーン事業部)の土田です。
今回から前回つくった開発環境を使ってDappsを作っていきたいと思います。
tec.tecotec.co.jp

今回のゴール

コントラクトを書いて、プライベートチェーン上にデプロイし、実行するところまでをやっていきます。

Ganacheの永続化

まずはGanacheで立てたノードの永続化を行います。起動時にオプションを指定するだけです。

yarn ganache-cli --db 任意のパス --mnemonic "任意の文字" --networkId 任意の数字

各種オプションについては下記を参照して下さい。 github.com

ニーモニックとネットワークIDを指定することで、同じアカウントを使うことができます。また、この指定が無いと毎回アドレスを生成してしまいますので、注意が必要です。

ニーモニック(Mnemonic)

MetaMaskでアカウントを作成した際に12個の単語が表示されたかと思います。アレです。
Ganacheではアドレス生成やトランザクション送信の署名等に使用されるそうです。開発で使ったニーモニックは絶対に本番で使用しないでください。

Ganache起動(永続化)

というわけで、クライアントとGUIを起動します。 f:id:teco_tsuchida:20210827180146p:plain f:id:teco_tsuchida:20210827180216p:plain

RPCを試す

せっかくなのでRPCも試しておきます。Ganache-cliで実装されているメソッドは下記辺りを参考にしてください。

github.com github.com

まずはマイニング報酬を受け取るアカウントを確認します。

curl -X POST http://127.0.0.1:8545 --data '{"jsonrpc":"2.0","method":"eth_coinbase","id":0}'

{"id":0,"jsonrpc":"2.0","result":"0xffd2004aa4baa1b1498f290b4e483b6df6d06699"}

表示されたresultのアドレスと、クライアントを起動した際に表示されていたアドレスが一致していることを確認してください。
送金も試しておきます。RPCでのコールの際、valueには10進数を16進数に変換し、0xを追加したものを指定します。
今回は0.01ETHを送金するので、100000000000000002386F26FC10000になり、0xを追加して0x2386F26FC10000となります。

curl -X POST http://127.0.0.1:8545 --data '{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[{"from":"0xFfd2004aA4BAa1b1498F290B4E483B6Df6d06699","value":"0x2386F26FC10000","to":"0xd144FB12830e7e2C41a070D44fb2cAe24959585D"}],"id":0}'

{"id":0,"jsonrpc":"2.0","result":"0xaf24403ceceb055b97fb42bfb759763e0402118e901733b9dd521e8b1d7e5bdf"}

GUI上でもトランザクションが処理されたことを確認してみます。 f:id:teco_tsuchida:20210827180829p:plain f:id:teco_tsuchida:20210827180836p:plain

トランザクションも確認でき、ETHも減っていました。

いざ、コントラクト開発

諸々の準備が出来ました。コントラクトを開発していきたいと思いますが、良い題材が思い浮かばなかったので、クリプトゾンビをやっていこうと思います。(クリプトゾンビの後半にはTruffleを使ったチャプターもあるので、そちらを参考にしても良いかもしれません。)

cryptozombies.io

レッスン1のチャプター13まで進めると、シンプルなゾンビを生成することができるコントラクトが完成します。こちらをプライベートチェーンにデプロイしてみましょう!

前回からの続きになりますので、truffle-boxで準備したディレクトリで作業します。まずはデプロイ用のjsを用意し、コントラクトをsolcファイルに転記します。
転記したコントラクトファイルをcontractsディレクトリ配下に配置して下さい。

次に、migrationsディレクトリ配下に下記のデプロイ用ファイルを用意します。

var ZombieFactory = artifacts.require("./ZombieFactory.sol");

module.exports = function(deployer) {
  deployer.deploy(ZombieFactory);
};

最終的な構造は下記のようになると思います。
f:id:teco_tsuchida:20210827184335p:plain

準備ができましたので、コンソールにてtruffle consoleを実行し、いざcompile!

truffle console
truffle(development)> compile

すると、おそらく下記のようなエラーになるかと思います。

truffle(development)> compile

Compiling your contracts...
===========================
> Compiling ./contracts/ZombieFactory.sol

project:/contracts/ZombieFactory.sol:18:28: TypeError: Data location must be "storage" or "memory" for parameter in function, but none was given.
    function _createZombie(string _name, uint _dna) private {
                           ^----------^
,project:/contracts/ZombieFactory.sol:23:33: TypeError: Data location must be "storage" or "memory" for parameter in function, but none was given.
    function _generateRandomDna(string _str) private view returns (uint) {
                                ^---------^
,project:/contracts/ZombieFactory.sol:28:33: TypeError: Data location must be "memory" for parameter in function, but none was given.
    function createRandomZombie(string _name) public {
                                ^----------^

Compilation failed. See above.

こちらはsolidityのバージョンの違いによるものです。ゾンビファクトリーのバージョンはpragma solidity ^0.4.19;となっていませんか?
truffleのsolidityのバージョンを確認してみましょう。

truffle(development)> version
Truffle v5.4.2 (core: 5.4.2)
Solidity v0.5.16 (solc-js)
Node v15.14.0
Web3.js v1.4.0

執筆時点で、v0.5.16でした。実はv0.5.0から破壊的変更がいくつかあるようで、その変更によりエラーとなっているようです。エラーの内容は、memoryを明示する必要があるとのことです。

docs.soliditylang.org

Explicit data location for all variables of struct, array or mapping types is now mandatory. This is also applied to function parameters and return variables. For example, change uint x = m_x to uint storage x = m_x, and function f(uint x) to function f(uint memory x) where memory is the data location and might be replaced by storage or calldata accordingly. Note that external functions require parameters with a data location of calldata.

コントラクトの一部を修正します。コンソールで指摘されている箇所をそれぞれstring memory _xxxxxと修正してください。
それでは、もう一度コンパイル!

truffle(development)> compile

Compiling your contracts...
===========================
> Compiling ./contracts/ZombieFactory.sol

project:/contracts/ZombieFactory.sol:20:9: TypeError: Event invocations have to be prefixed by "emit".
        NewZombie(id, _name, _dna);
        ^------------------------^
,project:/contracts/ZombieFactory.sol:24:36: TypeError: Invalid type for argument in function call. Invalid implicit conversion from string memory to bytes memory requested. This function requires a single bytes argument. Use abi.encodePacked(...) to obtain the pre-0.5.0 behaviour or abi.encode(...) to use ABI encoding.
        uint rand = uint(keccak256(_str));
                                   ^--^

Compilation failed. See above.

また違うエラーに・・・。一つずつやりましょう。

まずはTypeError: Event invocations have to be prefixed by "emit".です。
下記ドキュメントから、emitキーワードを明示的に付ける必要があるそうです。

docs.soliditylang.org

該当箇所をemit NewZombie(id, _name, _dna);と修正しましょう。

次にTypeError: Invalid type for argument in function call. 以下略 です。これは先程のv0.5.0の破壊的変更に記載があります。

docs.soliditylang.org

The functions .call(), .delegatecall(), staticcall(), keccak256(), sha256() and ripemd160() now accept only a single bytes argument. Moreover, the argument is not padded. This was changed to make more explicit and clear how the arguments are concatenated.

keccak256()にはbytes型のみ渡すことが出来るとのことで、修正していきます。bytesで渡せばいいので、uint rand = uint(keccak256(bytes(_str)));とすれば良いはずです。

いざ、コンパイル

truffle(development)> compile

Compiling your contracts...
===========================
> Compiling ./contracts/ZombieFactory.sol
> Artifacts written to /home/tsuchida/workspace/react-truffle-box/client/src/contracts
> Compiled successfully using:
   - solc: 0.5.16+commit.9c3226ce.Emscripten.clang

できました!!まだまだ油断はできません。そのままデプロイします。

truffle(development)> migrate

Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.

中略

3_deploy_contracts.js
=====================

   Deploying 'ZombieFactory'
   -------------------------
   > transaction hash:    0x3faf47ad2d2c9c080216665d6b696938c80c1ad1e47b5cc552681b9a60c996b6
   > Blocks: 0            Seconds: 0
   > contract address:    0x2E5F40D8e42c5c5795d63F70Ab744F814EbC3d43
   > block number:        6
   > block timestamp:     1629988251
   > account:             0xFfd2004aA4BAa1b1498F290B4E483B6Df6d06699
   > balance:             99.97594284
   > gas used:            351812 (0x55e44)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00703624 ETH

   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00703624 ETH

Summary
=======
> Total deployments:   3
> Final cost:          0.01224352 ETH

- Blocks: 0            Seconds: 0
- Saving migration to chain.
- Blocks: 0            Seconds: 0
- Saving migration to chain.
- Blocks: 0            Seconds: 0
- Saving migration to chain.

成功しているようです!
トランザクションも確認してみます。コンソールに表示されたトランザクションハッシュは下記です。

0x3faf47ad2d2c9c080216665d6b696938c80c1ad1e47b5cc552681b9a60c996b6

GUIで検索してみましょう。

f:id:teco_tsuchida:20210827181804p:plain GUIでも確認できましたね!
ここまでで、実際にコントラクトを自分で書いて、デプロイするということが出来たと思います。

実行までやってしまえ!

ついでなので、実行もしてみます。truffle consoleにてそのまま実行できます。公式通りにやります。

www.trufflesuite.com

コンソールに表示されたコントラクトアドレス(0x2E5F40D8e42c5c5795d63F70Ab744F814EbC3d43)を指定します。

truffle(development)> let myZombieFactory = await ZombieFactory.at("0x2E5F40D8e42c5c5795d63F70Ab744F814EbC3d43")
undefined

undefinedと出力されますが、問題ありません。取得したオブジェクトを確認してみます。

truffle(development)> myZombieFactory 
TruffleContract {
  constructor: [Function: TruffleContract] {
略

いろいろ出力されたかと思いますが、定義したNewZombie: [Function (anonymous)],等が確認出来るはずです。
それでは、ゾンビを生成しましょう。

truffle(development)> let myZombie = myZombieFactory.createRandomZombie("tsuchida")
undefined
truffle(development)> myZombie
{
  tx: '0x772b89dd762f54bf0156800d6987b47ef0739e3ffef643904c98eb8650e1e9cf',
  receipt: {
    transactionHash: '0x772b89dd762f54bf0156800d6987b47ef0739e3ffef643904c98eb8650e1e9cf',
    transactionIndex: 0,
    blockHash: '0x0e90df84c217cfb7e9dfc8a6e52f8efd45b13ac4c2ed39113b24b0c873b51e72',
    blockNumber: 8,
    from: '0xffd2004aa4baa1b1498f290b4e483b6df6d06699',
    to: '0x2e5f40d8e42c5c5795d63f70ab744f814ebc3d43',
    gasUsed: 87786,
    cumulativeGasUsed: 87786,
    contractAddress: null,
    logs: [ [Object] ],
    status: true,
    logsBloom: '0x00000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000240000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
    rawLogs: [ [Object] ]
  },
  logs: [
    {
      logIndex: 0,
      transactionIndex: 0,
      transactionHash: '0x772b89dd762f54bf0156800d6987b47ef0739e3ffef643904c98eb8650e1e9cf',
      blockHash: '0x0e90df84c217cfb7e9dfc8a6e52f8efd45b13ac4c2ed39113b24b0c873b51e72',
      blockNumber: 8,
      address: '0x2E5F40D8e42c5c5795d63F70Ab744F814EbC3d43',
      type: 'mined',
      removed: false,
      id: 'log_dd10701a',
      event: 'NewZombie',
      args: [Result]
    }
  ]
}

なにやらトランザクション情報が取得できました。これもGUIで確認してみましょう。 f:id:teco_tsuchida:20210827181917p:plain

ここでスクリーンショットをとっていて気付きましたが、イベントログが取れていないようです。コントラクトの連携もしていないので当たり前なのですが、こちらは次回以降調べてみます。

より詳しくゾンビが出来ているか、コンソール上で確認してみます。トランザクションハッシュやコントラクトのABIもわかっているので、トランザクションログをデコードして調べることにします。ドンピシャなQiita記事を見つけましたので、そのまま参考にさせていただきます。

qiita.com

truffle(development)> let txReceipt = await web3.eth.getTransactionReceipt("0x772b89dd762f54bf0156800d6987b47ef0739e3ffef643904c98eb8650e1e9cf")
undefined
truffle(development)> txReceipt
{
  transactionHash: '0x772b89dd762f54bf0156800d6987b47ef0739e3ffef643904c98eb8650e1e9cf',
  transactionIndex: 0,
  blockHash: '0x0e90df84c217cfb7e9dfc8a6e52f8efd45b13ac4c2ed39113b24b0c873b51e72',
  blockNumber: 8,
  from: '0xffd2004aa4baa1b1498f290b4e483b6df6d06699',
  to: '0x2e5f40d8e42c5c5795d63f70ab744f814ebc3d43',
  gasUsed: '0x156ea',
  cumulativeGasUsed: 87786,
  contractAddress: null,
  logs: [
    {
      logIndex: 0,
      transactionIndex: 0,
      transactionHash: '0x772b89dd762f54bf0156800d6987b47ef0739e3ffef643904c98eb8650e1e9cf',
      blockHash: '0x0e90df84c217cfb7e9dfc8a6e52f8efd45b13ac4c2ed39113b24b0c873b51e72',
      blockNumber: 8,
      address: '0x2E5F40D8e42c5c5795d63F70Ab744F814EbC3d43',
      data: '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000181898d76b058c00000000000000000000000000000000000000000000000000000000000000087473756368696461000000000000000000000000000000000000000000000000',
      topics: [Array],
      type: 'mined',
      removed: false,
      id: 'log_dd10701a'
    }
  ],
  status: true,
  logsBloom: '0x00000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000240000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
}
truffle(development)> let eventAbi = ZombieFactory.abi.filter(element => element.signature == txReceipt.logs[0].topics);
undefined
truffle(development)>  web3.eth.abi.decodeLog(eventAbi[0].inputs, txReceipt.logs[0].data, txReceipt.logs[0].topics)
Result {
  '0': '0',
  '1': 'tsuchida',
  '2': '6782444169266572',
  __length__: 3,
  zombieId: '0',
  name: 'tsuchida',
  dna: '6782444169266572'
}

回りくどいですが、しっかりname: 'tsuchida'で生成されているようです!プライベートチェーン上とはいえ、世界に一つの自分のゾンビです!(プライベートチェーン上なので、世に出ているわけではありませんが・・・)

終わり

というわけで、今回はここまでにしたいと思います。
GanacheGUIがトランザクション確認くらいしか出来ないので、本来の使いやすさを取り戻すべく調べつつ、コントラクト開発やフロントエンド開発を進めていきたいと思います。

ここまでご覧いただき、ありがとうございました。

tecotec.co.jp

その他参考

docs.soliditylang.org docs.soliditylang.org ethereum.stackexchange.com github.com