使用truffle來練習撰寫認養寵物的智能合約

Posted by Kubeguts on 2017-09-07

本篇是翻譯自Truffle官方所釋出的Dapp教學文檔Pet-Shop
http://truffleframework.com/tutorials/pet-shop

執行環境

開發前準備

首先要安裝testrpc、truffle :

1
2
3
4
5
npm install -g ethereumjs-testrpc
// 安裝測試用ethereum私有鍊

npm install -g truffle
// 開發智能合約的一套框架

安裝truffle上已經預先包好的練習專案(Truffle box: ETHEREUM PET SHOP):

1
2
3
4
5
6
7
8
mkdir pet-shop-tutorial 
// 創建一個目錄

cd pet-shop-tutorial
// 切換到該目錄

truffle unbox pet-shop
// 安裝truffle打包好的pet-shop練習專案檔

目錄架構

  • /contracts: 存放合約的地方,檔名為.sol,而Migrate.sol是負責紀錄其他合約如何deploy到區塊鏈,不能刪除!

  • /migrations: 負責將合約掛到區塊鏈上,並且追蹤合約的更動狀況。

  • /test: 包含Javascript and solidity檔案,負責測試合約內容。

  • truffle.js : truffle的設定檔

合約內容

在/contract 建立 Adoption.sol

宣告一個contract:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pragma solidity ^0.4.4;
// 告訴compiler 現在要用哪個版本編譯

contract Adoption { //宣告一個 contract的class 叫 Adoption
// 存放領養飼主的地址(預設是有16隻狗等待被認養)
address[16] public adopters;


// 有人要認養某隻寵物(petId),判斷是否可以認養,
// 回傳該認養的寵物ID以證明該寵物成功被某用戶認養
function adopt(uint petId) public returns (uint) {

require(petId >= 0 && petId <= 15);
// 判斷petId,若不符合結束該函式,不再往下執行

adopters[petId] = msg.sender;
// 透過msg.sender取得呼叫該函式的使用者
// 也就是認養該寵物的用戶ID

return petId;
}

// 回傳所有的認養者,回傳值的型態為address[16]
function getAdopters() public returns (address[16]) {
return adopters;
}

}

編譯(Compiling)和部署(Migrating)合約

寫好合約後,需要將.sol檔進行編譯成.bytecode,才能在EVM (ethereum virtual machine上執行).

然後在terminal上執行 testrpc 啟動

啟動後會出現:

  • 數組帳戶的address以及私鑰
  • HD wallet的資訊(稍後會提到metatask,會使用到Mnemonic section的資訊)

Mnemonic為數個變數的資訊,如下
Mnemonic: spider level team helmet shaft clarify abuse recipe stem ankle angry fee

執行 truffle compile 會看到 .sol檔被編譯

Migration

migrate.sol描述如何將合約的內容部署到鏈上,並且處理合約上state的更動。

/migration 檔案內:

  • 1_initial_migration.js:
1
2
3
4
5
6
// 引入編譯合約的內容
var Migrations = artifacts.require("./Migrations.sol");

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

透過該檔案可以追蹤後續contract的變化,已部署過的合約且沒有被修改就不用再被部署(不然又會消耗gas).

  • 2_deploy_contracts.js
    注意,這邊的命名開頭要編號,因為truffle進行migrate時會依據該編號而進行。
1
2
3
4
5
var Adoption = artifacts.require("./Adoption.sol");

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

在terminal執行 truffle migrate

會看到如下結果

1
2
3
4
5
6
7
8
9
10
11
12
Using network 'development'.

Running migration: 1_initial_migration.js
Deploying Migrations...
Migrations: 0x75175eb116b36ff5fef15ebd15cbab01b50b50d1
Saving successful migration to network...
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying Adoption...
Adoption: 0xb9f485451a945e65e48d9dd7fc5d759af0a89e21
Saving successful migration to network...
Saving artifacts...

若看到上述表面表示我們寫的第一隻 Adoption.sol的合約已經成功被部署到鏈上!

為Smart Contract寫測試

我們可以用javascript或solidity寫測試,
不過本範例用solidity來寫。


在 /test 目錄下創建 TestAdoption.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
pragma solidity ^0.4.11;

// 引用truffle內建的檔案
// Assert.sol用來做Unit test (判斷function input ?= output)
import "truffle/Assert.sol";
// 當測試執行,truffle會在testrpc中測試該合約,
// 該DeployedAddresses用來取得被部署合約的address
import "truffle/DeployedAddresses.sol";
// 要被測試的合約
import "../contracts/Adoption.sol";

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

// Testing The adopt() Function

function testUserCanAdoptPet() {
// 呼叫Adoption中的adopt方法
uint returnedId = adoption.adopt(8);

uint expected = 8;

// 判斷returnID 是否等於 expected的值
Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
}

// Testing Retrieval of a Single Pet's Owner

function testGetAdopterAddressByPetId() {
// 透過this取得目前合約的地址
address expected = this;

address adopter = adoption.adopters(8);

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

// Testing Retrieval of All Pet Owners

function testGetAdopterAddressByPetIdInArray() {
address expected = this;

address[16] memory adopters = adoption.getAdopters();

Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
}

}

執行 truffle test 若看到以下畫面表示test通過

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Using network 'development'.

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


TestAdoption
✓ testUserCanAdoptPet (91ms)
✓ testGetAdopterAddressByPetId (70ms)
✓ testGetAdopterAddressByPetIdInArray (89ms)


3 passing (670ms)

使用UI和Smart Contract互動

當解開truffle box的pet-shop,可以看到在 /src目錄底下
會有已經預設好的UI檔供練習用。

使用Web3.js初始化前端環境

Web3.js為用來和ethereum溝通的javascript library.
(而練習專案前端是使用jQuery)

/src/js/app.js檔案的內容改為以下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
App = {
web3Provider: null,
contracts: {},


init: function() {
// 載入寵物的資料
$.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() {
// 初始化web3.js並且設置provider連接testrpc

// 如果偵測到有metamask注入在瀏覽器的web3 instance
if (typeof web3 !== 'undefined') {
App.web3Provider = web3.currentProvider;
// 將currentProvider = metamask
//
web3 = new Web3(web3.currentProvider);
} else {
// 若沒有metamask or mist等等之類的,
// 那就用自己開啟testrpc當成是provider
App.web3Provider = new web3.providers.HttpProvider('http://localhost:8545');
web3 = new Web3(App.web3Provider);
}

return App.initContract();
},

initContract: function() {

$.getJSON('Adoption.json', function(data) {

// 取得contract的artifact(Adoption.json)
// 例如contract address, ABI (Application Binary Interface):
// 即如何使用contract的變數、函式等等
var AdoptionArtifact = data;

// truffle提供`truffle-contract`來幫助我們監聽已經被migrate的contract
// 並把contract的artifact傳給truffle-contract',好讓我們可以跟合約溝通
App.contracts.Adoption = TruffleContract(AdoptionArtifact);

// 為contract設置Provider(我們是用metamask)
App.contracts.Adoption.setProvider(App.web3Provider);

// 從Adpotion.json的合約資料中,判斷寵物的是否已被認養並做標示
return App.markAdopted();
});

return App.bindEvents();
},

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

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

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

var adoptionInstance;

// 使用web3來取得user's accounts
// 這時metamask會跳出交易訊息出來
web3.eth.getAccounts(function(error, accounts) {
if (error) {
console.log(error);
}

// 選擇第一個accounts作為我們的用戶
var account = accounts[0];

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

// 在這裡要執行會花費gas的transaction
// 取得認養用戶的account,以及點選欲認養的petId
return adoptionInstance.adopt(petId, {from: account});
}).then(function(result) {
// 若回傳結果正常執行markAdopted刷新UI上寵物認養中的狀態
return App.markAdopted();
}).catch(function(err) {
console.log(err.message);
});
});
},

markAdopted: function(adopters, account) {
var adoptionInstance;

App.contracts.Adoption.deployed().then(function(instance) {
// 取得Adpotion合約的內容
adoptionInstance = instance;

// 呼叫合約中的getAdopters方法
// 利用`call`可以直接讀取Blockchain上的資料,不用花費ether(gas)
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('Pending...').attr('disabled', true);
}
}
}).catch(function(err) {
console.log(err.message);
});
}
};

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

在Chrome上和Dapp互動

安裝 metamask的擴充套件

長這樣:
petshop_1

由於我們要測試自己的Dapp,點選該圖左上角切換到自己開啟testrpc:8545的私有鏈

因為是初次登入,點選I forgot my password

將一開始開啟testrpc 產生的 Mnemonic(數組變數名稱)貼到wallet seed.
Mnemonic:

1
spider level team helmet shaft clarify abuse recipe stem ankle angry fee

(Warning:你的testrpc產生的Mnemonic變數組會跟上面的不一樣)

petshop_2

設置自己的新密碼,確認後點選OK

進入後就會看到自己的第一筆account資訊了

petshop_3

若有顯示帳戶表示成功與testrpc連接
(原本testrpc是預設100 ether,顯示出來不是100 ether的原因是因為
部署合約到自己的private blockchain也需要費用)

安裝和設置lite-server啟動Dapp

lite-server已經包含在pet-shop的box內

bs-config.json檔案中

1
2
3
4
5
{
"server": {
"baseDir": ["./src", "./build/contracts"]
}
}

該設置告訴server要執行的基底目錄在哪。

./src:前端的內容
./build/contracts:放置合約的內容

在script檔中

1
2
3
4
"scripts": {
"dev": "lite-server",
"test": "echo \"Error: no test specified\" && exit 1"
},

設置 "dev" : lite-server"
方便我們可以直接在terminal下執行 npm run dev 來運行Dapp

運行成功後的樣子

petshop_4


若要進行認養:點選 Adopt按鈕

petshop_5

點選Sumit後即完成認養的動作,並且把該認養的資訊透過合約掛到testrpc的鏈上,並在該寵物上狀態改成 Pending... (已認養)

petshop_6

延伸