1. 参考资料

http://truffleframework.com/tutorials/pet-shop

这是一个使用以太坊开发的完整的去中心化应用(Dapp),出自tuffle suite。

Pete有一个宠物店,养了16只宠物,他想开发一个去中心化应用,让大家来领养宠物。

应用效果如下:

bef467b05914?from=timeline

屏幕快照 2018-02-09 上午11.00.25 (2).png

2. 环境搭建

安装nodejs

安装truffle

安装ganache-cli

安装MetaMask

3. 创建项目

感谢truffle unbox,你不必从头开始创建项目。

truffle unbox pet-shop

这一步需要稍等一会,truffle会自动下载pet-shop项目,项目目录如下:

bef467b05914?from=timeline

屏幕快照 2018-02-09 上午11.05.31.png

contracts 存放智能合约;

migrations 存放部署脚本;

test 存放测试用例;

truffle.js 配置文件,例如区块链地址和端口;

4. 编写智能合约

在contracts目录下,添加合约文件Adoption.sol

pragma solidity ^0.4.17;

contract Adoption {

address[16] public adopters; // 保存领养者的地址

// 领养宠物

function adopt(uint petId) public returns (uint) {

require(petId >= 0 && petId <= 15); // 确保id在数组长度内

adopters[petId] = msg.sender; // 保存调用这地址

return petId;

}

// 返回领养者

function getAdopters() public view returns (address[16]) {

return adopters;

}

}

5. 编译智能合约

truffle compile

6. 启动以太坊客户端

yangang:~ yangang$ ganache-cli

Ganache CLI v6.0.3 (ganache-core: 2.0.2)

Available Accounts

==================

(0) 0xebbc700bb15e3aa969f9d8ece9090aeb262cbf6e

(1) 0x940ead554300f8b0a40530f6f77b138d24b9359b

(2) 0xf47f7cd72e1377a6220bf512f5690d1027313dee

(3) 0x1c22e4df38807b47f26ad1f25503c23947e9e7e1

(4) 0xb8cf54c3ebdf06ae887222c23930bad155453891

(5) 0xc8232b9bc251ab9c393071747616e0a831b143ec

(6) 0x01c05db44cfbb7bf7b5671cc0e197e72607f0166

(7) 0xa08e0b2855556167afd44c63c6429c8372a6cf01

(8) 0x18ca58212d87cca4842471475ada5d78183d7fdb

(9) 0xcb465d0d7a7b067c7b0c8272078710bc4937d134

Private Keys

==================

(0) 0eb629b963b89ef28272b68437cf65f52a92a963188fa41ed86578158388f0f5

(1) a9c3f1332bd3d7b42b45c6aec59f2d966478c831ca4e71e0ebd251d248cfecf4

(2) b50d1f99eca8a322c2968265a48017be275fcf1f8e9f6ff382b9d1d4c4495443

(3) 6a14cbcb204156880cf663557cf0f919aa297ddf9bed83afc7c4bfe2a1aa5aef

(4) af1c52412f92380d7536324106875e0f30106fa9e50da3e0336eef17779da3ca

(5) 13d45f5707aab605bc42c411aa455c77b7346371609b3606bf001a6359a3a37c

(6) 3763c2a208e5c690a187e8228344a7ff6f25494ed5af907f0c14660074f9484d

(7) 230a4a6beb106e20f5204b14a3ce293e240ffcdf58c8c349ea297af446724c43

(8) 80410ba728a87b877921a8614cd802e5f06c638bf38d3df2b774bc2ecb969ea8

(9) 0f0b78f8b517f065e8a57d4ddcfbc6b3dcfa480f70a51bdd260d7cf530a543be

HD Wallet

==================

Mnemonic: powder omit enforce twelve student rigid large caution eternal sibling pepper human

Base HD Path: m/44'/60'/0'/0/{account_index}

Listening on localhost:8545

注意这里的Mnemonic和Accounts,后面MetaMask需要用到。

7. 部署合约(migrate)

在migrations目录下,创建一个部署脚本2_deploy_contracts.js:

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

module.exports = function(deployer) {

deployer.deploy(Adoption);

};

注意这里的写法,artifacts.require("Adoption")的参数为合约名称。

此外,为什么部署脚本要以数字开头呢?这是truffle部署时用的ID。

接下来执行部署命令:

yangang:migrations yangang$ truffle migrate --reset

Using network 'development'.

Running migration: 1_initial_migration.js

Replacing Migrations...

... 0xb61538c615d6268399b938d9d143ae6b941f5042047a352f282000e28d477968

Migrations: 0x359208970726992a5c8fc91fd73f4bd5e85ec797

Saving successful migration to network...

... 0x2d647da26ab9cec9023f9f6cf472a88d4b6c80589c5411dcd0625008f628c46c

Saving artifacts...

Running migration: 2_deploy_contracts.js

Replacing Adoption...

... 0x8794814a4af225df5034f55ac34b9ec93fb803286b5b12c152b151881b9d389b

Adoption: 0x3dd3623e515a405921f0479205cfa41596efe755

Saving successful migration to network...

... 0x58fac81009ebbe60969e4f13ff69bc63db5649abd1161e36a7a15613969442b4

Saving artifacts..

使用--reset来强制重编译并部署所有合约。

8. 测试合约

在test目录下,新建测试案例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.");

}

// Testing retrieval of all pet owners

function testGetAdopterAddressByPetIdInArray() public {

// Expected owner is this contract

address expected = this;

// Store adopters in memory rather than contract's storage

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

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

}

}

接下来,运行测试案例:

yangang:test yangang$ truffle test

Using network 'development'.

Compiling ./contracts/Adoption.sol...

Compiling ./test/TestAdoption.sol...

Compiling truffle/Assert.sol...

Compiling truffle/DeployedAddresses.sol...

TestAdoption

✓ testUserCanAdoptPet (98ms)

✓ testGetAdopterAddressByPetId (62ms)

✓ testGetAdopterAddressByPetIdInArray (90ms)

3 passing (781ms)

9. 创建UI和智能合约交互

在pet-shop项目中,已经包含了前端代码,在src目录下。

我们主要是修改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() {

/*

* Replace me...

*/

// 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:8545');

}

web3 = new Web3(App.web3Provider);

return App.initContract();

},

initContract: function() {

/*

* Replace me...

*/

// 加载Adoption.json,保存了Adoption的ABI(接口说明)信息及部署后的网络(地址)信息,它在编译合约的时候生成ABI,在部署的时候追加网络信息

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

// 用Adoption.json数据创建一个可交互的TruffleContract合约实例。

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) {

/*

* Replace me...

*/

var adoptionInstance;

App.contracts.Adoption.deployed().then(function(instance) {

adoptionInstance = instance;

// 调用合约的getAdopters(), 用call读取信息不用消耗gas

return adoptionInstance.getAdopters.call();

}).then(function(adopters) {

for (i = 0; i < adopters.length; i++) {

if (adopters[i] !== '0x0000000000000000000000000000000000000000') {

console.log('hahahah');

console.log(adopters[i]);

$('.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'));

/*

* Replace me...

*/

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;

// 发送交易领养宠物

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();

});

});

9.1 实例化合约

Adoption部署成功后,在build/contracts目录下会生成Adoption.json文件,记录合约部署信息;

initContract函数读取Adoption.json文件,创建一个合约实例contracts.Adoption.

markAdopted函数使用合约实例,调用getAdopters()方法,获取所有领养者列表,回到页面上,把对应宠物的领养按钮置灰,改名为Success。

bef467b05914?from=timeline

屏幕快照 2018-02-09 上午11.41.58.png

9.2 处理领养

handleAdopt函数处理领养按钮,首先获取区块链用户账号,然后调用合约实例的adopt(petId)方法,领养宠物。

10. 在浏览器中访问区块链

MetaMask是一款插件形式的以太坊客户端,我使用的是firefox浏览器。

MetaMask登录界面:

bef467b05914?from=timeline

屏幕快照 2018-02-09 下午1.33.30.png

选择可供连接的网络:

bef467b05914?from=timeline

屏幕快照 2018-02-09 下午1.35.47.png

因为ganache-cli的监听端口为8545,所以我们选择“Localhost 8545”:

点击“Restore from seed phrase”,

bef467b05914?from=timeline

屏幕快照 2018-02-09 下午1.40.04.png

注意,"Wallet Seed"框中填入ganache-cli的Mnemonic:

HD Wallet

==================

Mnemonic: powder omit enforce twelve student rigid large caution eternal sibling pepper human

设置密码,确认即可,下次登录直接使用该密码。

bef467b05914?from=timeline

屏幕快照 2018-02-09 下午1.44.04.png

登录后,能看到用户及相关交易信息,默认用户名为Account 1,查看该用户地址:

0xebBc700bb15e3aA969f9D8ECe9090aEb262CBf6e

与ganache-cli的用户列表相比:

Available Accounts

==================

(0) 0xebbc700bb15e3aa969f9d8ece9090aeb262cbf6e

(1) 0x940ead554300f8b0a40530f6f77b138d24b9359b

(2) 0xf47f7cd72e1377a6220bf512f5690d1027313dee

(3) 0x1c22e4df38807b47f26ad1f25503c23947e9e7e1

(4) 0xb8cf54c3ebdf06ae887222c23930bad155453891

(5) 0xc8232b9bc251ab9c393071747616e0a831b143ec

(6) 0x01c05db44cfbb7bf7b5671cc0e197e72607f0166

(7) 0xa08e0b2855556167afd44c63c6429c8372a6cf01

(8) 0x18ca58212d87cca4842471475ada5d78183d7fdb

(9) 0xcb465d0d7a7b067c7b0c8272078710bc4937d134

发现什么了吗?其实就是第一个用户。

11. 安装和配置lite-server

接下来需要本地的web服务器提供服务的访问,Truffle Box pet-shop里面,提供了一个lite-server可以直接使用,我们看看它是如何工作的。

bs-config.json指示了lite-server的工作目录:

{

"server": {

"baseDir": ["./src", "./build/contracts"]

}

}

与此同时,在pckage.json的scriptes中添加了dev命令:

"scripts": {

"dev": "lite-server",

"test": "echo \"Error: no test specified\" && exit 1"

},

这样,当运行npm run dev的时候,就会启动lite-server.

12. 启动服务

npm run dev

会自动打开浏览器,显示我们的dapp:

bef467b05914?from=timeline

屏幕快照 2018-02-09 下午1.54.01 (2).png

现在领养第四只宠物Melissa看看,当我们点击Adopt时,MetaMask会提示我们交易的确认:

bef467b05914?from=timeline

屏幕快照 2018-02-09 下午1.55.21.png

注意:MetaMask和dapp必须在同一个浏览器中哦,否则不会提示的。

点击submit确认后,如图所示,交易29执行成功了。

bef467b05914?from=timeline

屏幕快照 2018-02-09 下午1.57.33.png

刷新页面,我们可以发现,Melissa已经被领养了。

bef467b05914?from=timeline

屏幕快照 2018-02-09 下午1.58.57 (2).png

13. 总结

到此为止,我们已经完成了一个简单的dapp,恭喜自己吧。

后续还有其他问题,例如如何控制用户权限,如何处理并发领养等等。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐