[关闭]
@Rays 2018-03-25T03:50:34.000000Z 字数 10691 阅读 2667

使用Solidity和Truffle创建去中心化宠物商店应用

区块链


摘要: 本文以宠物商店为例,给出了一个在以太坊上构建去中心化区块链应用的教程。教程中使用了Solidity语言和Truffle框架,详细说明了智能合约的编写、编译、模拟部署和测试过程,并介绍了如何使用Truffle Box构建应用的UI。

作者: Junpei Shimotsu

正文:

基于区块链构建的去中心化应用(Dapp,Distributed application)引人关注,因为这样的应用并非集中于某个特定的管理者。

下面,我们将生成一个基本的去中心化应用,以此实际了解此类应用的机制。

遵循Truffle指南中提供的“以太坊宠物商店”,我将在Web上实际构建一个宠物商店去中心化应用。基于Truffle开发框架,我使用了一种称为“Solidity”的语言编写智能合约。

项目完工的效果如下图。如果用户点击了心仪宠物所对应的“adopt”按钮,应用将启动MetaMask检查所展示的宠物的数量和费用、创建交易并使用ETH支付。

我们可以看到,该应用不同于一般的电子商务网站,在购买时不必输入个人信息或信用卡信息。此外,购买数据以交易形式记录在以太坊区块链中,因此不会被篡改。

实现去中心化应用的具体流程如下:

  1. 设置开发环境;
  2. 使用Truffle Box创建Truffle项目;
  3. 描述智能合约;
  4. 编译并模拟部署(Migrating)智能合约;
  5. 测试智能合约;
  6. 建立附着于智能合约之上的UI;
  7. 在浏览器中使用去中心化应用。

创建Truffle项目

首先准备使用node和npm的环境。对于Ubuntu操作系统,安装Node.js 8.x的操作命令为:

  1. $ apt-get update
  2. $ curl -sL https://deb.nodesource.com/setup_8.x | bash -
  3. $ apt-get install -y nodejs

第一步,我们需要安装Truffle。

  1. $ npm install -g truffle

Truffle是一个以太坊开发框架。对于智能合约开发,Truffle是一种非常有用的框架,可以高效地实现源代码的编译和部署。

第二步,建立一个名为“pet-shop-tutorial”的文件夹作为工作目录。通常使用命令truffle init初始化工作目录,并创建一个空目录。但是在本文给出的教程中,是在一个预先准备好的项目“Truffle Box”手工中创建了这个目录:

  1. $ mkdir pet-shop-tutorial
  2. $ cd pet-shop-tutorial
  3. $ truffle unbox pet-shop

第三步,在“pet-shop-tutorial”目录中建立如下图所列的文件和目录:

实际使用的目录和文件如下:

其中,Solidity是一种描述以太坊智能合约的编程语言。

描述智能合约

下面,我将使用Solidity编写智能合约。在所创建的contracts目录中,建立一个名为“Adoption.sol”的文件,文件内容如下:

  1. pragma solidity ^0.4.4;
  2. contract Adoption {
  3.  address[16] public adopters;
  4.  function adopt(uint petId) public returns(uint) {
  5.   require(petId >= 0 && petId <= 15);
  6.   adopters[petId] = msg.sender;
  7.   return petId;
  8.  }
  9.  function getAdopters() public returns (address[16]) {
  10.   return adopters;
  11.  } 
  12. }

下面我依次介绍代码的各个部分:

  1. pragma solidity ^0.4.4;

该语句指定了Solidity编译器的版本信息。此外,Solidity与JavaScript类似,需在代码行结尾处添加分号“;”。

  1. contract Adoption {
  2. ・・・
  3. }

这里定义了一个名为Adoption的合约,并在其中实现合约。

  1. address[16] public adopters;

这句话定义了一个名为adopters的状态变量。鉴于Solidity是一种静态语言,因此变量必须要定义类型。除了string、uint等通用数据类型之外,Solidity还具有一种特有的数据类型addressaddress中包含账户的地址。

这里,定义了一个名为adoptersaddress数组,该变量具有16个地址。

此外,在adopters变量前指定了public,即任何人都可以访问合约。

在定义了以上变量之后,开始定义合约的方法。

  1. function adopt(uint petId) public returns (uint) {
  2.  require(petId >= 0 && petId <= 15);
  3.  adopters[petId] = msg.sender;
  4.  return petId;
  5. }

根据adopters数组的长度,将整数类型变量petId的值设为0到15(数组的索引值从0开始)。

代码中使用require()函数设置petId的值为0到15。

msg.sender表示执行函数者(或智能合约)的地址。

这样,语句adopters [petId] = msg.sender;将执行函数者的地址添加到adopters数组。

返回值在petId中。

上面定义的adopt()函数返回一个地址,因为petIdadopters数组的键值。

但是,鉴于每次重加载都需要做16次API调用,我使用下面定义的getAdopters()函数返回整个adopters数组:

  1. function getAdopters() public returns (address[16]) {
  2.  return adopters;
  3. }

鉴于变量adopters已经定义,函数可以仅指定数据类型,并将返回值返回。

至此,我完成了对智能合约的描述。

总结一下,我创建了如下的Adoption合约:“共有16种宠物。如果用户想领养一只宠物,就将用户地址和该宠物ID绑定在一起”。

下面,我们将继续编译智能合约,并模拟部署。

编译

编译将以编程语言编写的源代码转译为机器可直接执行的机器语言。换句话说,本例中就是将Solidity语言编写的代码转换为EVM(以太坊虚拟机,Ethereum Virtual Machine)可执行的字节码。

在包含去中心化应用的目录中,使用终端等方式加载Truffle Develop:

  1. $ truffle develop

然后,在启动的Truffle开发控制台上,输入compile命令:

  1. $ truffle(develop)> compile

如果输出如下,表明编译成功。

  1. Compiling ./contracts/Adoption.sol...
  2. Compiling ./contracts/Migrations.sol...
  3. Writing artifacts to ./build/contracts

尽管其中可能存在一些公开可见的警告,但是继续编译是没有问题的。下面请一直保持Truffle开发控制台的运行。

模拟部署

模拟部署(Migration)类似于“移动”,就是将已有系统或类似系统迁移到一个新的平台上。

在本例中,模拟部署文件完成将所创建的Adoption合约部署到以太坊区块链网络上。

如果查看migrations目录的内容,其中已经存在一个名为“1_initial_migration.js”的JavaScript部署文件。该文件将Migrations.sol部署到contracts目录中,并管理它,这使得一系列的智能合约可以正确地迁移。

下面在migrations目录中创建一个名为“2_deploy_contracts.js”的部署文件。在部署文件中写入如下内容:

  1. const Adoption = artifacts.require("Adoption");
  2. module.exports = (deployer) => {
  3.  deployer.deploy(Adoption);
  4. };

在前面打开的Truffle开发控制台上,运行migrate命令:

  1. $ truffle(develop)> migrate

如果生成如下输出,表明模拟部署成功完成:

  1. Running migration: 1_initial_migration.js
  2. Deploying Migrations...
  3. ... 0xa1f5bc4affc464999763799648db42acae31772140af652d27f921ee11cb330d
  4. Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
  5. Saving successful migration to network...
  6. ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
  7. Saving artifacts...
  8. Running migration: 2_deploy_contracts.js
  9. Deploying Adoption...
  10. ... 0xe46e604dce4322e0492be99b5d3744468e20f8a233e3da551dd42ad9272839b9
  11. Adoption: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
  12. Saving successful migration to network...
  13. ... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
  14. Saving artifacts...
  15. 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”的文件,其中的内容如下:

  1. pragma solidity ^0.4.11;
  2. import "truffle/Assert.sol";
  3. import "truffle/DeployedAddresses.sol";
  4. import "../contracts/Adoption.sol";
  5. contract TestAdoption {
  6.  Adoption adoption = Adoption(DeployedAddresses.Adoption());
  7.  function testUserCanAdoptPet() {
  8.   uint returnedId = adoption.adopt(8);
  9.   uint expected = 8;
  10.   Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
  11.  }
  12.  function testGetAdopterAddressByPetId() {
  13.   address expected = this;
  14.   address adopter = adoption.adopters(8);
  15.   Assert.equal(adopter, expected, "Owner of pet ID 8 should be recorded.");
  16.  }
  17.  function testGetAdopterAddressByPetIdInArray() {
  18.   address expected = this;
  19.   address[16] memory adopters = adoption.getAdopters();
  20.   Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
  21.  }
  22. }

文件内容略长。我将分解该文件做介绍。

  1. pragma solidity ^0.4.11;
  2. import "truffle/Assert.sol";
  3. import "truffle/DeployedAddresses.sol";
  4. import "../contracts/Adoption.sol";
  5. contract TestAdoption {
  6.  Adoption adoption = Adoption(DeployedAddresses.Adoption());
  7.  /*
  8.  *此处添加函数体。
  9.  */
  10. }

首先,我导入了如下三个合约:

创建一个名为“TestAdoption”的合约,并定义变量adoptionadoption包含DeployedAddresses

在下面给出的TestAdoption合约中,我定义了用于测试的函数:

  1. function testUserCanAdoptPet() {
  2.  uint returnedId = adoption.adopt(8);
  3.  uint expected = 8;
  4.  Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
  5. }

该代码测试Adoption合约中定义的adopt()函数。如果adopt()函数功能正常,它将返回与参数具有同一数值的petId(即返回值)。

此处将值为8的petId置入adopt()函数,并使用Assert.equal()函数确保与petId返回值匹配。

  1. function testGetAdopterAddressByPetId() {
  2.  address expected = this;
  3.  address adopter = adoption.adopters(8);
  4.  Assert.equal(adopter, expected, "Owner of pet ID 8 should be recorded.");
  5. }

需要测试的是petId是否关联了正确的所有者地址。代码测试了宠物ID是8的所有者的地址是否正确。

顺便提及,变量this表示的是当前合约的地址。

  1. function testGetAdopterAddressByPetIdInArray() {
  2.  address expected = this;
  3.  address[16] memory adopters = adoption.getAdopters();
  4.  Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
  5. }

最后,检查具有所有地址的数组adopters是否被正确返回。

属性memory并未保存在合约的“存储”中,这意味着它是一个临时记录的值。

现在可以编写测试。我将使用Truffle Develop返回测试文件。

  1. $ truffle(develop)> test

如果输出如下,表明测试成功。

  1. Using network 'develop'.
  2. Compiling ./contracts/Adoption.sol...
  3. Compiling ./test/TestAdoption.sol...
  4. Compiling truffle/Assert.sol...
  5. Compiling truffle/DeployedAddresses.sol...
  6. TestAdoption
  7. testUserCanAdoptPet (133ms)
  8. testGetAdopterAddressByPetId (112ms)
  9. testGetAdopterAddressByPetIdInArray (196ms)
  10. 3 passing (1s)

创建UI

目前为止,我们已经完成了智能合约的创建,模拟部署在本地环境的测试区块链中,并测试其是否正常工作。

下面,我们将创建用户界面,在浏览器中实际查看宠物商店。

基本结构已经由Truffle Box构建,我们只需在以太坊中添加特性函数。

应用的前端部分位于src目录中,我们需要编辑其中的/src/js/app.js文件。

下面给出App对象的声明,我随后在①到④处添加代码。

  1. App = {
  2.  web3Provider: null,
  3.  contracts: {},
  4.  init: function() {
  5.   // Load pets.
  6.   $.getJSON('../pets.json', function(data) {
  7.    var petsRow = $('#petsRow');
  8.    var petTemplate = $('#petTemplate');
  9.    for (i = 0; i < data.length; i ++) {
  10.     petTemplate.find('.panel-title').text(data[i].name);
  11.     petTemplate.find('img').attr('src', data[i].picture);
  12.     petTemplate.find('.pet-breed').text(data[i].breed);
  13.     petTemplate.find('.pet-age').text(data[i].age);
  14.     petTemplate.find('.pet-location').text(data[i].location);
  15.     petTemplate.find('.btn-adopt').attr('data-id', data[i].id);
  16.     petsRow.append(petTemplate.html());
  17.    }
  18.   });
  19.   return App.initWeb3();
  20.  },
  21.  initWeb3: function() {
  22. /*
  23. *①在此处添加代码。
  24. */
  25.   return App.initContract();
  26.  },
  27.  initContract: function() {
  28. /*
  29. * ②在此处添加代码。
  30. */
  31.   return App.bindEvents();
  32.  },
  33.  bindEvents: function() {
  34.   $(document).on('click', '.btn-adopt', App.handleAdopt);
  35.  },
  36.  markAdopted: function(adopters, account) {
  37. /*
  38. * ③在此处添加代码。
  39. */
  40.  },
  41.  handleAdopt: function(event) {
  42.   event.preventDefault();
  43.   var petId = parseInt($(event.target).data('id'));
  44. /*
  45. * ④在此处添加代码。
  46. */
  47.  }
  48. };
  49. $(function() {
  50.  $(window).load(function() {
  51.   App.init();
  52.  });
  53. });

下面分别介绍在① ~ ④处添加的源代码。

① web3实例化

  1. if (typeof web3 !== 'undefined') {
  2.  App.web3Provider = web3.currentProvider;
  3. } else {
  4.  App.web3Provider = new Web3.providers.HttpProvider('http://localhost:9545');
  5. }
  6. web3 = new Web3(App.web3Provider);

首先,确保web3的实例是“活动”的。如果它是“活动”的,那么使用所创建应用的web3对象替换它。如果它并非“活动”的,那么在本地开发环境中创建web3对象。

② 合约实例化

鉴于我们现在可通过web3与“以太坊网络”建立通讯,这时需要实例化所创建的“智能合约”。为实现合约的实例化,我们需要将合约的具体位置以及工作方式告知web3。

  1. $.getJSON('Adoption.json', function(data) {
  2.  var AdoptionArtifact = data;
  3.  App.contracts.Adoption = TruffleContract(AdoptionArtifact);
  4.  App.contracts.Adoption.setProvider(App.web3Provider);
  5.  return App.markAdopted();
  6. });

Truffle提供了一个有用的软件库,称为“truffle-contract”。该软件库作用于web3上,简化了与“智能合约”的联系。例如,truffle-contract实现模拟部署期间合约信息的同步,无需手工更改部署地址。

Artifact文件提供了部署地址和ABI(应用二进制接口,Application Binary Interface)信息。

ABI表示了合约接口上的信息,即变量、函数、参数等。

TruffleContract()函数中插入Artifact,并实例化合约。然后设置由web3实例化所创建的App.web3Provider到合约中。

此外,如果先前已经选定了宠物,那么这时需要调用markAdopted()。每次智能合约数据发生改变时,都有必要对UI进行更新。更新UI定义为在③处给出的各种“函数”。

③ UI更新

下面的代码确保宠物状态保持更改,并且UI得到了更新。

  1. var adoptionInstance;
  2. App.contracts.Adoption.deployed().then(function(instance) {
  3.  adoptionInstance = instance;
  4.  return adoptionInstance.getAdopters.call();
  5. }).then(function(adopters) {
  6.  for (i = 0; i < adopters.length; i++) {
  7.   if (adopters[i] !== '0x0000000000000000000000000000000000000000') {
  8.    $('.panel-pet').eq(i).find('button').text('Success').attr('disabled', true);
  9.   }
  10.  }
  11. }).catch(function(err) {
  12.  console.log(err.message);
  13. });

代码首先在所部署的Adoption合约实例上调用getAdopters()函数。call()函数并不更改区块链的状态,它只是读取数据,因此这里无需支付GAS。

此后,代码检查是否每个petId都绑定了一个地址。如果地址存在,就将按钮状态改为“Success”,这样用户不能再次按下按钮。

④ 操作adopt()函数

  1. var adoptionInstance;
  2. web3.eth.getAccounts(function(error, accounts) {
  3.  if (error) {
  4.   console.log(error);
  5.  }
  6.  var account = accounts[0];
  7.  App.contracts.Adoption.deployed().then(function(instance) {
  8.   adoptionInstance = instance;
  9.   return adoptionInstance.adopt(petId, {from: account});
  10.  }).then(function(result) {
  11.   return App.markAdopted();
  12.  }).catch(function(err) {
  13.   console.log(err.message);
  14.  });
  15. });

在本例中,确认web3使用账号无误后,就实际进行交易处理。交易执行通过adopt()函数完成,输入参数为一个包含petId和账号地址的对象。

之后,使用在③中定义的markAdopted()函数,将交易结果在UI上以新数据显示。

一旦万事俱备,现在就可以在浏览器中查看上面创建的去中心化应用。

完成去中心化应用

这里需要预先做一些安装工作,因为我们将使用Chorome的一个扩展MetaMask。账号将通过下面给出的“钱包私钥”(Wallet Seed),使用Truffle Develop的账号。在执行Truffle Develop时,会显示该私钥(它是通用私钥)。

  1. 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软件库)。

  1. $ npm run dev

这样,在浏览器中就能显示如下的去中心化应用。

一旦点击心仪宠物的“adopt”按钮,交易就通过MetaMask发出,使用者可以用ETH支付宠物购买。

总结

鉴于本文只是给出一个教程,因此内容主要聚焦于使用Truffle Box在以太坊中实现的去中心化应用的一些特性。即便读者并不具备详细的以太坊区块链知识,只要能按教程实际动手操作,就可理解去中心化应用的工作机制。

查看英文原文: How to Make a Dapp of a pet shop using Solidity and Truffle!

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注