@Rays
2018-03-25T11:50:34.000000Z
字数 10691
阅读 2764
区块链
摘要: 本文以宠物商店为例,给出了一个在以太坊上构建去中心化区块链应用的教程。教程中使用了Solidity语言和Truffle框架,详细说明了智能合约的编写、编译、模拟部署和测试过程,并介绍了如何使用Truffle Box构建应用的UI。
作者: Junpei Shimotsu
正文:
基于区块链构建的去中心化应用(Dapp,Distributed application)引人关注,因为这样的应用并非集中于某个特定的管理者。
下面,我们将生成一个基本的去中心化应用,以此实际了解此类应用的机制。
遵循Truffle指南中提供的“以太坊宠物商店”,我将在Web上实际构建一个宠物商店去中心化应用。基于Truffle开发框架,我使用了一种称为“Solidity”的语言编写智能合约。
项目完工的效果如下图。如果用户点击了心仪宠物所对应的“adopt”按钮,应用将启动MetaMask检查所展示的宠物的数量和费用、创建交易并使用ETH支付。
我们可以看到,该应用不同于一般的电子商务网站,在购买时不必输入个人信息或信用卡信息。此外,购买数据以交易形式记录在以太坊区块链中,因此不会被篡改。
实现去中心化应用的具体流程如下:
首先准备使用node和npm的环境。对于Ubuntu操作系统,安装Node.js 8.x的操作命令为:
$ apt-get update
$ curl -sL https://deb.nodesource.com/setup_8.x | bash -
$ apt-get install -y nodejs
第一步,我们需要安装Truffle。
$ npm install -g truffle
Truffle是一个以太坊开发框架。对于智能合约开发,Truffle是一种非常有用的框架,可以高效地实现源代码的编译和部署。
第二步,建立一个名为“pet-shop-tutorial”的文件夹作为工作目录。通常使用命令truffle init
初始化工作目录,并创建一个空目录。但是在本文给出的教程中,是在一个预先准备好的项目“Truffle Box”手工中创建了这个目录:
$ mkdir pet-shop-tutorial
$ cd pet-shop-tutorial
$ truffle unbox pet-shop
第三步,在“pet-shop-tutorial”目录中建立如下图所列的文件和目录:
实际使用的目录和文件如下:
contracts目录:包含描述智能合约的Solidity源文件;
migrations目录:模拟部署(migration)系统,用于部署智能合约过程中。
test目录:目录中为测试文件,使用JavaScript和Solidity编写;
truffle.js:Truffle配置文件。
其中,Solidity是一种描述以太坊智能合约的编程语言。
下面,我将使用Solidity编写智能合约。在所创建的contracts目录中,建立一个名为“Adoption.sol”的文件,文件内容如下:
pragma solidity ^0.4.4;
contract Adoption {
address[16] public adopters;
function adopt(uint petId) public returns(uint) {
require(petId >= 0 && petId <= 15);
adopters[petId] = msg.sender;
return petId;
}
function getAdopters() public returns (address[16]) {
return adopters;
}
}
下面我依次介绍代码的各个部分:
pragma solidity ^0.4.4;
该语句指定了Solidity编译器的版本信息。此外,Solidity与JavaScript类似,需在代码行结尾处添加分号“;”。
contract Adoption {
・・・
}
这里定义了一个名为Adoption
的合约,并在其中实现合约。
address[16] public adopters;
这句话定义了一个名为adopters
的状态变量。鉴于Solidity是一种静态语言,因此变量必须要定义类型。除了string、uint等通用数据类型之外,Solidity还具有一种特有的数据类型address
。address
中包含账户的地址。
这里,定义了一个名为adopters
的address
数组,该变量具有16个地址。
此外,在adopters
变量前指定了public
,即任何人都可以访问合约。
在定义了以上变量之后,开始定义合约的方法。
function adopt(uint petId) public returns (uint) {
require(petId >= 0 && petId <= 15);
adopters[petId] = msg.sender;
return petId;
}
根据adopters
数组的长度,将整数类型变量petId
的值设为0到15(数组的索引值从0开始)。
代码中使用require()
函数设置petId的值为0到15。
msg.sender
表示执行函数者(或智能合约)的地址。
这样,语句adopters [petId] = msg.sender;
将执行函数者的地址添加到adopters
数组。
返回值在petId
中。
上面定义的adopt()
函数返回一个地址,因为petId
是adopters
数组的键值。
但是,鉴于每次重加载都需要做16次API调用,我使用下面定义的getAdopters()
函数返回整个adopters
数组:
function getAdopters() public returns (address[16]) {
return adopters;
}
鉴于变量adopters
已经定义,函数可以仅指定数据类型,并将返回值返回。
至此,我完成了对智能合约的描述。
总结一下,我创建了如下的Adoption
合约:“共有16种宠物。如果用户想领养一只宠物,就将用户地址和该宠物ID绑定在一起”。
下面,我们将继续编译智能合约,并模拟部署。
编译将以编程语言编写的源代码转译为机器可直接执行的机器语言。换句话说,本例中就是将Solidity语言编写的代码转换为EVM(以太坊虚拟机,Ethereum Virtual Machine)可执行的字节码。
在包含去中心化应用的目录中,使用终端等方式加载Truffle Develop:
$ truffle develop
然后,在启动的Truffle开发控制台上,输入compile
命令:
$ truffle(develop)> compile
如果输出如下,表明编译成功。
Compiling ./contracts/Adoption.sol...
Compiling ./contracts/Migrations.sol...
Writing artifacts to ./build/contracts
尽管其中可能存在一些公开可见的警告,但是继续编译是没有问题的。下面请一直保持Truffle开发控制台的运行。
模拟部署(Migration)类似于“移动”,就是将已有系统或类似系统迁移到一个新的平台上。
在本例中,模拟部署文件完成将所创建的Adoption合约部署到以太坊区块链网络上。
如果查看migrations目录的内容,其中已经存在一个名为“1_initial_migration.js”的JavaScript部署文件。该文件将Migrations.sol部署到contracts目录中,并管理它,这使得一系列的智能合约可以正确地迁移。
下面在migrations目录中创建一个名为“2_deploy_contracts.js”的部署文件。在部署文件中写入如下内容:
const Adoption = artifacts.require("Adoption");
module.exports = (deployer) => {
deployer.deploy(Adoption);
};
在前面打开的Truffle开发控制台上,运行migrate
命令:
$ truffle(develop)> migrate
如果生成如下输出,表明模拟部署成功完成:
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0xa1f5bc4affc464999763799648db42acae31772140af652d27f921ee11cb330d
Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying Adoption...
... 0xe46e604dce4322e0492be99b5d3744468e20f8a233e3da551dd42ad9272839b9
Adoption: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...
With that, you can create a Smart Contract, compile it, and deploy it in the test block chain of the local environment. Next, let's test whether this is actually done correctly.
测试智能合约是非常重要的一步。这是因为智能合约中的设计错误和缺陷将与用户的代币(资产)直接相关,可导致对用户利益的严重损害。
智能合约测试主要分为手工测试和自动测试。下面分别介绍这两种测试。
手工测试使用Ganache等本地开发环境工具,检查应用的运行情况。这易于理解,因为这些工具实际指向GUI中的交易。
本文将跳过对手工测试的介绍。下面介绍自动测试。
在Truffle中,可使用JavaScript或Solidity描述智能合约的自动测试。在本例中,我采用Solidity编写。
在所创建的test目录中,创建一个名为“TestAdoption.sol”的文件,其中的内容如下:
pragma solidity ^0.4.11;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";
contract TestAdoption {
Adoption adoption = Adoption(DeployedAddresses.Adoption());
function testUserCanAdoptPet() {
uint returnedId = adoption.adopt(8);
uint expected = 8;
Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
}
function testGetAdopterAddressByPetId() {
address expected = this;
address adopter = adoption.adopters(8);
Assert.equal(adopter, expected, "Owner of pet ID 8 should be recorded.");
}
function testGetAdopterAddressByPetIdInArray() {
address expected = this;
address[16] memory adopters = adoption.getAdopters();
Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
}
}
文件内容略长。我将分解该文件做介绍。
pragma solidity ^0.4.11;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";
contract TestAdoption {
Adoption adoption = Adoption(DeployedAddresses.Adoption());
/*
*此处添加函数体。
*/
}
首先,我导入了如下三个合约:
创建一个名为“TestAdoption”的合约,并定义变量adoption
。adoption
包含DeployedAddresses
。
在下面给出的TestAdoption合约中,我定义了用于测试的函数:
function testUserCanAdoptPet() {
uint returnedId = adoption.adopt(8);
uint expected = 8;
Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
}
该代码测试Adoption合约中定义的adopt()
函数。如果adopt()
函数功能正常,它将返回与参数具有同一数值的petId
(即返回值)。
此处将值为8的petId
置入adopt()
函数,并使用Assert.equal()
函数确保与petId
返回值匹配。
function testGetAdopterAddressByPetId() {
address expected = this;
address adopter = adoption.adopters(8);
Assert.equal(adopter, expected, "Owner of pet ID 8 should be recorded.");
}
需要测试的是petId
是否关联了正确的所有者地址。代码测试了宠物ID是8的所有者的地址是否正确。
顺便提及,变量this
表示的是当前合约的地址。
function testGetAdopterAddressByPetIdInArray() {
address expected = this;
address[16] memory adopters = adoption.getAdopters();
Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
}
最后,检查具有所有地址的数组adopters
是否被正确返回。
属性memory
并未保存在合约的“存储”中,这意味着它是一个临时记录的值。
现在可以编写测试。我将使用Truffle Develop返回测试文件。
$ truffle(develop)> test
如果输出如下,表明测试成功。
Using network 'develop'.
Compiling ./contracts/Adoption.sol...
Compiling ./test/TestAdoption.sol...
Compiling truffle/Assert.sol...
Compiling truffle/DeployedAddresses.sol...
TestAdoption
✓ testUserCanAdoptPet (133ms)
✓ testGetAdopterAddressByPetId (112ms)
✓ testGetAdopterAddressByPetIdInArray (196ms)
3 passing (1s)
目前为止,我们已经完成了智能合约的创建,模拟部署在本地环境的测试区块链中,并测试其是否正常工作。
下面,我们将创建用户界面,在浏览器中实际查看宠物商店。
基本结构已经由Truffle Box构建,我们只需在以太坊中添加特性函数。
应用的前端部分位于src目录中,我们需要编辑其中的/src/js/app.js文件。
下面给出App对象的声明,我随后在①到④处添加代码。
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() {
/*
*①在此处添加代码。
*/
return App.initContract();
},
initContract: function() {
/*
* ②在此处添加代码。
*/
return App.bindEvents();
},
bindEvents: function() {
$(document).on('click', '.btn-adopt', App.handleAdopt);
},
markAdopted: function(adopters, account) {
/*
* ③在此处添加代码。
*/
},
handleAdopt: function(event) {
event.preventDefault();
var petId = parseInt($(event.target).data('id'));
/*
* ④在此处添加代码。
*/
}
};
$(function() {
$(window).load(function() {
App.init();
});
});
下面分别介绍在① ~ ④处添加的源代码。
① web3实例化
if (typeof web3 !== 'undefined') {
App.web3Provider = web3.currentProvider;
} else {
App.web3Provider = new Web3.providers.HttpProvider('http://localhost:9545');
}
web3 = new Web3(App.web3Provider);
首先,确保web3的实例是“活动”的。如果它是“活动”的,那么使用所创建应用的web3对象替换它。如果它并非“活动”的,那么在本地开发环境中创建web3对象。
② 合约实例化
鉴于我们现在可通过web3与“以太坊网络”建立通讯,这时需要实例化所创建的“智能合约”。为实现合约的实例化,我们需要将合约的具体位置以及工作方式告知web3。
$.getJSON('Adoption.json', function(data) {
var AdoptionArtifact = data;
App.contracts.Adoption = TruffleContract(AdoptionArtifact);
App.contracts.Adoption.setProvider(App.web3Provider);
return App.markAdopted();
});
Truffle提供了一个有用的软件库,称为“truffle-contract”。该软件库作用于web3上,简化了与“智能合约”的联系。例如,truffle-contract实现模拟部署期间合约信息的同步,无需手工更改部署地址。
Artifact文件提供了部署地址和ABI(应用二进制接口,Application Binary Interface)信息。
ABI表示了合约接口上的信息,即变量、函数、参数等。
在TruffleContract()
函数中插入Artifact,并实例化合约。然后设置由web3实例化所创建的App.web3Provider
到合约中。
此外,如果先前已经选定了宠物,那么这时需要调用markAdopted()
。每次智能合约数据发生改变时,都有必要对UI进行更新。更新UI定义为在③处给出的各种“函数”。
③ UI更新
下面的代码确保宠物状态保持更改,并且UI得到了更新。
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);
});
代码首先在所部署的Adoption合约实例上调用getAdopters()
函数。call()
函数并不更改区块链的状态,它只是读取数据,因此这里无需支付GAS。
此后,代码检查是否每个petId
都绑定了一个地址。如果地址存在,就将按钮状态改为“Success”,这样用户不能再次按下按钮。
④ 操作adopt()函数
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);
});
});
在本例中,确认web3使用账号无误后,就实际进行交易处理。交易执行通过adopt()
函数完成,输入参数为一个包含petId
和账号地址的对象。
之后,使用在③中定义的markAdopted()
函数,将交易结果在UI上以新数据显示。
一旦万事俱备,现在就可以在浏览器中查看上面创建的去中心化应用。
这里需要预先做一些安装工作,因为我们将使用Chorome的一个扩展MetaMask。账号将通过下面给出的“钱包私钥”(Wallet Seed),使用Truffle Develop的账号。在执行Truffle Develop时,会显示该私钥(它是通用私钥)。
candy maple cake sugar pudding cream honey rich smooth crumble sweet treat
如果使用MetaMask,可以通过菜单项“Lock”访问如下的屏幕。
为了将MetaMask连接到Truffle Develop创建的区块链,要将左上位置的“Main Network”改为“Custom RPC”,“Truffle Develop”更改为“http://localhost:9545”,并将显示从“Main Network”更改为“Private Network”。
账号由上面给出的私钥生成,其中应该会具有少许的100ETH,它们来自于合约部署中消费的GAS量。
一旦对MetaMask做了如上设置,就可以在终端等处输入下面的命令,启动本地Web服务器(鉴于已经bs-config.json和package.json已经创建,还可以使用lite-server软件库)。
$ npm run dev
这样,在浏览器中就能显示如下的去中心化应用。
一旦点击心仪宠物的“adopt”按钮,交易就通过MetaMask发出,使用者可以用ETH支付宠物购买。
鉴于本文只是给出一个教程,因此内容主要聚焦于使用Truffle Box在以太坊中实现的去中心化应用的一些特性。即便读者并不具备详细的以太坊区块链知识,只要能按教程实际动手操作,就可理解去中心化应用的工作机制。
查看英文原文: How to Make a Dapp of a pet shop using Solidity and Truffle!