Photo by Maria Elena Zuñiga / Unsplash

Background

俗話說的好 畢圈一日 人間一年..... 開始學 Smart Contract吧, part 3.

Pet Shop

來紀錄一個官方的教學範例.

$ mkdir pet_shop
$ cd pet_shop
$ truffle unbox pet-shop

下載完之後會發現除了合約的部分其他的東西都幫你準備好了.

Writing Contract

$ cd contracts
$ truffle create contract Adoption

Adoption.sol

pragma solidity ^0.4.22;


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]) {
        return adopters;
    }
}

Deploy

$ cd migrations
$ vim 2_deploy_contracts.js

2_deploy_contracts.js

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

module.exports = function(deployer) {
  deployer.deploy(Adoption);
};
$ truffle migrate
Using network 'development'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0xc78bbe7b90643f4ff35039d6bfaed22e120743fe6d40ad759819d0c36196b9a5
  Migrations: 0x76ee5867f55d8e9df4f0e437b63a10bff955329a
Saving successful migration to network...
  ... 0x1cefb50f7606696714020bdfb24665ece62f23fa9dfe387eca155ba6d68c0e04
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying Adoption...
  ... 0x888a92fdbdc23d4cac68b277f50496a9ba274ac2c42dcd775d472a3d02d70a24
  Adoption: 0x8d151cbc7f5637f0c17097bba531636fb9973ef4
Saving successful migration to network...
  ... 0x18c885354c40d82ae85592060910fcc39c65a958083231fe848f37838d183178
Saving artifacts...

Testing

$ cd test
$ vim TestAdoption.sol

TestAdoption.sol

pragma solidity ^0.4.17;

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

contract TestAdoption {
    Adoption adoption = Adoption(DeployedAddresses.Adoption());

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

        uint expected = 8;

        Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
    }

    // Testing retrieval of a single pet's owner
    function testGetAdopterAddressByPetId() public {
        // Expected owner is this contract
        address expected = this;

        address adopter = adoption.adopters(8);

        Assert.equal(adopter, expected, "Owner of pet ID 8 should be recorded.");
    }
}
$ truffle test
Using network 'development'.

Compiling ./contracts/Adoption.sol...
Compiling ./test/TestAdoption.sol...
Compiling truffle/Assert.sol...
Compiling truffle/DeployedAddresses.sol...


  TestAdoption
    ✓ testUserCanAdoptPet (74ms)
    ✓ testGetAdopterAddressByPetId (82ms)


  2 passing (784ms)

Creating a user interface to interact with the smart contract

/src/js/app.js

App = {
  web3Provider: null,
  contracts: {},

  init: 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 App.initWeb3();
  },
  initWeb3: function() {

    // Is there an injected web3 instance?
    if (typeof web3 !== 'undefined') {
        App.web3Provider = web3.currentProvider;
    } else {
        // If no injected web3 instance is detected, fall back to Ganache
        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(adopters, account) {
    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();
  });
});

簡單講解一下整段code的部分:

  • initWeb3: 這邊我們需要透過web3來跟ethereum的節點做溝通. 所以這邊預設的對象會是Ganache的開發節點.
  • initContract: 我們需要告知web3哪裡可以找到我們的smart contract. 基本上就是把 compile smart contract 的結果丟進去. (build/contracts/Adoption.json) 接著透過TruffleContract()來創造一個contract的object來操作它.
  • markAdopted: 簡搭來說就是呼叫了一個 adoptionInstance. 透過它來知道目前有哪些pet被領養. 並且同時改變前端的UI資訊.
  • handleAdopt: 這是跟著bindEvents一起看的. 整個function就是在處理在網頁見面上點擊adopt後所需要產生的流程. (這邊需了解JQuery會比較容易理解)

Installing and configuring MetaMask

整個網頁需要搭配 MetaMask, 所以建議在chrome上安裝MetaMask的extension並且登入.

接著我們要改變 MetaMask 的 network. 讓他連到我們的dev node (Ganache).

Screen-Shot-2018-07-22-at-8.56.53-PM

點選 Custom RPC

Screen-Shot-2018-07-22-at-8.56.58-PM

填入 local dev node 的資訊, 然後import在ganache上的一個帳號進去才會有錢(透過private key即可).

Run Service

好了之後我們就需要把整個 web service run 起來看看網頁長什麼樣子.

$ vim bs-config.json

bs-config.json

{
  "server": {
    "baseDir": ["./src", "./build/contracts"]
  },
  "scripts": {
    "dev": "lite-server",
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}
$ npm run dev

接著應該就會順利出現網頁畫面, 像下圖一樣:

Screen-Shot-2018-07-22-at-9.00.16-PM

接下來就可以簡單地玩一下. 點選Adopt應該會出現MetaMask的錢包. 選擇剛剛import的帳號並且按下submit即可.

Reference