truffle 宠物店开发教程

目录结构

默认的 Truffle 目录结构包含以下内容:

contract/:包含我们智能合约的 Solidity 源文件。 这里有一个名为 Migrations.sol 的重要合约,我们稍后会谈到。
migrations/:Truffle 使用迁移系统来处理智能合约部署。 迁移是一种额外的特殊智能合约,用于跟踪更改。
test/:包含我们智能合约的 JavaScript 和 Solidity 测试
truffle-config.js:配置文件


宠物店 Truffle Box 里面有额外的文件和文件夹,但我们暂时不用担心这些。

 

编写智能合约

我们将通过编写充当后端逻辑和存储的智能合约来启动我们的 dapp。

在contracts/ 目录中创建一个名为Adoption.sol 的新文件。

将以下内容添加到文件中:

pragma solidity ^0.5.0;
contract Adoption {
}

注意事项:

所需的最低版本的 Solidity 在合同顶部注明:pragma solidity ^0.5.0;。 pragma 命令表示“只有编译器关心的附加信息”,而插入符号 (^) 表示“指示的版本或更高版本”。
与 JavaScript 或 PHP 一样,语句以分号结尾。

变量设置

Solidity 是一种静态类型语言,这意味着必须定义字符串、整数和数组等数据类型。 Solidity 有一种独特的类型,称为地址。 地址是以太坊地址,存储为 20 字节值。 以太坊区块链上的每个账户和智能合约都有一个地址,并且可以向该地址发送和接收以太币。

在下一行添加以下变量 contract Adoption {

address[16] public adopters;

注意事项:

我们定义了一个变量:采用者。 这是一组以太坊地址。 数组包含一种类型,并且可以具有固定或可变长度。 在这种情况下,类型是地址,长度是 16。

您还会注意到采用者是公开的。 公共变量具有自动 getter 方法,但在数组的情况下,键是必需的,并且只会返回一个值。 稍后,我们将编写一个函数来返回整个数组以在我们的 UI 中使用。

 

你的第一个功能:收养宠物

让我们允许用户提出采用请求。

在我们上面设置的变量声明之后,将以下函数添加到智能合约中。

// Adopting a pet
function adopt(uint petId) public returns (uint) {
  require(petId >= 0 && petId <= 15);

  adopters[petId] = msg.sender;

  return petId;
}

注意事项:

在 Solidity 中,必须指定函数参数和输出的类型。 在这种情况下,我们将接收一个 petId(整数)并返回一个整数。

我们正在检查以确保 petId 在我们的采用者数组的范围内。 Solidity 中的数组从 0 开始索引,因此 ID 值需要在 0 到 15 之间。我们使用 require() 语句来确保 ID 在范围内。

如果 ID 在范围内,然后我们将调用的地址添加到我们的采用者数组中。 调用此函数的人或智能合约的地址由 msg.sender 表示。

最后,我们返回作为确认提供的 petId。

 

Your second function: Retrieving the adopters

如上所述,数组 getter 仅从给定键返回单个值。 我们的 UI 需要更新所有宠物的收养状态,但是进行 16 次 API 调用并不理想。 所以我们的下一步是编写一个函数来返回整个数组。

在我们上面添加的adopt()函数之后,将以下getAdopters()函数添加到智能合约中:

// Retrieving the adopters
function getAdopters() public view returns (address[16] memory) {
  return adopters;
}

注意事项:

由于已经声明了采用者,我们可以简单地返回它。 请务必将返回类型(在本例中为采用者的类型)指定为 address[16] 内存。 memory 给出了变量的数据位置。

函数声明中的 view 关键字表示该函数不会修改合约的状态。 有关视图施加的确切限制的更多信息,请参见此处。https://solidity.readthedocs.io/en/latest/contracts.html#view-functions

Compiling and migrating the smart contract

现在我们已经编写了智能合约,接下来的步骤是编译和迁移它。

Compilation

Solidity 是一种编译语言,这意味着我们需要将 Solidity 编译为字节码,以供以太坊虚拟机 (EVM) 执行。 将其视为将我们人类可读的 Solidity 转换为 EVM 可以理解的东西。

在终端中,确保您位于包含 dapp 的目录的根目录中,然后键入:

truffle compile

You should see output similar to the following:

Compiling your contracts...
===========================
> Compiling ./contracts/Adoption.sol
> Compiling ./contracts/Migrations.sol
> Artifacts written to /Users/cruzmolina/Code/truffle-projects/metacoin/build/contracts
> Compiled successfully using:
   - solc: 0.5.0+commit.1d4f565a.Emscripten.clang

Migration

现在我们已经成功编译了我们的合约,是时候将它们迁移到区块链了!

迁移是一个部署脚本,旨在更改应用程序合约的状态,将其从一种状态移动到另一种状态。 对于第一次迁移,您可能只是部署新代码,但随着时间的推移,其他迁移可能会移动数据或用新的合同替换合同。

您会在 migrations/ 目录中看到一个 JavaScript 文件:1_initial_migration.js。 这处理部署 Migrations.sol 合约以观察后续智能合约迁移,并确保我们将来不会重复迁移未更改的合约。

现在我们准备创建自己的迁移脚本。

在 migrations/ 目录中创建一个名为 2_deploy_contracts.js 的新文件。

将以下内容添加到 2_deploy_contracts.js 文件中:

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

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

在我们将合约迁移到区块链之前,我们需要运行一个区块链。 在本教程中,我们将使用 Ganache,这是一个用于以太坊开发的个人区块链,您可以使用它来部署合约、开发应用程序和运行测试。 如果您还没有,请下载 Ganache 并双击图标以启动应用程序。 这将生成一个在 7545 端口本地运行的区块链。

 

  1. 回到我们的终端,将合约迁移到区块链。
    truffle migrate
    You should see output similar to the following:
    1_initial_migration.js
    ======================
    
       Deploying 'Migrations'
       ----------------------
       > transaction hash:    0x3b558e9cdf1231d8ffb3445cb2f9fb01de9d0363e0b97a17f9517da318c2e5af
       > Blocks: 0            Seconds: 0
       > contract address:    0x5ccb4dc04600cffA8a67197d5b644ae71856aEE4
       > account:             0x8d9606F90B6CA5D856A9f0867a82a645e2DfFf37
       > balance:             99.99430184
       > gas used:            284908
       > gas price:           20 gwei
       > value sent:          0 ETH
       > total cost:          0.00569816 ETH
    
    
       > Saving migration to chain.
       > Saving artifacts
       -------------------------------------
       > Total cost:          0.00569816 ETH
    
    
    2_deploy_contracts.js
    =====================
    
       Deploying 'Adoption'
       .............................
       .............................
    

您可以看到按顺序执行的迁移,然后是与每个迁移相关的一些信息。 (您的信息会有所不同。)

在 Ganache 中,请注意区块链的状态已更改。 区块链现在显示当前块,以前为 0,现在为 4。此外,虽然第一个帐户最初有 100 个以太币,但由于迁移的交易成本,现在它更低了。 稍后我们将更多地讨论交易成本。

 

使用 JavaScript 测试智能合约

Truffle 在智能合约测试方面非常灵活,因为测试可以用 JavaScript 或 Solidity 编写。 在本教程中,我们将使用 Chai 和 Mocha 库在 Javascript 中编写测试。
在 test/ 目录中创建一个名为 testAdoption.test.js 的新文件。
将以下内容添加到 testAdoption.test.js 文件中:

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

contract("Adoption", (accounts) => {
  let adoption;
  let expectedAdopter;

  before(async () => {
      adoption = await Adoption.deployed();
  });

  describe("adopting a pet and retrieving account addresses", async () => {
    before("adopt a pet using accounts[0]", async () => {
      await adoption.adopt(8, { from: accounts[0] });
      expectedAdopter = accounts[0];
    });
  });
});

我们通过导入来启动合约:

* Adoption:我们想要测试的智能合约。 我们通过使用 artifacts.require 导入我们的 Adoption 合约来开始我们的测试。
注意:编写此测试时,我们的回调函数采用参数帐户。 这为我们提供了使用此测试时网络上可用的帐户。

然后,我们利用 before 为以下内容提供初始设置:

* 领养 id 为 8 的宠物并将其分配给网络上测试帐户中的第一个帐户。

* 该函数稍后用于检查petId:8是否被accounts[0]采用。

 

### 测试采用功能

要测试采用函数,请记住,成功后它会返回给定的采用者。 我们可以确保返回基于给定 petID 的采用者,并与采用函数中的预期采用者进行比较。

 1.在 testAdoption.test.js 测试文件中,在代码块声明之前添加以下函数。

describe("adopting a pet and retrieving account addresses", async () => {
  before("adopt a pet using accounts[0]", async () => {
    await adoption.adopt(8, { from: accounts[0] });
    expectedAdopter = accounts[0];
  });

  it("can fetch the address of an owner by pet id", async () => {
    const adopter = await adoption.adopters(8);
    assert.equal(adopter, expectedAdopter, "The owner of the adopted pet should be the first account.");
  });
});

注意事项:

我们调用智能合约方法采用者,看看哪个地址采用了 petID 为 8 的宠物。
Truffle 为用户导入 Chai,因此我们可以使用断言函数。 我们将实际值、预期值和失败消息(如果测试未通过,则会打印到控制台)传递给 assert.equal()。### 测试所有宠物主人的检索

由于数组只能在给定单个键的情况下返回单个值,因此我们为整个数组创建自己的 getter。

在 testAdoption.test.js 中之前添加的函数下面添加这个函数。

it("can fetch the collection of all pet owners' addresses", async () => {
  const adopters = await adoption.getAdopters();
  assert.equal(adopters[8], expectedAdopter, "The owner of the adopted pet should be in the collection.");
});

由于采用者是一个数组,并且我们从第一次采用测试中知道我们采用了 petId 为 8 的宠物,因此我们将合约的地址与我们期望找到的地址进行比较。

Running the tests

  1. Back in the terminal, run the tests:
truffle test
  1. If all the tests pass, you'll see console output similar to this:
Using network 'development'.

Compiling your contracts...
===========================
> Compiling ./test/TestAdoption.sol
> Artifacts written to /var/folders/z3/v0sd04ys11q2sh8tq38mz30c0000gn/T/test-11934-19747-g49sra.0ncrr
> Compiled successfully using:
   - solc: 0.5.0+commit.1d4f565a.Emscripten.clang

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


  3 passing (670ms)

创建用户界面以与智能合约交互

现在我们已经创建了智能合约,将其部署到我们的本地测试区块链并确认我们可以通过控制台与之交互,是时候创建一个 UI,以便 Pete 可以为他的宠物店使用一些东西!

宠物店 Truffle Box 中包含应用程序前端的代码。 该代码存在于 src/ 目录中。

前端不使用构建系统(webpack、grunt 等)以尽可能容易上手。 应用程序的结构已经存在; 我们将填写以太坊独有的功能。 这样,您可以将这些知识应用到您自己的前端开发中。

Instantiating web3

在文本编辑器中打开 /src/js/app.js。

检查文件。 请注意,有一个全局 App 对象来管理我们的应用程序,在 init() 中加载宠物数据,然后调用函数 initWeb3()。 web3 JavaScript 库与以太坊区块链交互。 它可以检索用户帐户、发送交易、与智能合约交互等等。

从 initWeb3 中删除多行注释并将其替换为以下内容:

// Modern dapp browsers...
if (window.ethereum) {
  App.web3Provider = window.ethereum;
  try {
    // Request account access
    await window.ethereum.request({ method: "eth_requestAccounts" });;
  } catch (error) {
    // User denied account access...
    console.error("User denied account access")
  }
}
// Legacy dapp browsers...
else if (window.web3) {
  App.web3Provider = window.web3.currentProvider;
}
// If no injected web3 instance is detected, fall back to Ganache
else {
  App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
}
web3 = new Web3(App.web3Provider);

注意事项:

首先,我们检查我们是否使用现代 dapp 浏览器或更新版本的 MetaMask,其中将以太坊提供程序注入到窗口对象中。 如果是这样,我们使用它来创建我们的 web3 对象,但我们还需要使用 ethereum.enable() 显式请求访问帐户。

如果以太坊对象不存在,我们然后检查注入的 web3 实例。 如果存在,则表明我们使用的是较旧的 dapp 浏览器(如 Mist 或较旧版本的 MetaMask)。 如果是这样,我们获取它的提供者并使用它来创建我们的 web3 对象。

如果不存在注入的 web3 实例,我们将基于本地提供者创建 web3 对象。 (此回退适用于开发环境,但不安全且不适合生产环境。)

 实例化合约

现在我们可以通过 web3 与以太坊进行交互,我们需要实例化我们的智能合约,以便 web3 知道在哪里可以找到它以及它是如何工作的。 Truffle 有一个名为 @truffle/contract 的库来帮助解决这个问题。 它使有关合约的信息与迁移保持同步,因此您无需手动更改合约的部署地址。

仍然在 /src/js/app.js 中,从 initContract 中删除多行注释并将其替换为以下内容:

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

注意事项:

我们首先为我们的智能合约检索工件文件。 工件是关于我们合约的信息,例如其部署地址和应用程序二进制接口 (ABI)。 ABI 是一个 JavaScript 对象,定义了如何与合约交互,包括其变量、函数及其参数。

一旦我们的回调中有工件,我们将它们传递给 TruffleContract()。 这会创建一个我们可以与之交互的合约实例。

实例化合约后,我们使用之前在设置 web3 时存储的 App.web3Provider 值设置其 web3 提供程序。

然后我们调用应用程序的 markAdopted() 函数,以防之前访问时已经收养了任何宠物。 我们将其封装在一个单独的函数中,因为我们需要在任何时候对智能合约的数据进行更改时更新 UI。

获取领养宠物并更新 UI

仍然在 /src/js/app.js 中,从 markAdopted 中删除多行注释并将其替换为以下内容:

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() 允许我们从区块链读取数据而无需发送完整的交易,这意味着我们不必花费任何以太币。

在调用 getAdopters() 之后,我们遍历所有这些,检查是否为每只宠物存储了一个地址。 由于数组包含地址类型,以太坊用 16 个空地址初始化数组。 这就是我们检查空地址字符串而不是 null 或其他虚假值的原因。

一旦找到对应地址的petId,我们禁用它的adopt按钮并将按钮文本更改为“Success”,以便用户得到一些反馈。

任何错误都会记录到控制台。

调用adopt()函数

 仍然在 /src/js/app.js 中,从 handleAdopt 中删除多行注释并将其替换为以下内容:

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

注意事项:

我们使用 web3 来获取用户的帐户。 在错误检查后的回调中,我们然后选择第一个帐户。

从那里,我们像上面一样获取部署的合约,并将实例存储在采用实例中。 不过这一次,我们将发送事务而不是调用。 交易需要“发件人”地址并具有相关成本。 这种以以太币支付的成本称为gas。 天然气成本是在智能合约中执行计算和/或存储数据的费用。 我们通过执行带有宠物 ID 和包含帐户地址的对象(我们之前存储在帐户中)的对象来发送交易。

发送交易的结果就是交易对象。 如果没有错误,我们继续调用我们的 markAdopted() 函数以将 UI 与我们新存储的数据同步。

 

Installing and configuring lite-server

我们现在可以启动本地 Web 服务器并使用 dapp。 我们正在使用 lite-server 库来提供我们的静态文件。 这与宠物店 Truffle Box 一起提供,但让我们来看看它是如何工作的。

在文本编辑器(在项目的根目录中)打开 bs-config.json 并检查内容:

{
  "server": {
    "baseDir": ["./src", "./build/contracts"]
  }
}

这告诉 lite-server 哪些文件要包含在我们的基本目录中。 我们为我们的网站文件添加 ./src 目录,为合约工件添加 ./build/contracts 目录。

我们还在项目根目录的 package.json 文件中的脚本对象中添加了一个 dev 命令。 scripts 对象允许我们将控制台命令别名为单个 npm 命令。 在这种情况下,我们只执行一个命令,但可能有更复杂的配置。 这是您的外观:

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

当我们从控制台执行 npm run dev 时,这告诉 npm 运行我们的 lite-server 本地安装。

 

posted @ 2022-02-15 00:50  jwenlee  阅读(181)  评论(0编辑  收藏  举报