Truffle のチュートリアル ETHEREUM PET SHOP をやってみた

# はじめに
「[Solidity とは何か？ 開発環境を構築する！](https://qiita.com/nekoharuyuki/items/b00754ee638ec3418668)」で、Solidity の基礎を理解し、スマートコントラクトプロジェクトを作成する事が出来ました。
Dapps開発のフレームワークである「Truffle」の公式サイトで紹介されている「[ETHEREUM PET SHOP](https://trufflesuite.com/tutorial/)」のチュートリアルをやってみて、実際にウェブ上に分散型アプリケーションを作成しましたので、備忘録としてまとめます。

https://trufflesuite.com/tutorial/

![petshop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229633/7d5578fb-69f1-736e-eba0-660f0c473332.png)

「Truffle」以外にも **Hardhat** を使用して Dapps開発をする方法があります。

https://hardhat.org/

### 参考にした本
* [マスタリング・イーサリアム ―スマートコントラクトとDAppの構築](https://amzn.to/3xRzfF8)
* [SolidityとEthereumによる実践スマートコントラクト開発 ―Truffle Suiteを用いた開発の基礎からデプロイまで](https://amzn.to/3xRWFu5)
* [詳解 ビットコイン ―ゼロから設計する過程で学ぶデジタル通貨システム](https://amzn.to/3xqrGnC)
* [ブロックチェーン dapp＆ゲーム開発入門 Solidityによるイーサリアム分散アプリプログラミング](https://amzn.to/39o2IgD)

## ETHEREUM PET SHOP

ペットの「adopt」というボタンを押すとMetaMaskが起動し、表示された金額と手数料を確認してトランザクションが作成されETHで支払うことができます。

## Truffle Box を使った Truffleプロジェクトの作成
pet-shop-tutorialというディレクトリを作り、このディレクトリ内で作業していきます。
```
$ mkdir pet-shop-tutorial
$ cd pet-shop-tutorial
```
今回はチュートリアルのためTruffle Boxというあらかじめ用意されたプロジェクトから作成します。
```
$ truffle unbox pet-shop
```
## スマートコントラクトを書く
`contracts/`ディレクトリの中に`Adoption.sol`というファイルを作成します。
```Adoption.sol
pragma solidity ^0.5.0;

contract Adoption {
    address[16] public adopters;

    // Adopting a pet
    function adopt(uint petId) public returns (uint) {
        require(petId >= 0 && petId <= 15);

        adopters[petId] = msg.sender;

        return petId;
    }

    // Retrieving the adopters
    function getAdopters() public view returns (address[16] memory) {
        return adopters;
    }
}
```
`truffle compile` を実行します。

## マイグレーション
`migrations/`ディレクトリの中に`2_deploy_contracts.js`というマイグレーションファイルを作成します。
```2_deploy_contracts.js
var Adoption = artifacts.require("Adoption");

module.exports = function(deploy) {
    deploy.deploy(Adoption);
}
```
プライベートネットを作るツールとして、[Ganache](https://trufflesuite.com/docs/ganache/quickstart/) を利用します。
「Ganache」を起動した状態にして、ターミナルで `truffle migrate` を実行します。

## スマートコントラクトのテストコードの作成

「test」ディレクトリ内に「TestAdoption.sol」というテストコードを作成しましょう。
```TestAdoption.sol
pragma solidity ^0.5.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";

contract TestAdoption {
  // The address of the adoption contract to be tested
  Adoption adoption = Adoption(DeployedAddresses.Adoption());

  // The id of the pet that will be used for testing
  uint expectedPetId = 8;

  // The expected owner of adopted pet is this contract
  address expectedAdopter = address(this);

  // Testing the adopt() function
  function testUserCanAdoptPet() public {
    uint returnedId = adoption.adopt(expectedPetId);

    Assert.equal(returnedId, expectedPetId, "Adoption of the expected pet should match what is returned.");
  }

  // Testing retrieval of a single pet's owner
  function testGetAdopterAddressByPetId() public {
    address adopter = adoption.adopters(expectedPetId);

    Assert.equal(adopter, expectedAdopter, "Owner of the expected pet should be this contract");
  }

  // Testing retrieval of all pet owners
  function testGetAdopterAddressByPetIdInArray() public {
    // Store adopters in memory rather than contract's storage
    address[16] memory adopters = adoption.getAdopters();

    Assert.equal(adopters[expectedPetId], expectedAdopter, "Owner of the expected pet should be this contract");
  }
}
```

`truffle test` を実行します。

## スマートコントラクトと連動したUIの作成
フロントエンド部分のロジックは src ディレクトリで、スマートコントラクトの連携する部分を実装する為に、 `/src/js/app.js` ファイルを編集します。

```app.js
App = {
  web3Provider: null,
  contracts: {},

  init: async function() {
    // Load pets.
    $.getJSON('../pets.json', function(data) {
      var petsRow = $('#petsRow');
      var petTemplate = $('#petTemplate');

      for (i = 0; i < data.length; i ++) {
        petTemplate.find('.panel-title').text(data[i].name);
        petTemplate.find('img').attr('src', data[i].picture);
        petTemplate.find('.pet-breed').text(data[i].breed);
        petTemplate.find('.pet-age').text(data[i].age);
        petTemplate.find('.pet-location').text(data[i].location);
        petTemplate.find('.btn-adopt').attr('data-id', data[i].id);

        petsRow.append(petTemplate.html());
      }
    });

    return await App.initWeb3();
  },

  initWeb3: async function() {
    // Modern dapp browsers...
    if (window.ethereum) {
      App.web3Provider = window.ethereum;
      try {
        // Request account access
        await window.ethereum.request({ method: "eth_requestAccounts" });;
      } catch (error) {
        // User denied account access...
        console.error("User denied account access")
      }
    }
    // Legacy dapp browsers...
    else if (window.web3) {
      App.web3Provider = window.web3.currentProvider;
    }
    // If no injected web3 instance is detected, fall back to Ganache
    else {
      App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
    }
    web3 = new Web3(App.web3Provider);

    return App.initContract();
  },

  initContract: function() {
    $.getJSON('Adoption.json', function(data) {
      // Get the necessary contract artifact file and instantiate it with @truffle/contract
      var AdoptionArtifact = data;
      App.contracts.Adoption = TruffleContract(AdoptionArtifact);

      // Set the provider for our contract
      App.contracts.Adoption.setProvider(App.web3Provider);

      // Use our contract to retrieve and mark the adopted pets
      return App.markAdopted();
    });
    return App.bindEvents();
  },

  bindEvents: function() {
    $(document).on('click', '.btn-adopt', App.handleAdopt);
  },

  markAdopted: function() {
    var adoptionInstance;

    App.contracts.Adoption.deployed().then(function(instance) {
      adoptionInstance = instance;

      return adoptionInstance.getAdopters.call();
    }).then(function(adopters) {
      for (i = 0; i < adopters.length; i++) {
        if (adopters[i] !== '0x0000000000000000000000000000000000000000') {
          $('.panel-pet').eq(i).find('button').text('Success').attr('disabled', true);
        }
      }
    }).catch(function(err) {
      console.log(err.message);
    });
  },

  handleAdopt: function(event) {
    event.preventDefault();

    var petId = parseInt($(event.target).data('id'));

    var adoptionInstance;

    web3.eth.getAccounts(function(error, accounts) {
      if (error) {
        console.log(error);
      }

      var account = accounts[0];

      App.contracts.Adoption.deployed().then(function(instance) {
        adoptionInstance = instance;

        // Execute adopt as a transaction by sending account
        return adoptionInstance.adopt(petId, {from: account});
      }).then(function(result) {
        return App.markAdopted();
      }).catch(function(err) {
        console.log(err.message);
      });
    });
  }

};

$(function() {
  $(window).load(function() {
    App.init();
  });
});

```

### MetaMaskとブロックチェーンを接続する
1. 右上の丸いボタンをクリックし、マイアカウントの「設定」を選択する。
1. 「ネットワーク」を選択する。
1. 「ネットワーク追加」をクリックする。
1. ネットワーク名とRPC URLに `http://127.0.0.1:7545` を入れる。
1. チェーンIDに `1337` を入れる。
1. 通貨記号に `ETH` を入れ「保存」を押す。
1. 右上のXをクリックし設定を終了する。
1. Ganacheのアカウント１の残高が反映されたことを確認する。

## Dappの完成
`npm run dev` を実行します。
Dappがブラウザ上で表示できます。「adopt」ボタンをクリックすると、MetaMaskによりトランザクションが送られ、ETH払いでペットを購入することができます。

# まとめ
この記事では、`truffle unbox pet-shop` で作成される、Truffleのチュートリアルで ETHEREUM PET SHOP の流れにそって、実際にウェブ上に分散型アプリケーションを作成する事が出来るようになりました。

今後は以下の内容をまとめる予定です。

公開されているテストネットワーク Görli testnet にデプロイする。