nekoblog

nekoblog

ちょっとためになるブログ

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

はじめに

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

https://trufflesuite.com/tutorial/

petshop.png

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

https://hardhat.org/

参考にした本

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というファイルを作成します。

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というマイグレーションファイルを作成します。

var Adoption = artifacts.require("Adoption");

module.exports = function(deploy) {
    deploy.deploy(Adoption);
}

プライベートネットを作るツールとして、Ganache を利用します。 「Ganache」を起動した状態にして、ターミナルで truffle migrate を実行します。

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

「test」ディレクトリ内に「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 = {
  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. 右上の丸いボタンをクリックし、マイアカウントの「設定」を選択する。
  2. 「ネットワーク」を選択する。
  3. 「ネットワーク追加」をクリックする。
  4. ネットワーク名とRPC URLに http://127.0.0.1:7545 を入れる。
  5. チェーンIDに 1337 を入れる。
  6. 通貨記号に ETH を入れ「保存」を押す。
  7. 右上のXをクリックし設定を終了する。
  8. Ganacheのアカウント1の残高が反映されたことを確認する。

Dappの完成

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

まとめ

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

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

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