首页 > 其他分享 >区块链开发七天实用指南(全)

区块链开发七天实用指南(全)

时间:2024-05-01 15:58:30浏览次数:20  
标签:指南 七天 函数 以太 代码 测试 使用 区块 我们

原文:zh.annas-archive.org/md5/af79b3f5ee1ede6c2518deb93a96c328

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

区块链是一项革命性技术,以太坊是一个运行智能合约的分散式区块链平台。

7 天实战区块链开发将教会你如何使用以太坊区块链构建一个在线游戏应用。本书的每一部分都将介绍与创建在线游戏相关的基本区块链编程概念,然后紧接着是实践性的练习,作为家庭作业来实施。通过本书,你将获得创建智能合约、与以太坊网络交互、构建用户界面以及将应用部署到互联网所需的核心区块链应用程序开发技能。本书提供了七个独立的课程,以实用、动手的方式进行教学。

通过本书,你将会惊讶于在一周内在以太坊网络上,你已经学到了多少关于区块链应用程序开发!

本书适合对象

本书适合渴望获得区块链应用程序开发技能并希望掌握区块链应用程序开发的软件工程师和 IT 专业人士。本书非常适合那些具有有限编程经验的人。

本书内容

第一章,第一天 - 应用程序介绍、安装和设置,带领我们完成了运行应用程序所需的基本环境设置。我们将了解我们的应用程序做什么以及它是如何运行的,还会学习我们将用于与区块链交互的各种工具。

第二章,第二天 - Solidity 变量和数据类型,教导我们有关 Solidity 编程语言的一切。它向我们展示了如何为 Solidity 编写代码,Solidity 中不同类型的变量以及如何实现它们。

第三章,第三天 - 在智能合约中实现业务逻辑,向我们展示了智能合约是什么,以及它们如何用于处理区块链应用。然后,我们将学习如何在这些智能合约中编写业务逻辑,以使我们的应用程序按预期运行。

第四章,第四天 - 创建测试,向我们展示了测试的重要性。在本书中,我们将广泛使用测试来确保我们的代码没有任何问题。本章向我们展示了各种类型的测试,如何创建它们以及如何使用它们来改进我们的应用程序。

第五章,第五天 - 构建用户界面,教会了我们关于 React 框架的一切,我们将使用它作为后端来创建应用程序的用户界面。我们将学习如何通过用户界面与我们的区块链网络交互,确保它正常运行,并学习如何通过用户界面将我们的应用程序连接到网络。

第六章,第六天 - 使用钱包,向我们展示了什么是区块链钱包,它们是如何工作的,以及为什么我们应该使用它们。我们将学习如何将我们的钱包与区块链网络联系起来,并使用它来管理我们应用程序中存在的各种交易。

第七章,第七天 - 部署到网络,教会了我们如何上传我们的应用程序供全球用户使用。我们将学习如何使用 Ropsten 测试网络来测试我们的应用程序,然后部署到真实的以太坊区块链上。我们将学习如何使用亚马逊网络服务AWS)上传和托管我们的游戏用户界面,全球用户将使用它来玩游戏。

要充分利用本书

对于您理解和应用本书中的所有概念,一些关于编程的基本知识是必需的。有关区块链工具的基本知识将是额外的优势。

下载示例代码文件

您可以从您在 www.packt.com 的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问 www.packt.com/support 并注册,以便直接通过电子邮件接收文件。

您可以按照以下步骤下载代码文件:

  1. www.packt.com 上登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保您使用以下最新版本解压或提取文件夹:

  • 用于 Windows 的 WinRAR/7-Zip

  • 用于 Mac 的 Zipeg/iZip/UnRarX

  • 用于 Linux 的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上:github.com/PacktPublishing/Hands-on-Blockchain-Development-in-7-Days。如果代码有更新,它将更新在现有的 GitHub 存储库中。

我们还提供了来自我们丰富图书和视频目录的其他代码包。请查看:github.com/PacktPublishing/

下载彩色图像

我们还提供了一份 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在此处下载:www.packtpub.com/sites/default/files/downloads/9781838640101_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码单词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入以及 Twitter 句柄。这是一个例子:“在上面的示例中,我们声明了一个无符号整数 (uint) 作为 foo,并将其可见性设置为 public。”

代码块设置如下:

function foo() public returns
(string) {
    return "Hello";
}

function bar() external {
    foo_ = foo(); //Not valid
    foo_ = this.foo(); //Valid
}

当我们希望引起您对代码块的特定部分的注意时,相关行或项将以粗体显示:

function foo() public returns
(string) {
    return "Hello";
}

function bar() external {
    foo_ = foo();   //Not valid
    foo_ = this.foo();  //Valid
}

任何命令行输入或输出均如下所示:

$ geth --testnet --syncmode "light" --rpc --rpcapi db, eth, net, web3, personal, admin --cache=1024 --rpcport 8545

粗体:表示一个新术语,一个重要词汇,或者屏幕上显示的词语。例如,菜单或对话框中的单词在文本中会以这样的形式出现。这是一个例子:“现在它将打开 Chrome Web Store。之后,点击 添加到 Chrome 按钮。”

警告或重要提示会出现在这样的形式下。

小贴士和技巧会以这样的形式出现。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在消息主题中提及书名,发送邮件至 [email protected]

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误是不可避免的。如果您在本书中发现了错误,我们将不胜感激。请访问 www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,然后输入详细信息。

盗版:如果您在互联网上发现了我们作品的任何形式的非法副本,请向我们提供位置地址或网站名称,我们将不胜感激。请通过邮件联系我们,邮箱为 [email protected],并附上材料的链接。

如果您有兴趣成为作者:如果您在某个主题上拥有专业知识,并且对编写或为书籍做贡献感兴趣,请访问 authors.packtpub.com

评论

请留下您的评论。一旦您阅读并使用了本书,为什么不在您购买它的网站上留下评论呢?潜在读者可以看到并使用您的客观意见做出购买决定,我们在 Packt 可以了解您对我们产品的看法,而我们的作者可以看到您对他们的书的反馈。谢谢!

欲了解更多关于 Packt 的信息,请访问 packt.com

第一章:第一天 - 应用程序介绍、安装和设置

区块链是一个如此广阔的话题,在短短一周内就能变得高效,启动您的区块链项目,或者成为一名区块链开发人员,这真的可能吗?区块链技术正在出现在各个行业,从我们如何银行到我们如何旅行,我们如何证明我们的身份,甚至我们如何获得医疗保健等方面。所以,这让我们产生了一个问题,您真的可以在短短七天内学会创建一个区块链应用程序吗?

在这个项目中,您将学习如何使用以太坊区块链在七天内创建一个在线游戏应用程序!在此过程中,您将学习如何使用 Solidity 创建和使用变量,并使用函数执行业务逻辑;通过学习编写测试来消除代码中的错误和错误,您将与以太坊区块链交互,通过一个使用 React 和 Redux 的用户界面。您将编写代码来从您的去中心化应用程序发送和接收资金,最后您将学习如何将您的去中心化应用程序部署到以太坊网络和亚马逊网络服务上。在这本书中,我们将一切都拆解到基础,这样您就可以放心,您成功所需的唯一一件事就是想成为区块链开发人员的愿望和愿意付出努力。

本章将作为开始这个项目的基石,涵盖以下主题:

  • 我们应用程序的介绍

  • 安装所需的工具

  • 创建我们的第一个智能合约

  • 理解基本语法

  • 写下您的第一个测试

我们应用程序的介绍

现在让我们来看一下我们将要构建的应用程序。我们将构建一个游戏应用程序,向玩家展示一个介于 0 和 9 之间的数字,然后他们可以选择下注一个神秘数字是否高于或低于显示的数字,然后可以下注他们愿意下注的以太币数量。他们游戏的结果显示在历史窗口中,当他们赢了或输了时,资金会自动从他们的以太坊账户中添加或扣除。以下截图展示了应用程序的演示:

应用程序介绍

要构建这个应用程序,您将使用 Solidity 创建包含并在区块链网络上执行游戏规则的合约。用户界面将使用 React 编写,您将使用 Solidity 编写合约代码。您将学习一个名为 Ganache 的工具,它允许您在本地工作站上托管自己的以太坊区块链。我们现在已经准备好开始构建应用程序了。让我们从在我们的工作站上设置所有必需的工具开始。

安装所需的工具

让我们来看看你将需要的所有工具,不仅是本书,还有作为一个区块链开发者成功所需的工具。我们将了解托管我们的应用程序、编译和迁移我们的代码以及测试我们的应用程序所使用的所有技术。所以让我们开始我们的第一个工具吧!

Visual Studio Code

我们将使用的第一个工具是 Visual Studio Code。在其最简单的形式中,它是一个文本编辑器,但是当你对它更加熟悉时,它将成为你作为区块链开发者的一部分。它包含了许多功能,这些功能将使你的生活更加轻松,比如语法高亮、智能感知以及针对特定编程语言的扩展,比如本书中将要使用的 JavaScript 和 Solidity。我们将使用 Visual Studio Code 使用 Solidity 编程语言创建我们的智能合约。

要安装它,可以访问以下网站:code.visualstudio.com/,并下载适合你操作系统的安装包。你应该看到一个类似于以下屏幕截图的首页:

这是唯一非必需的工具;你真正需要的只是一个你熟悉的代码编辑器,所以如果你已经有一个编辑器,你也可以继续在本书中使用它。

我们的智能合约为我们的应用程序提供了两条规则——它是用一种叫做 Solidity 的编程语言编写的,并且它存在于以太坊区块链网络上。另一种想象合约的方式是想象购买一辆汽车。这样做,你同意购买价格、首付款以及可能的融资条款。所有这些细节都输入到一个合约中,你签署以购买车辆。我们不会打印出那个合同然后在一张纸上签名,我们将这些细节放入一个智能合约中,你或者买方会进行加密签名。

我们还将使用 Visual Studio Code 编写渲染我们应用程序用户界面的 JavaScript。我们将具体使用 React 和 Redux 来创建用户界面。最终,我们希望的是某人访问我们的网站,并且服务器向他们发送一个包含我们应用程序的网页。我们将使用 Node.js 来实现这一点,所以你需要安装它。

Node.js

正如我们在前一节中提到的,我们将需要一个网络框架,可以用来发送和接收数据到和从我们的网站。Node.js 是全球范围内用于此类目的最流行的框架之一。

Node.js 主要用于实现后端 API,这些 API 可以在浏览器之外运行 JavaScript 代码。在本书中,我们将使用它来连接我们的合约、GUI 和编程后端到我们的网站。你可以通过访问 nodejs.org/en/download/ 并选择适合你操作系统的正确包来安装 Node.js。Node.js 的首页看起来类似于以下屏幕截图:

Truffle 框架和 Ganache

当用户在我们的应用程序中执行需要写入区块链的操作时,称为事务。事务不会立即写入;相反,它被发送到网络,在那里等待矿工确认为有效的事务。一旦矿工确认了它,它就会被写入区块链,此时我们可以向用户提供更新后的状态信息。

现在,以太坊网络上的所有这些都代表着数十万台服务器,但我们没有数十万台服务器闲置,并且您不希望在开发期间每次需要测试时都等待外部服务器。

因此,我们将使用 Ganache 模拟我们自己的以太坊网络。它是一个独立的应用程序,可以在您自己的工作站上创建一个以太坊测试网络。要安装 Ganache,我们将前往 truffleframework.com/ganache,并下载其安装程序包。以下截图显示了此的登陆页面:

一旦完成了这一步,我们需要安装 Truffle 框架。这是一个以太坊开发框架,使我们能够更轻松地完成一些事情,比如编译我们的合约、与区块链网络交互以及将我们的合约迁移到以太坊网络。

为此,我们将打开一个终端,无论是 Bash shell 还是 Windows 命令提示符,然后输入以下命令:

$ npm install -g truffle

注意,npm 被打包为 Node.js 的一部分,这意味着如果您跳过安装 Node.js 步骤,那么在此命令起作用之前,您需要返回并完成它。

现在,为了获取我们应用程序所需的代码,我们将使用以下命令:

$ truffle unbox github_url

此命令为您下载并设置代码。请确保在前面的块中用实际的 GitHub 仓库替换 github_url 值!

在运行 truffle 命令时,您可能会遇到一些错误。这是一个已知的问题,并作为官方文档的一部分嵌入其中。您可以参考以下链接找到解决方法:truffleframework.com/docs/truffle/reference/configuration#resolving-naming-conflicts-on-windows

经过我们的先决条件,现在是时候进行更有趣的事情了,这包括编写一些代码。在下一节中,我们将看看写合约意味着什么,以及它如何作为区块链网络的一部分进行交互。

创建我们的第一个智能合约

在本节中,您将学习以太坊合约的基础知识。您将学习它们是什么,它们居住在哪里,如何创建它们以及如何将它们部署到以太坊网络。

合同是由其函数和数据或合同的当前状态表示的代码集合。它位于以太坊网络上的特定地址,并且需要记住的一件重要事情是,以太坊网络是公开的,这意味着任何人都可以查看您的合同及其数据。

这与你可能熟悉的传统应用程序不同。在传统应用程序中,代码通常存储在应用程序中,而数据则存储在其他地方,可以是磁盘上的文件或数据库中。现在,让我们看看合同包含什么。

分析合同

我们合同的源代码存储在contracts文件夹中。当我们编译它们时,它们将发送到以太坊区块链。编译还会创建一个存储在build/contracts文件夹中的合同元数据。以下截图显示了我们将要构建的应用程序的结构:

合同本身以pragma语句开头,pragma语句告诉编译器有关代码的信息。在我们的情况下,我们想告诉编译器,此代码是用 Solidity 编程语言编写的,并且使用了 Solidity 语言的0.5.0版本。因此,提及此内容的代码如下所示:

pragma solidity 0.5.0

在前面的代码块中看到的版本信息是SemVer语义化版本。基本上,在 SemVer 中,第一个数字表示包的主要版本,中间数字表示次要版本,第三个数字表示补丁级别。

在升级时需要记住的一点是,升级版本可能会导致一些不兼容的情况;然而,遵循 SemVer 的应用程序中的一个好处是,如果引入的变化与以前的版本不兼容,可以增加主要版本号来告诉编译器使用较新的工具补丁。此外,虽然不太常见,但某些应用程序也会在次要补丁号中引入重大变更,这就是我们在代码中使用^符号的原因,用于 Solidity 的旧版本。这个^符号告诉编译器,可以使用 Solidity 编程语言的任何版本,从0.4.24一直到但不包括0.5.0。这确保了您使用的编译器版本与您编写的版本兼容。然而,由于我们在这里使用的是0.5.0,所以我们不会将其纳入考虑。

要声明一个合同,我们使用contract关键字后跟我们的合同名称,按照常规的惯例,这个合同名称跟随文件名,因此这个Gaming合同的文件将会是Gaming.sol。我们有一组左大括号{,然后如果需要包含注释,可以这样做,就像在 SQL 编程语言中一样,用/*符号,然后我们用右大括号}来结束我们的合同。这可以在以下代码片段中看到:

pragma solidity 0.5.0;

contract Gaming {
/* Our Online gaming contract*/
}

在 Solidity 中有一个称为构造函数的特殊函数,它在合同创建时仅运行一次。通常用于初始化合同数据。例如,让我们看看以下代码片段:

pragma solidity 0.5.0;

contract Gaming {
/* Our Online gaming contract*/
address owner;
bool online;

constructor() public {
        owner = msg.sender;
        online = true;
    }
}

如前面的代码片段所示,我们有一个名为owner的变量和一个名为online的变量。当合同被创建时,我们将owner变量设为将合同推送到网络的以太坊地址,我们还将online变量设为true

Solidity 是一种编译语言,为了在区块链上使用合同,它必须被编译和迁移到该网络。要编译它,我们将使用 Truffle 框架。我们可以用truffle compile命令来做到这一点,它会创建一个包含合同元数据的 JSON 文件,其中包含有关您的合同的信息,我们将使用该 JSON 文件来与合同交互并验证其源代码。

为了在区块链网络上使用我们的合同,我们必须将其从我们的工作站传输到网络上,这就是迁移。因为我们使用 Truffle 框架,我们可以使用truffle migrate命令很容易地完成这个过程。

现在,如果我们查看我们的目录布局,在migrations目录中,我们将找到一个名为1_initial_migration.js的文件。该文件是由 Truffle 框架提供的,它处理合同的部署。让我们来看看该文件中的代码:

var Migrations =
artifacts.require("./Migrations.sol");
module.exports = function(deployer){
    deployer.deploy(Migrations);
};

有一个名为Migrations的变量,它需要 Truffle 库Migrations.sol,然后导出一个函数,该函数需要一个deployer对象来部署迁移。

我们还有一个名为2_deploy_contracts.js的文件,它包含在下载中。这个文件实际上是要迁移我们作为本书的一部分编写的合同。现在让我们来看看这个文件中的代码:

var Gaming =
artifacts.require("./Gaming.sol");
module.exports = function(deployer){
    deployer.deploy(Gaming);
};

类似于前一个文件,有一个名为Gaming的变量,它需要我们创建的Gaming.sol合同文件,然后运行deploy方法来部署Gaming合同。

测试合同

为了帮助巩固迁移这个概念,我们实际上将迁移一些合同,然后分析网络会发生什么。为了做到这一点,我们将使用以下步骤:

  1. 我们的第一步是启动 Ganache 应用程序,这将使我们的私有区块链运行起来,如下面的屏幕截图所示:

正如前面的截图所示,我们有 Ganache 应用程序正在运行。让我们看一下余额,它目前为 100 以太,还没有被挖掘的区块,也没有交易。

  1. 切换到控制台,我们将输入以下命令将合同迁移到网络上:
$ truffle migrate 
  1. 一旦迁移成功,我们将返回 Ganache,那里会看到类似这个截图的界面:

我们可以看到地址 0 或账户 0 已经花费了一些以太坊。这是用来支付合同迁移的费用。从区块来看,我们可以看到挖掘了四个区块,以及用于创建和迁移这些合同的四笔交易。

  1. 如果我们切换到代码编辑器,本例中为 Visual Studio Code,在build/contracts文件夹中,您可以看到编译结果产生的合同元数据文件在这里:

如果我们打开Migrations.json文件并滚动到底部,您可以看到它存储了此合同已部署到的网络,以及部署合同的地址,如下截图所示:

祝贺!如果你已经到达这里,那么你已经成功创建并部署了你的第一个合同。你已经看到了一些语法的用法,因为我们指定了owner变量并构建了constructor函数。在下一节中,我们将讨论 Solidity 编程语言使用的一些语法和风格指南。

理解基本语法

我们不能在没有覆盖一些基本的 Solidity 代码开发语法指南的情况下开始我们的学习之旅。编写代码时的一致性目标并不是为了确定什么是对或错,而是为了提供指南,帮助确保代码总是相同的。这使得代码更易于阅读和理解。

这是一个重要的记住点——它并不是正确的方式或最好的方式,只是一个一致的方式。这并不意味着风格指南适用于每个情况。如有疑问,您应该查看其他脚本示例,然后做出最好的判断。

代码布局

对于代码布局,我们应该始终使用每级缩进四个空格。空格优先于制表符,但即使使用制表符,也要避免在同一文件中混合使用制表符和空格。让我们在接下来的代码块中看一个例子:

pragma solidity 0.5.0;

contract Gaming {
    function determineWinner() public(){
        if (guess == true){
            return true;
        }
        else if (guess == false){
            return false;
        }
    }
}

我们首先看到指定的contract,然后第一个声明的函数缩进了四个空格。if块本身从那里缩进了四个空格。

空行

建议用两个空行包围顶层声明,用单个空行包围函数级别的声明。这将帮助您比随机查找文件并希望找到所需行更快地发现错误。让我们使用以下示例来看看如何使用空行:

contract A {
    function foo() public{
        //...
    }

    function bar() public{
        //...
    }
}

contract B{
    function foo() public{
        //...
    }
}

在前述的代码块中,我们声明了两个合同,在这两个合同之间有两个空行,这样就有了一个很好的大空白,方便区分。在我们的函数声明中,每个函数都用一个空行分隔开。这应该有助于轻松区分代码片段的每个元素。

行长度

对于行长度,建议最多 79 个字符。79 个字符的推荐值是在很久以前设定的,当时人们使用的 TTY 终端最大宽度为 80 个字符。现在看到最大宽度为 99 个字符实际上已经变得相当普遍了。让我们参考以下示例:

myReallyLongAndDescriptiveFunctionN
ame(
    reallyLongVariableOne,
    reallyLongVariableTwo,
    reallyLongVariableThree,
    reallyLongVariableFour
);

如图所示,第一个参数不附加在声明函数的行上,它只有一个缩进。此外,每行只有一个参数,最后,结束元素单独占一行。

函数布局

你的函数应该按特定顺序排列。这是正确的顺序:

  • 构造函数

  • 回退函数

  • 外部函数

  • 公共函数

  • 内部函数

  • 私有函数

我们还没有讨论函数,所以这些内容可能对你来说还有点模糊,但我们将在下一节中详细讨论这个问题,所以请耐心等待。

为了构造函数,我们将在同一行上打开括号,并且在与开始声明的唯一缩进水平的地方关闭该行。同样,开放的大括号之前应该有一个单一空格。让我们看一下下面的代码块:

pragma solidity 0.5.0;

contract Gaming {
    function determineWinner() public() {
        if (guess == true){
            return true;
        }
        else if (guess == false){
            return false;
        }
    }
}

如图所示,当我们声明Gaming合同时,我们的开放大括号之间有空格。同样,对于我们的determineWinner()函数,它在同一行上有开始大括号,然后我们的函数的结束大括号就在下面,函数的第一个字符开始的位置。

在声明变量时,我们更喜欢双引号而不是单引号来表示字符串。我们在操作符周围加上一个空格,因为这有助于在代码中突出显示它们,这样您就可以更轻松地识别它们。以下代码片段说明了这一点:

string str = "foo";  //This
string str='foo';    //Not this

但是,当您有优先级较高的运算符时,您可以省略周围的空间以表示其优先级。

命名约定

在命名变量时,应该始终避免使用单个字母的变量名,例如 int L,bool O 等。这样做的原因是它们看起来非常相似,对你的代码只会增加不必要的复杂性。

在命名合约、库、结构体和事件时,你应该使用 CapWords(或者 CamelCase),其中你将变量名的每个单词的第一个字母大写。以下片段展示了一个示例:

contracts SimpleGame {
    //...
}

contracts MyPlayer {
    //...
}

在命名函数、函数参数、变量和修饰器时,你应该使用 mixedCase,这与 CapWords 非常相似,只是你不要将第一个字母大写,如下面的代码块所示:

contracts SimpleGame {
    function simpleGame () {
        //...
    }
}

在命名常量时,你应该使用全大写字母命名它们。

你可以使用下划线(_)符号来避免函数、变量和代码中的其他对象之间的命名冲突。以下代码块显示了一个示例:

function mysteryNumber() returns (uint) {
    uint randomnumber = blockhash%10 + 1;
    return randomnumber;
}

uint mysteryNumber_ = mysteryNumber();

在前面的代码块中,我有一个名为mysteryNumber()的函数,在稍后的代码中,当使用该函数时,我真的很合理地将我的变量命名为mysteryNumber,因为它是一个神秘的数字,但我不能重用该名称而导致名称冲突。因此,当我实际获取到神秘数字变量时,我在其末尾加了一个下划线,以便mysteryNumber_成为我从mysteryNumber()函数中获得的变量。这样很容易区分这两者,但非常清楚我从哪里获取了那个神秘数字。

到现在为止,你可能已经看到,实施一致的编写代码指南如何使代码更易读和维护。虽然这不会使代码运行更快或者保证它是正确的,但它确实使编码的人类因素更加愉快,这反过来可能会使合作和讨论函数而不是代码格式更容易。在下一节中,我们将看看为我们的代码编写测试以及为什么我们要这样做。

编写你的第一个测试

在前面的一节中,我们学习了如何高效编写代码。虽然这样做可以使代码更易读,但这并不意味着我们的代码就能正常工作,毕竟,破损的代码也应该看起来漂亮,对吧?

让我们稍微谈谈你目前的测试策略。在编写代码后,你是否通过使用它并检查输出来手动测试它,然后在发布最新版本之前重复相同的过程?嗯,让我问你这个问题,你是否曾经忘记过一步,结果导致你发布了一个 bug,如果你记得执行那一步,就可以捕捉到那个 bug?在进行任何更改之前运行测试是个好主意。我们都花了不少时间试图弄清楚我们的代码是如何破坏了测试的,只是后来才发现,在我们开始做任何更改之前,测试就已经破了。

在这本书中,让我们编写自动执行这项任务的测试,我们将利用节省的所有时间来编写更多优秀的代码。我们的测试可以用 Solidity 编写,也可以用 JavaScript 编写。它们可以自动验证代码是否按预期执行,并且应在每次代码更改之前和之后运行。

因此,为了测试Gaming合同,我们将命名我们的测试文件为TestGaming.sol,测试本身放在项目的test目录中。它们也是一个实际的 Solidity 合同,这使得它们相对容易编写,因为你使用与编写任何其他 Solidity 合同相同的技术。

让我们来看一下应用程序中的一个实际的示例测试合同以及实施它们的一些最佳实践。你可以通过在文本编辑器中打开TestGaming.sol文件来访问合同。

现在,让我们将这个合同分解成单独的部分。合同开始如下:

pragma solidity 0.5.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Gaming.sol"

因此,我们有了pragma solidity语句和我们的 Solidity 版本。然后,我们从 Truffle 导入Assert库和DeployedAddresses库。除此之外,我们还将导入我们正在进行测试的实际合同,即Gaming.sol。下一步是定义我们的合同:

contract TestGaming {
    uint public initialBalance = 10 ether;
    Gaming gaming;

就像任何其他 Solidity 合同一样,我们定义我们的合同并给它命名为TestGaming。我们给它一些初始的ether来进行保存,我们可以在我们的测试中使用它,然后创建我们的gaming变量,这是Gaming合同的实例。

在我们的任何测试运行之前,我们先拿到我们部署的合同,因为在测试过程中,每次测试运行都会向测试网络部署一个全新的合同实例。以下代码显示了如何做到这一点:

function beforeAll() public {
    gaming = Gaming(DeployedAddresses.Gaming());
}

然后,我们为我们想要测试的每一个测试场景创建函数,如下面的代码块所示:

function testPlayerWonGuessHigher() public {
    bool expected = true;
    bool result = gaming.determineWinner(5, 4, true);

    Assert.equal(expected, result, "The player should have won by guessing the mystery number was higher than their number");
}

function testPlayerLostGuessLower() public {
    bool expected = false;
    bool result = gaming.determineWinner(5, 4, false);

    Assert.equal(expected, result, "The player should have lost by guessing the mystery number was lower than their number");
}

testPlayerWonGuessHigher()函数测试玩家是否猜测数字应该更高,如果数字确实更高,那么他们应该赢了。

testPlayerLostGuessLower()函数测试当数字实际更高时猜测较低的玩家是否应该输掉。

我们在我们的测试中设置这些场景,定义我们期望发生的事情,然后使用这些断言来验证实际发生的情况。因此,我们只需要在需要感觉或者在部署之前运行这个测试,我们就可以自信地说,我们确定谁赢得比赛和谁输掉比赛的功能是准确工作的。

作业

在这本书的每一章中,我都会给你布置一项作业,要在你开始下一天或下一章之前完成。今天的作业主要是关于设置你的开发环境。这不仅将帮助你完成本书的剩余内容,而且我将向你展示的工具将帮助你处理你接手的每一个区块链项目。以下是你需要做的:

  1. 安装 Visual Studio Code。这是唯一可选的步骤,只有在你已经有一个你熟悉和喜爱的代码编辑器时才是可选的。

  2. nodejs.org安装 Node.js。

  3. 从 Truffle 框架网站安装 Ganache。

  4. 使用npm模块安装 Truffle 框架本身。

如果您之前已安装了 Truffle,请确保您至少安装了 Truffle 版本 4.1,并在需要时进行更新。您可以使用 truffle version 命令随时检查您的 Truffle 版本。

  1. 最后,使用 truffle unbox 命令和本书 GitHub 仓库的 URL 安装课程代码。

  2. 完成后,启动终端或命令提示符,根据您的操作系统,转到下载代码的目录,然后输入 truffle test

如果一切顺利,Truffle 将会返回如下屏幕截图所示的成功消息:

总结

这就是本书第一天的内容了!在这一章中,我们学习了使用区块链和智能合约的所有基础知识。我们学会了如何设置环境来创建区块链应用程序。我们还学习了所有关于基本代码语法、命名约定、风格和结构的知识,以便达到最佳效率。我们学会了如何创建智能合约,以及如何测试它以确保其按预期工作。

在下一章,也就是第二天,我们将看看 Solidity 变量和数据类型,以及如何使用它们构建一些业务逻辑和数据。欢迎来到区块链的世界!

第二章:第二天 - Solidity 变量和数据类型

欢迎来到本书的第二天。在上一章中,我们设置了开发环境,并了解了本课程中要使用的工具。在本章中,我们将学习 Solidity 编程语言中的变量;我们将介绍它们是什么以及它们如何使用。本章涵盖以下主题:

  • 理解 Solidity 变量

  • Solidity 中的数据类型

  • 使用 Solidity 变量

  • 理解 Solidity 操作符

  • 使用 Solidity 操作符

理解 Solidity 变量

如果你是编程新手,这是一个需要掌握的关键概念。如果你是一位经验丰富的程序员,在本章中仍然有一些有价值的提示,因为我们将涵盖 Solidity 中变量特定的一些细微差别。

Solidity 是一种静态类型语言;这意味着,在声明变量时,必须声明变量的类型。考虑以下示例:

uint public foo = 4;

在上述示例中,我们声明了一个无符号整数 (uint) 作为 foo,并将其可见性设置为 public。我们可以选择为其分配一个值,然后以分号结束语句。

在 JavaScript 中,你可能会看到 foolet 声明,或者作为一个常量,如果你在查看 ECMAScript 2016 之前的旧 JavaScript 代码,甚至可能会看到它声明为一个变量。

在下一节中,我们将详细介绍 Solidity 中所有常见类型,但现在,让我们专注于这个可见性标识符的含义。

理解可见性

Solidity 中的所有状态变量都必须声明其可见性,因此,我们有几种类型的可见性:

  • 外部

  • 公共

  • 内部

  • 私有

当可见性声明为 external 时,这意味着变量或函数成为合约接口的一部分。这暴露了函数或变量,以便可以从其他合约和交易中调用它们。例如,如果我们在智能合约中有一个函数,用于确定我们游戏的玩家是否赢了或输了一轮,并且我们需要从 React 应用程序中调用它,我们将不得不将其声明为 external,因为尽管我们的 React 应用程序位于我们的 Web 服务器上,但我们的智能合约位于以太坊网络上。

同样,具有 public 可见性的状态变量和函数是合约接口的一部分,可以从其他合约和交易中调用它们。它们无法在内部调用,而必须使用关键字 this,并且所有公开声明的变量都会自动生成一个自动获取器函数。

语句 can't be called internally without this 是什么意思?让我们使用以下示例:

function foo() public returns
(string) {
 return "Hello";
}

function bar() external {
 foo_ = foo(); //Not valid
 foo_ = this.foo(); //Valid
}

如果我们的合约中有一个名为 foo() 的函数,它返回字符串 Hello,那么直接从该合约中调用 foo() 函数将失败。如果我们想要调用它,我们需要以 this.foo() 的方式调用它。在内部声明的变量和函数只能在当前合约及其派生合约中访问。

在同一个示例中,如果我们将 foo 函数的可见性更改为 internal,那么直接调用该函数而无需 this 关键字现在可以工作了。

私有函数与内部函数非常相似,它们在当前合约内可用,但它们的不同之处在于它们在派生自该合约的合约中不可用。但需要注意的是,这并不意味着您在 private 状态变量和函数中的代码是私有的,这只是意味着它不能被调用。

以太坊区块链上的所有内容都是公开可见的。为了强调这一点,通过使用诸如 Etherscan 这样的工具,我可以查看流行的去中心化应用程序dApp)的源代码,例如 CryptoKitties,并查看所有源代码,包括私有函数和变量。

声明为 public 的状态变量将自动创建一个 getter 函数。这是一个节省时间的功能。毕竟,由于您将变量声明为 public,您可能期望您的客户在某个时候需要访问该变量的值。您不必花时间和精力来创建该函数,因为 Solidity 编译器会为您创建一个。让我们考虑以下示例:

contract A {
    uint public foo = 42;
}

A.foo();

如前例所示,我们有一个名为 A 的合约,声明了一个 public 变量 foo。这意味着任何交易或合约都可以通过调用 A.foo() 函数来获取 foo 变量的值。您会注意到,您不需要编写任何代码来使其工作,因为编译器已经为您完成了。

现在,我们已经了解到 Solidity 是一种静态类型语言,我们在声明变量时必须声明我们正在使用的变量类型。有了这个理解,让我们进入下一节,学习 Solidity 中可用的不同数据类型以及它们的作用。

Solidity 中的数据类型

Solidity 中的数据类型分为两大类:

  • 值类型

  • 引用类型

值类型是按值传递的,这意味着每当一个变量引用与另一个相同时,值总是被复制。与此同时,引用类型表示更复杂的类型,可能不总能适应 256 位。由于它们的大小较大,复制有时可能会很昂贵,我们有两个不同的位置可用于存储它们:内存或存储。现在让我们来讨论一下值数据类型。

值类型

我们将讨论的第一个值类型是布尔值。 它可以有两个值之一,要么是true,要么是false。 在 Solidity 中,当创建时,一切都会初始化为默认值 0。

对于布尔值,0 对应默认值false。 布尔值可用的运算符如下:

  • 逻辑否定:表示给定值的相反,并由!符号表示。 例如,foo = !false

  • 逻辑合取:表示两个值的逻辑结果,并使用&&符号表示。 例如,(foo && bar) > 1

  • 逻辑析取:表示两个值之间的结果使用,并使用||符号表示。 例如,(foo || bar) > 1

  • 相等性:用于将变量与某个固定值进行比较,并使用==符号表示。 例如,foo_ == true

  • 不等式:用于检查变量的值是否不等于指定值,并使用!=符号表示。 例如,foo_!= true

下一个值类型是整数。 整数是整数,意味着没有小数点,它们可以是两种类型:

  • 有符号整数(int

  • 无符号整数(uint

您可以分步声明整数的确切大小,从 8 到 256。 因此,值可以声明为uint8uint16,一直到uint256。 如果不声明大小,只声明intuint,则它是int256uint256的别名。 此外,重要的是要知道,使用整数,除法总是截断的; 因此,两个整数的除法结果始终是一个整数。

尽管所有先前的数据类型都是其他编程语言共有的,但地址数据类型是 Solidity 独有的。 它是一个 20 字节的值,用于表示以太坊网络上的地址。 地址可以是用户的以太坊账户或部署到网络上的合约。

它有一个.balance()成员,可用于检查与该账户关联的余额,以及一个.transfer()成员,可用于向该地址转账。 还有一个.send()成员,它是.transfer()成员的低级对应。 如果使用.send(),您必须检查操作的返回值以确定成功或失败,但如果您使用.transfer(),转账成员会自动处理这个问题。

下一个值类型是字符串。 字符串只是一块文本,但从技术上讲,它是一个字节数组。 这意味着您可以对字符串执行与数组相同的操作。 在声明时,您可以使用单引号或双引号,但正如我们在第一章中学到的那样,第一天 - 应用程序介绍、安装和设置,双引号更受推荐。 它们可以隐式转换为字节。 此外,字符串值支持转义字符,例如换行(\n)和制表符(\t)。

现在我们完成了值类型。让我们来看看 Solidity 提供的引用类型。

引用类型

第一个是一个数组。数组是一组数据元素,可以通过它们的索引号来识别。它们可以是固定大小的或动态的。要声明一个数组,您需要指定数组将包含的数据类型,然后是变量名称,然后是方括号,如下面的示例所示:

uint foo[]

如果数组声明为 public,您将获得一个自动 getter 函数。它与我们在上一节中看到的示例不同,因为 getter 函数需要知道要返回数组的哪个元素,并且它不会返回整个数组。因此,此函数需要元素或索引号作为参数。

它有一个 .length() 成员,将返回数组的长度,还有一个 .push() 成员,用于将新项目添加到数组的末尾。

接下来的数据类型是结构体。结构体提供了一种定义新数据类型的方法,但结构体不能包含自己的数据类型。您不能定义由您正在定义的东西组成的东西。考虑以下示例:

struct Player {
    address player;
    string playerName;
    uint playerBalance;
    uint wins;
    uint losses;
}

Player jim =
Player(msg.sender, "Jim", 0, 0, 0);

上述示例展示了如何创建一个数据类型来保存我们游戏中每个玩家的信息。我们首先将数据类型定义为 struct,将其命名为 Player,并打开大括号。在里面,我们定义了所有我们存储在玩家身上的属性(例如玩家的以太坊地址、玩家的姓名、他们当前的余额、他们赢的次数以及他们输的次数),然后关闭大括号。然后,为了为我们的游戏创建一个新玩家,我们定义一个新的 Player 类型变量,并使用代表新玩家的值初始化 Player 结构。

接下来是映射键类型。如果您使用过其他编程语言,可能会认识到映射类似于哈希表。这种数据类型是键类型到值类型的映射,当它被创建时,它被虚拟初始化,以使每个可能的键都存在,并且它被映射到一个其字节表示都是零的值。如果其可见性设置为 public,Solidity 将创建一个自动 getter。因此,基于我们先前为玩家创建的结构体的示例,我们可以创建所有玩家的映射,如下所示:

mapping (uint => Player) players;

在这里,我们将使用无符号整数作为玩家数据类型映射的键,这个对象将被称为 players。然后,我们可以通过指定映射的第一个元素并将新玩家分配给它来向映射中添加我们的第一个玩家。

现在我们了解了 Solidity 中可用的基本数据类型,让我们学习如何在 Solidity 合约中实际使用它们。

使用 Solidity 变量

现在我们已经了解了 Solidity 中变量的概念,让我们在合约中将它们实际应用起来。让我们看一下我们游戏中的合约:

pragma solidity 0.5.0;

contract Gaming {
    /*Our Online Gaming Contract*/
    address owner;
    bool online;
}

在声明合同后面,声明了两个变量。第一个是owner,它是一个address数据类型,代表部署该合同到以太坊网络的人的以太坊地址。然后我们有一个bool数据类型的叫做online,我们将使用它来确定您的游戏是在线还是离线;如果需要,它允许我们将游戏设置为离线。

嗯,在以太坊合同中有一个特殊的函数叫做constructor,它在合同部署时执行一次且仅执行一次。在其中,我们将做两件事:我们将把我们在合同开头声明的owner变量设置为将该合同部署到网络的以太坊地址:

    constructor() public payable {
        owner = msg.sender;
        online = true;    
    }

这意味着如果我部署了合同,我拥有这个游戏,如果你部署了合同,你就拥有这个游戏。这是您编写合同并将其交付给您的客户的一种方式,当他们部署它们时,他们将拥有合同积累的所有资产和货币。我们还将把我们的online变量设置为true;,我们稍后将使用它来有效地将我们的游戏设置为离线,如果需要的话。

我们还有一个叫做winOrLose()的函数,我们将从我们的 React 应用程序中调用它,并提供所需的参数来确定我们的玩家在这一轮中是赢还是输。还有另一个叫做isWinner()的函数,如果玩家赢了,则返回 true,如果输了,则返回 false。所以让我们使用以下代码片段来看一下它是如何工作的:

function winOrLose(uint display, bool guess, uint wager)
external payable returns (bool) {
    if (isWinner == true ) {
        /*Player won*/
        msg.sender.transfer(wager*2);
        return true;
    } else if (isWinner == false) {
        /*Player lost*/
        return false;
    }
}

在这里,我们有一个if语句,我们正在评估其中包含的条件。接下来,我们有包含要执行的代码的大括号。在括号内部,我们有我们的isWinner()变量,它是一个布尔数据类型,我们使用==符号来评估这个变量是否评估为布尔true。如果是,它将执行包含在块中的代码。

这段代码使用了一个特殊的消息sender变量,其中包含调用该函数的账户的地址。在这种情况下,就是我们的玩家。玩家赢了,所以我们将使用.transfer()成员将下注金额的两倍转移给玩家;我们之所以要翻倍,是因为玩家必须在此次交易中包含他们想要下注的金额,所以我们需要将它们归还给他们,再加上他们从该赌注中赢得的金额。

如果该语句的评估结果不为true,那么该代码块将不会执行,因此代码执行会继续到else if代码块。它的操作方式与if代码块相同。它将评估括号内的语句,如果isWinner()为 false,该代码块将返回 false 给我们的 React 客户端。

在我们的 React 代码中,我们将检查这个值,并根据情况更新 UI,以告知玩家他们在这一轮中失败了。

以太单位

当我们谈到变量时,让我们看看可以应用于变量的一些特殊单位。我们将从Ether单位开始。它们用于指定货币,并且可以应用于任何文字数字。它们还可以在 Ether 的子单位之间进行转换。

Ether 的子单位如下:

  • Wei:这是 Ether 中最小的货币单位

  • Szabo:也被称为微 Ether,等于 10 的 12 次方 Wei

  • Finney:也被称为毫 Ether,等于 10 的 15 次方 Wei

  • Ether:等于 10 的 18 次方 Wei

要使用这些单位,只需在任何文字数字的末尾指定单位,编译器就知道如何在不同的子单位之间进行转换。

其他特殊单位

我们还有时间单位可供使用,并且它们可以用于指定时间单位。它们的声明方式与货币一样:任何文字数字都可以附加所需的单位。这里可以使用的不同时间单位包括:

  • 分钟

  • 小时

但要注意的是,不要在日历计算中使用它。并非每一天都有 24 小时,这是由于闰秒。闰秒类似于闰年,只是它们是秒。实际上,Solidity 过去也有一年的时间单位,但由于闰年的问题,已被废弃。

还有一些独特的变量。第一个是区块编号(block.number)。请记住,以太坊矿工始终在确认交易,将其写入区块,然后将这些区块添加到区块链中。这是从该操作中的当前区块编号;它用于跟踪当前正在挖掘的区块。

区块时间戳(block.timestamp)是当前区块的时间戳,报告自 1970 年 1 月 1 日以来经过的秒数。还有一个别名称为now,它指的是block.timestamp

然而,这两者都不应该依赖,因为它们可以在一定程度上被恶意矿工操纵,这可能被用来利用您的合约的时间戳。一般来说,您可以确信当前时间戳大于上一个区块的时间戳,并且它将小于下一个区块的时间戳,但就这样了。

消息价值(msg.value)是随消息发送的货币数量。在构建我们的应用程序时,我们将更详细地探讨这一点。我们将使用它来收集我们游戏玩家的赌注。

你已经见过msg.sender并了解它是当前调用者的以太坊地址。还有一个tx.origin传输起源,它是交易发送者的以太坊地址。你可能会觉得诱人,但总的来说,消息发送者可能是你想要的。tx.originmg.sender可能不是同一个东西,特别是在合同或功能调用其他合同或功能的情况下。

好了!现在,我们已经见过 Solidity 数据类型,也学习了它们在代码中的使用;接下来,我们将看看不同的运算符,这些运算符可以用来构建合同中的复杂业务规则和逻辑。

理解 Solidity 运算符

在本节中,我们将看看 Solidity 中可用的不同运算符。这样做将帮助您更好地理解可以编写到合同中实现业务逻辑的逻辑操作。

我们将要讨论的前三个运算符如下:

  • 赋值

  • 相等性

  • 不等式

刚开始学编程时,你可能会觉得这些很困惑;然而,现在花一分钟将为你未来节省数小时的沮丧。

赋值用于将值赋给变量。例如,我们可以将变量foo赋值为bar,如下面的代码片段所示:

string foo = "bar";
foo == bar;
foo != bar;

接下来的运算符是相等性,我们用它来确定一个变量是否等于另一个变量。在上面的示例中,我们正在检查变量foo的值是否等于字符串bar,在我们的案例中,是的,所以这个表达式将评估为 true。

最后我们有不等式,它与等式完全相反。所以,在前一个示例的第三行中,我们正在检查foo是否不等于bar,但它是相等的,所以这个表达式将评估为 false。

我们还有一组比较运算符。我们可以使用这些来确定一个值大于>)、大于或等于>=)、小于<)或小于或等于<=)另一个值。这些运算符的工作方式与我们刚刚看过的等式和不等式运算符类似。

当您编写代码时,我们还有一些速记运算符可以节省您的时间。其中一些如下所示:

  • +=:这是加法的速记运算符

  • -=:这是减法的速记运算符

  • *=:这是乘法的速记运算符

  • /=:这是除法的速记运算符

  • %=:这是余数的速记运算符

  • |=:这是逻辑与的速记运算符

  • &=:这是逻辑或的速记运算符

a += e等价于写a = a + e,但更短更容易输入。

我们还可以使用a++a--运算符来递增或递减计数器 1。但是,在执行其中一个时,表达式将返回更改之前的a的值,因此如果a = 1,并且我们执行a++运算符,则表达式返回1的输出,但是a的值现在是2

还有++a--a,它们执行相同的操作。它们递增或递减 1,但它们返回变化后的实际值。所以,如果a = 1,并且我们执行++a,表达式将返回a的新值,即2

我们可以使用delete运算符将变量分配给其类型的初始值。例如,如果foo是一个整数,执行delete foo将使foo的值设置为 0。

当运算符应用于不同类型时,编译器会尝试将其中一个操作数隐式转换为另一个的类型。如果在转换中语义上有意义,并且转换中没有丢失信息,则可以进行隐式转换。例如,8 位无符号整数(uint8)可以转换为uint16uint28uint256,但 8 位整数(int8)不能转换为无符号 256 位整数,因为uint256不能保存负数。

现在我们对运算符和变量有了一定的了解,以及如何在 Solidity 中使用它们,让我们看一些实际的例子,通过使用它们来在我们的合约中创建业务逻辑。

使用 Solidity 运算符

在本节中,我们将看一些使用 Solidity 运算符的实际示例。这样做将使您具备在去中心化应用的智能合约中开始实现自己的业务逻辑所需的技能。

让我们从声明一个变量开始;我们通过指定其类型来做到这一点,在本例中是无符号整数,并将变量命名为a。我们没有为其分配一个值,因此 Solidity 将其分配一个初始值为 0。我们可以使用=符号将值5赋给a,我们也可以像下面的代码片段中所示一样一行完成同样的事情:

uint a; //initialized as 0
a = 5;  //assign a value of 5
uint b = 10;  //create and assign value of 10

现在我们可以写出表达式a == b,以检查变量ab是否相等。由于 5 不等于 10,因此该表达式返回false。如果我们写表达式a != b,则其求值为true,因为 5 不等于 10。我们还可以使用大于(>)运算符来查看a是否大于b,由于 5 不大于 10,它将返回false;使用小于(<)运算符,我们可以检查a是否小于b,由于 5 小于 10,该表达式返回 true。

让我们看另一个例子:

uint x[];
for (uint i = 0; i < 10;  i++) {
    x.push(i);
}

在这里,我们声明了一个变量x作为无符号整数数组,通过将类型指定为uint来指定无符号整数,然后将变量名分配为x,并包括方括号以指示它是一个数组。然后,我们创建了一个具有三个部分的for循环。

第一个是我们的初始变量,也就是我们循环的起点,所以它是一个我们初始化为 0 的无符号整数i。接下来是我们循环的条件,它告诉循环何时停止;在这里,我们希望它在i小于 10 时继续循环。最后,我们的增量是每次for循环迭代时使用的。我们使用++来递增i的值。然后,我们有大括号,其中包含每次循环执行的代码。在这些大括号中,我们想要将i的值推送到我们的数组x中。结果是我们的数组x被填充了十个值,每个值代表了在for循环中该实例中i的值。

我们应用中的运算符

现在,让我们深入了解一下我们将要构建的游戏中的一个函数。我们将在下一章节中详细讨论函数,即第三章Day Three - 在智能合约中实现业务逻辑,所以我们暂时跳过了解这个函数如何工作的细节,并专注于其中运算符的使用。以下代码片段显示了该函数:

function winOrLose(uint display, bool guess, uint wager)
external payable returns (bool) {
    require(online == true);
    require(msg.sender.balance > msg.value, "Insufficient 
funds");
    uint mysteryNumber_ mysteryNumber();
    bool isWinner = determineWinner (mysteryNumber_,
display, guess);
    if (isWinner == true ) {
        /*Player won*/
        msg.sender.transfer(wager*2);
        return true;
    } else if (isWinner == false) {
        /*Player lost*/
        return false;
    }
}

在这里,我们声明了一个名为mysteryNumber_的无符号整数,它的值来自函数mysteryNumber()

function mysteryNumber() internal view returns (uint)
{
uint randomNumber = uint(blockhash(block.number-1))%10
+1;
    return randomNumber;
}

我们还声明了一个名为randomNumber的无符号整数,并将其作为此函数的结果返回。为了生成我们的随机数,我们使用了之前学习过的一个特殊变量,block.number,它是当前正在挖掘的块,并从中减去了 1,因此我们得到了前一个块的编号。然后,我们使用 Solidity 的blockhash()函数来获取该块的哈希值,然后将其转换为无符号整数,并使用取余(%)运算符来获得将该blockhash除以 10 的余数。基本上,这给了我们blockhash的最后一个数字;我们将在该数字上加 1,这将是我们的随机数,并且函数将此作为其代码的最后一行返回。

回到之前的winOrLose()函数中,我们现在有了我们的mysteryNumber,所以我们声明了一个名为isWinner的布尔变量,它的值来自determineWinner()函数:

function determineWinner
    (uint number, uint display, bool 
guess)
    public pure returns (bool) {
    if (guess == true){
        if (number > display) {
            return true;
        }
    }else if (guess == false){
        if (number > display) {
            return false;
        }
    }
}

此函数接受三个参数,并确定我们的玩家本轮是赢了还是输了。这三个参数是我们刚生成的神秘数字、在我们的 React 应用程序中向玩家显示的数字以及玩家猜测的神秘数字是否比他们的数字更高或更低。我们函数中的第一件事是使用if-else if语句来确定玩家是否猜测更高还是更低,然后执行相应的逻辑。

这是一个复杂的情况,因为这是唯一的两种可能性,但是以这种方式构建可以防止玩家在猜测高或低以外的情况下操纵游戏并赢得胜利。当你编写代码时,要记住这个重要的概念:要明确。一旦我们评估了玩家的猜测,我们就评估他们是否正确,并在他们赢了时返回 true,在他们输了时返回 false。

你可能注意到这个函数还不完整。例如,如果玩家猜高了,而数字实际上更低,我们没有处理这种情况。为此,我们需要一个额外的else if条件。我们故意在这里省略了它,这样我们就可以专注于这个块,而不必在屏幕上显示太多代码,以至于混淆了概念。它肯定会出现在我们游戏的最终代码中。

事实上,剧透警告!你将编写代码!现在,当我们结束第二天时,是时候完成你的作业了,在这里,你将有机会应用我们今天学到的一切。

作业

好了!让我们来看看今天的作业。在这个作业中,你将有机会应用我们今天在 Solidity 中学到的一些关于变量的概念:

  1. 你要做的第一件事是切换到第二天的 Git 分支,在那里我设置了一些你需要访问的场景。为此,你将在下载了本书代码的目录中打开一个终端,并输入以下命令:
git checkout -b dayTwo
  1. 你还需要确保 Ganache 正在运行;你昨天安装了它,今天需要它来验证你是否正确完成了练习。

  2. 如果你运行命令truffle test,你会看到一堆错误,如下面的截图所示:

我在这个分支中创建了一些测试,如果你正确完成作业,所有这些错误都将消失,你将看到四个通过的测试。

  1. 要获得那些通过的测试,你需要使用编辑器打开contracts文件夹中的Gaming.sol Solidity 合约。

  2. 在那个合约中,你将创建一个名为onlinepublic布尔变量,并查看同一文件中的owner变量,如果你需要提示。

  3. 接下来,我们将创建一个名为Player的结构体,记住我们用大写字母命名结构体。创建具有以下成员的结构体:一个名为playerAddress的以太坊地址类型,一个名为playerName的字符串,一个名为playerBalance的无符号整数,一个名为playerWins的无符号整数,以及一个名为playerLosses的无符号整数。

  4. 一旦完成了这一步,我们将创建一个名为playerspublic映射类型,将无符号整数作为键映射到类型Player

最终结果是,这将给我们一个名为 players 的键值对对象,其中键是无符号整数,值是表示我们游戏中一个玩家的 player 结构的实例。

如果你遇到困难,请查看今天章节中关于 Solidity 中的数据类型 部分寻求帮助。

  1. 一旦你正确完成了所有这些步骤,你可以运行truffle test命令,并查看如下截图所示的通过测试:

摘要

恭喜!你已经完成了本书的第二天。我们学习了 Solidity 中的各种数据类型以及它们的使用方法。我们还学习了 Solidity 中的运算符。最后,我们学会了如何在我们的游戏应用程序中实现它们。

给自己一个鼓励,并休息一下,因为在我们的下一章中,我们将深入探讨 Solidity 函数,这些函数是智能合约业务逻辑的基石。我们将使用它们来实现我们这里的小型在线游戏的规则,同时,我们将学习 Solidity 函数的基本原理。

第三章:第三天 - 在您的智能合约中实现业务逻辑

好了,今天就讲函数。我们将更深入地了解如何创建它们,它们是如何工作的,以及我们如何使用它们来创建使我们的智能合约工作的业务逻辑。

本章将涵盖以下主题:

  • Solidity 函数

  • 向函数添加代码

  • 函数可见性

  • 使用函数执行业务逻辑

  • 了解修饰符

Solidity 函数

函数是合约内的可执行代码单元。要创建一个函数,您需要指定function关键字,给函数命名,指定完成其工作所需的任何参数,设置其可见性,添加所需的任何修饰符,声明它是一个视图还是纯函数,如果它可以接收以太币则标记为可付款,并定义任何将产生的返回变量;然后,我们有开放和闭合的花括号,在这些花括号之间是我们将添加函数代码的地方。并非每个函数都具有所有这些选项 - 这只是您进行操作时它们的顺序。例如,您不能将函数标记为纯函数且可付款,因为纯函数不允许修改状态 - 而接收以太币会这样做。在今天的课程中,随着我们使用更多的函数,这将变得更加清晰:

function myCoolFunction(unit aNumber) external myModifier view payable returns (bool) {
    //Write cool code here
}

我们在函数中使用return关键字表示函数将向调用方返回一个值。例如,我们可以声明一个名为sum的无符号整数,其值来自addThis函数的结果,该函数接受两个参数:

unit sum = addThis(4, 2);

我们声明它是一个函数,命名为addThis,指定它以两个无符号整数(unit a, unit b)作为参数,将其标记为internal,然后指定它返回一个无符号整数。所以,这个returns关键字不会返回任何东西,它只是指定了函数签名,只有在函数体中使用return关键字并实际将值返回给调用者时才会出现,本例中的return a + b即为变量sum。函数签名中的returns关键字和返回本身两个部分都是必需的。我们的加法函数可能如下所示:

function addThis (unit a, unit b) internal returns (unit) {
         return a + b;
}

我们也可以像这样指定返回,即我们说这个函数将返回c变量,然后在函数块内部,我们将c变量分配给a + b的总和并返回它:

function addThis (unit a, unit b) internal returns (unit c) {
          c = a + b;
           return c;
}

向函数添加代码

函数为合约创建应用逻辑;这意味着它们必须执行某些操作,并告诉它们该做什么意味着编写代码。你编写的代码放在函数的花括号之间。花括号内部,代码的执行一次从上到下一行一行地进行。唯一的例外是如果你有条件逻辑,比如这个if语句。如果isWinner不为真,那么花括号内的这些代码行将被跳过,程序执行将在if块之后的第一行代码处恢复。当函数到达最后一行代码或返回语句时,函数退出:

function winOrLose(unit display, bool guess, unit wager) external payable returns (bool) {
           /* Use true for a higher guess, false for a lower guess */
           require(online == true);
           require(msg.sender.balance > msg.value, "Insufficient funds");
           unit mysteryNumber_ = mysteryNumber();
           bool isWinner = determineWinner(mysteryNumber_, display, guess);
          if (isWinner == true) {
        /* Player won */ 
         msg.sender.transfer(wager * 2);
         return true;
         } else if (isWinner == false) {
         /* Player lost */
         return false;
         }
}

变量作用域

这带我们到一个非常有趣的点:叫做变量作用域的东西。看一下这个。我们有一个叫做saySomething的变量,值为"hello"。在这个doStuff函数内部,我们有另一个叫做saySomething的变量,值为"goodbye"。那么,当我们在这个函数内部时,你觉得saySomething的值是什么?如果你说是 goodbye,你是对的,在函数内部的saySomething变量被称为遮蔽了函数外部相同变量名的变量,并且你可以看到这是个坏事。当这个函数退出时,saySomething的值现在恢复到了原始值"hello";这是因为在这个函数内声明的变量只存在于函数内部。一旦函数退出,这些变量就消失了。在doStuff函数之外,甚至没有一个叫做saySomethingElse的东西可以被访问。在构建函数时,记住一个重要的点:函数内部需要哪些变量,以及函数退出后需要哪些数据:

string saySomething = "hello";
function doStuff() internal {
         string saySomething = "goodbye";
         string saySomething = "I have nothing else to say";
}

//saySomething is "hello"
//saySomethingElse doesn't exist

事件

还有一种特殊类型的函数叫做事件;它是我们使用以太坊虚拟机日志设施的一种方式。这对我们很重要,因为,如果你记得的话,当我们的玩家在游戏中采取行动时,它并不是实时的;它会发送到以太坊网络,在那里等待矿工确认,然后才会被写入区块链。当发生这种情况时,与该交易相关的任何事件都会被触发。我们可以利用这些来调用 JavaScript 回调函数并更新玩家的 UI。事件是合约的可继承成员,这意味着任何写入合约的事件都可以被从中继承的任何合约使用。最后,事件参数本身存储在交易日志中;这是区块链的一个特殊数据结构,我们可以看到其中哪些事件作为交易的一部分触发了。

让我们来看一个真实的事件,以更好地理解我的意思。在我们的合约内部,我们使用event关键字定义一个事件,给它一个名字——注意这里的名字以大写字母开头:PlayerWon——然后为我们想要索引的数据点添加参数:

event PlayerWon(address player, unit amount);

在我们的winOrLose函数中,一旦我们确定玩家赢了,我们就可以省略玩家获胜事件,将玩家的地址和他们赢得的金额写入事务日志。在我们应用的 JavaScript 中,我们可以监听此事件,当我们收到时让玩家知道这个好消息:

function winOrLose(unit display, bool guess, unit wager) external payable returns (bool) {
           /* Use true for a higher guess, false for a lower guess */
           require(online == true);
           require(msg.sender.balance > msg.value, "Insufficient funds");
           unit mysteryNumber_ = mysteryNumber();
           bool isWinner = determineWinner(mysteryNumber_, display, guess);
          if (isWinner == true) {
        /* Player won */ 
        emit PlayerWon(msg.sender, msg.value);
        msg.sender.transfer(wager * 2);
         return true;
         } else if (isWinner == false) {
         /* Player lost */
         return false;
         }
}

构造函数

我想介绍给你的另一个特殊函数是构造函数。当合约被创建时调用它,它只能执行一次。一旦合约被创建,它就永远不能被再次调用。它通常用于设置合约使用的变量的初始状态。你会看到一些例子,每个合约只允许一个构造函数,因此不支持重载。

在这里,我们有我们的游戏合约的一部分,在合约内部,我们声明了两个变量,owneronline

contract Gaming {
    address owner;
    bool online;
}

接下来,我们声明我们的构造函数,这样当这个合约被创建时,我们将把owner变量设置为部署此合约的人的地址。同时,我们将把online变量设置为true,表示我们的游戏正在营业。这两个变量对我们很重要,使用构造函数在合约创建后尽快设置它们的值使我们能够尽快锁定它们的值。你将看到的另一种变体是一个与合约同名的函数。所以,不是这个constructor函数,你会看到一个名为Gaming的函数。它做同样的事情,但使用contract名称作为函数名来创建构造函数已经被弃用,不应再使用。不过,很多代码仍然在使用,所以我想让你知道,这样你在看到它时就能识别出来:

constructor() public {
    owner = msg.sender;
    online = true;
}

Fallback 函数

最后一个我们要讨论的特殊函数是fallback 函数。合约可以有一个未命名函数,即我们的 fallback 函数。它不能有任何参数,也不能返回任何东西,如果合约被调用而合约中没有与调用匹配的函数,则执行它。当合约接收到以太币但没有数据时也会执行它。你可能会觉得这听起来在这一点上没什么用,但让我给你举个例子,说明为什么你可能需要包含一个。

想象一下我们的游戏合约。假设有人向这个合约发送了以太币,但没有附带任何数据。我们的合约不知道该怎么办,所以就撤销了交易,以太币退还给调用者。但如果这是另一个应用程序,而且在该应用程序中,您需要能够通过直接从外部账户进行转账(例如直接从某人的以太坊钱包)接受以太币怎么办呢?唯一的方法就是使用降级函数。创建一个降级函数,并将其标记为payable,可以使您的合约接收以太币的直接转账。然而,这样做的缺点是您需要考虑如何处理这些以太币:主要是,您必须有一种方法将其取出。例如,如果有人错误地将以太币发送到您的合约,而他们本来是想把它发送到另一个地址,如果您的降级函数被标记为payable,您将收到这笔以太币,如果没有一种允许您提取它的功能,它就会永远被困在那里:

contract Gaming {
         function() public payable {
         }
}

好的,这就是函数的基础知识。在下一节中,我们将介绍函数的能见度。让我们深入探讨能见度如何影响函数的功能。

函数能见度

在定义函数时,visibility关键字是一个必需的元素。通过指定能见度,我们可以控制谁可以调用它,谁可以继承它。我们还可以选择性地定义函数是否应读取状态变量,或者根本不查看它们。

能见度简要说明

我们昨天定义了能见度修饰符,今天我们将使用这个表格来巩固它们之间的区别:

外部调用 内部调用 可继承 自动获取器
外部
公共
内部
私有

外部函数可以从外部调用。它们也可以使用this关键字在内部调用,但因为这有点绕过的方式,我没有选择内部调用;它们是可继承的,所以您可以在继承的合约中访问所有外部函数,并且任何从您继承的合约都将包含在您的合约中定义的外部函数。公共函数既可以从外部调用,也可以从内部调用。像外部函数一样,它们是可继承的,而且对于定义为公共的变量,您会得到一个免费的获取器函数。内部函数只能从内部调用,并且它们是可继承的。最后,私有函数只能从内部调用,但请记住,这并不意味着数据是私有的,这只是意味着它不能被调用或继承;在区块链上,它仍然是对观察者可见的。

让我们看看一个外部函数。这是我们游戏中的 winOrLose 函数:它由我们的 React 应用程序调用,以确定玩家是否赢得了本轮游戏。它被标记为外部,因为我们将从我们的 UI 中调用它,并且它被标记为可支付,因为玩家将在此函数调用中包含他们的赌注。它执行此逻辑以确定玩家赢还是输,然后如果他们赢则返回布尔值 true,如果他们输则返回布尔值 false。因此,此函数也可以标记为公共并保留相同的功能。此外,我们将能够从合约内部调用该函数,而无需使用 this 关键字。因此,要从合约内部调用该函数,我们只需调用 winOrLose,但是由于它被标记为外部,如果我们想从合约内部调用它,我们将不得不调用 this.winOrLose。由于此函数没有理由从合约内部调用,我将可见性设置为 external

function winOrLose(unit display, bool guess, unit wager) public payable returns (bool) {
           /* Use true for a higher guess, false for a lower guess */
           require(online == true);
           require(msg.sender.balance > msg.value, "Insufficient funds");
           unit mysteryNumber_ = mysteryNumber();
           bool isWinner = determineWinner(mysteryNumber_, display, guess);
          if (isWinner == true) {
        /* Player won */ 
        emit PlayerWon(msg.sender, msg.value);
        msg.sender.transfer(wager * 2);
         return true;
         } else if (isWinner == false) {
         /* Player lost */
         return false;
         }
}

我们的 mysteryNumber 函数是私有的。它由我们的 winOrLose 函数调用,用于生成玩家押注的神秘数字。它仅在合约内部被 winOrLose 函数调用,因此不需要外部或公共可见性。另外,我不希望继承的合约能够访问这个函数,因此将其标记为 private。这里有一个新关键字 view。将此函数标记为 view 表示该函数不会尝试修改、创建或更新状态:

function mysteryNumber() private view returns (unit) {
         unit randomNumber = unit(blockhash(block.number-1))%10 + 1;
         return randomNumber;
}

视图函数

在以下列表中,我们可以看到被视为修改状态含义的事物,并且如果您的函数是视图函数,则不允许这些事物:

  • 写入状态变量

  • 发出事件

  • 创建其他合约

  • 使用自毁

  • 通过调用发送以太币

  • 调用任何未标记为视图或纯的其他函数

  • 使用低级别调用

  • 使用包含两个操作码的内联汇编

低级别调用和内联汇编操作码超出了本课程的范围,因此我们不会在此处涵盖它们。

最后,我们有一个内部函数,我们的 determineWinner 函数。此函数评估本轮的条件,即神秘数字、显示给玩家的数字以及他们猜测的高低。它的返回值是一个布尔值,当调用 winOrLose 函数时返回给它。除了 winOrLose 函数以外,没有理由让其他任何函数调用此函数,尤其是外部调用,因此将其定义为外部或公共是不可能的,而且我不介意继承我的合约的人使用此函数,因此我将其标记为 internal。它还被标记为 pure

function determineWinner(unit number, unit display, bool guess)
    internal pure returns (bool) {
    if (guess == true} {
        if (number > display) {
            return true;
        }
    } else if (guess == false) {
        if (number > display) {
            return false;
        }
    }
}

纯函数

纯函数非常类似于视图函数,因为它承诺不修改状态,但它更进一步承诺甚至不读取状态。

如果将函数标记为纯函数,则不能执行以下操作:

  • 读取状态变量

  • 访问账户余额

  • 访问任何块、tx 或消息的成员

  • 调用任何未标记为纯函数的函数

  • 使用包含特定操作码的内联汇编

在下一节中,我们将看到如何将所有内容结合起来创建我们应用程序的业务逻辑。

使用函数执行业务逻辑

因此,我们对函数如何工作以及如何创建它们有了一些信心,现在让我们将这些放入实际情境中,看看我们如何使用它们来实现智能合约的业务逻辑。回想一下我们的应用程序,我们将在 UI 中向玩家显示一个随机数,他们将对他们认为的神秘数字是更高还是更低下注。显示的数字和他们的赌注被发送到我们的智能合约,我们在其中使用 Solidity 代码执行游戏规则:

让我们再次看看我们的winOrLose函数。自从上次看到它以来它有些变化。当玩家准备玩一轮时,他们会下注,应用程序将调用这个函数。当它这样做时,将发送给玩家显示的数字,玩家的猜测,以及他们的赌注作为特殊的msg.value变量附加到此交易。由于这是从我们的 UI 调用的,必须标记为 external,而且由于它以以太形式收到他们的赌注,必须标记为 payable。

我们定义返回两个对象:一个布尔值和一个无符号整数。布尔值表示他们赢了还是输了,无符号整数将返回他们下注对手的mysteryNumber_。这将允许我们在告诉玩家他们赢了或输了时显示mysteryNumber_

function winOrLose(unit display, bool guess, unit wager) external payable returns (bool, unit) {
           /* Use true for a higher guess, false for a lower guess */
           require(online == true);
           require(msg.sender.balance > msg.value, "Insufficient funds");
           unit mysteryNumber_ = mysteryNumber();
           bool isWinner = determineWinner(mysteryNumber_, display, guess);
          if (isWinner == true) {
        /* Player won */ 
        emit PlayerWon(msg.sender, msg.value);
        msg.sender.transfer(msg.value * 2);
         return (true, mysteryNumber_);
         } else if (isWinner == false) {
         /* Player lost */
         return (false, mysteryNumber_);
         }
}

在我们的函数内部,我们有两个 require 语句。我会跳过这些,因为我们将在下一节详细介绍它们。

我们有一个无符号整数从名为mysteryNumber的函数获得其值;这意味着我们的mysteryNumber函数必须返回一个无符号整数 - 在定义函数时我们声明这一点 - 而且由于我们的合约外部不需要访问这个函数,我们将其标记为私有。我们的函数也不对状态进行任何修改。它只是返回一个数字,因此我们也可以将其标记为view函数:

function mysteryNumber() private view returns (unit) {
         unit randomNumber = unit(blockhash(block.number-1))%10 + 1;
         return randomNumber;
}

现在我们已经拥有确定玩家本轮是否赢得或输掉所需的所有数据。因此,我们声明一个名为isWinner的布尔值,通过将所有所需信息发送到determineWinner函数来获取其值,该函数如下所示:

function determineWinner(unit number, unit display, bool guess)
    internal pure returns (bool) {
    if (guess == true} {
        if (number > display) {
            return true;
        }
    } else if (guess == false) {
        if (number > display) {
            return false;
        }
    }
}

它有参数接受一个神秘数字,显示的数字和玩家的猜测。再次强调,合约之外没有任何原因调用这个函数,因此它被标记为internal,并且因为它不读取或修改状态,我们将其标记为pure。接下来,我们遍历所有不同的获胜和失败的组合,一旦我们有足够的信息来确定这一轮是赢还是输,我们使用return退出函数,然后true表示赢,false表示输。当函数返回时,它留下了我们在这里,我们可以评估isWinner变量,然后根据赢或输采取适当的行动,这包括发出事件表示该轮的状态,将赢得的任何钱返还给玩家,并返回玩家看到的结果。让我们用视觉来看这个来帮助巩固这些关系。我们的应用程序调用winOrLose函数,然后从mysteryNumber_函数获得一个新的神秘数字,然后将数据发送到determineWinner函数,以查看玩家是否赢了或输了,然后采取适当的行动,最后通知玩家结果。接下来,我们将讨论修饰符。它们是强大的函数,允许您在何时执行函数时放置约束。

理解修饰符

好吧,修饰符是函数中的重要组成部分:它们允许我们快速轻松地强制执行规则和限制。修饰符用于以声明方式改变函数行为。这意味着我们获得了一种重复简洁的方式来强制执行规则。修饰符通常用于在执行函数之前检查条件;这在区块链开发中非常重要。我们支付矿工以气体形式执行我们的函数,所以如果函数将违反约束,最好尽快失败。

还有可继承的属性,这意味着在合同中定义的修饰符也可在任何从中派生的合同中使用。修饰符的一个重要组成部分是一个方便的函数称为require。因此,在我们深入了解修饰符之前,让我们首先了解require函数。

require 函数

require是我们在 Solidity 中可以使用的方便函数来处理错误。它是一个状态回滚异常,这意味着在异常发生之前对状态的任何更改都将自动回滚;这是一个很棒的功能,因为它确保我们的所有交易完成,或者都没有完成,从而防止我们不得不猜测哪些部分完成了的情况发生。我们通常使用require来确保满足有效条件,例如输入或合同状态变量。并且,可选地,当require语句失败时,我们可以包含一个返回给调用者的字符串消息,以告知调用者发生了什么。对我来说,虽然这个流消息是可选的,但它是一个要求;在这个特性存在之前,我甚至都无法猜测我花了多少小时去排除一个函数失败的原因,只是后来才了解到这是由于我在函数中放置的要求。要定义一个require,我们使用require关键字,后跟括号中的条件。

如果您还记得我们的合同定义,我们设置了一个名为online的变量,然后在构造函数中将其值设置为true,因此此语句检查online变量是否仍为true。如果不是,则程序执行停在此处,这是我们如何有效地使我们的游戏离线的方法:

require(online == true);

顺序在require语句中也起着重要作用。通常,您希望将它们放在函数的顶部,以便如果条件不满足,函数能够尽快失败。在这里,您可以看到两个require语句,一个用于验证游戏是否在线,第二个用于确保玩家有足够的资金来支付他们的赌注,就是这样。require语句非常简单直接。

我们可以使用require语句来构建修饰符。修饰符是使用modifier关键字创建的,给它一个名称,然后在括号中加上可选的参数。这与函数非常相似。在修饰符中,我们添加条件。在这里,我们要求消息发送者与存储在owner变量中的地址相同。这个修饰符的效果是限制使用它的任何函数只能由合同的所有者执行。修饰符的最后一部分是这个下划线,它的位置非常重要,因为它决定了调用此修饰符的代码何时应该执行:

modifier isOwner() {
   require(msg.sender == owner);
   _;
}

让我举个例子来进一步解释。这个函数允许调用者提取合同从游戏中赚取的资金。我们使用isOwner修饰符确保只有合同的所有者可以调用它:

function withdrawFunds() public isOwner {
     msg.sender.transfer(address(this).balance);
}

现在让我们看看下划线的作用。下划线有两个有效的位置:

它可以出现在修饰符的开头:

modifier isOwner() {
    _;
    require(msg.sender == owner);
}

或者它可以出现在修饰符的末尾:

modifier isOwner() {
    require(msg.sender == owner);
     _;
}

区别在于函数中的代码何时运行。如果你将下划线放在修饰器的开头,当提取资金函数执行时,它会运行其函数中的所有代码,然后调用修饰器。如果下划线在结尾,修饰器首先执行其逻辑,然后函数执行其逻辑。

另一种看待这个问题的方式是下划线代表函数本身。在这个例子中,如果我们先放置下划线,函数首先执行并从合同中提取所有资金,然后修饰器检查是否是所有者这样做了。将下划线放在最后,修饰器首先检查调用是否来自所有者,然后在修饰器通过时执行函数。这对于函数的期望结果有很大的区别,所以在使用修饰器时要特别注意。

在下一节中,我们将看一些使用修饰器的实际例子,以更好地理解它们的能力。

在本节中,我们将看一些修饰器的不同用例。我非常喜欢修饰器,因为它们允许对执行函数进行很多控制,但它们仍然非常易读。让我们看看我们的第一个例子,一个名为onlyBy的修饰器。它将只允许调用它的函数在被所需地址调用时执行:

modifier onlyBy(address _account) {
           if (msg.sender != _account) {
                   revert();
           }
           _;
}

我们可以在changeOwner函数中看到它的运作方式。当调用changeOwner时,onlyBy修饰器用于确保只有当前所有者才能为合同指定一个newOwner变量:

function changeOwner(address _newOwner) onlyBy(owner) {
    owner = _newOwner;
}

这里有另一个例子,使用时间限制。如果当前时间小于作为参数传递的时间,修饰器将抛出异常结束程序执行。将其用于实践,我们可以看到disown函数使用onlyAfter修饰器确保只能在创建时间之后的 6 周内调用它。这是使用多个修饰器确保满足多个条件的绝佳示例。要指定多个修饰器,你只需在函数声明中依次列出它们。你可以像这样每行一个,或者在同一行用空格分隔每个修饰器:

modifier onlyAfter(unit _time) {
    if (now < _time) revert();
    _;
}

function disown()
     onlyBy(owner)
     onlyAfter(creationTime + 6 weeks)
{
     delete owner;
}

让我们看一个更多的例子。这个修饰器要求在函数调用时支付一定的费用,所以现在我们可以收取更换所有者的费用。只要这个交易中包含所需的 200 以太币,交易就会执行。但这里有一个警告:如果调用者在函数正常退出时发送的以太币超过 200,多余的以太币将返回给调用者。不过,如果我们明确使用return,多余的以太币就不会被返回:

modifier costs(unit _amount) {
    if (msg.value < _amount) {
        revert;
        _;
    }
    if (msg.value > _amount) {
        msg.sender.send(amount - msg.value);
    }
}

function forceOwnerChange(address _newOwner) costs(200 ether) {
    owner = _newOwner;
    if(unit(owner) & 0 == 1) {
       return;
    }
}

好了,我们已经讨论了函数,虽然我们还可以讨论更多,但这代表了你开始构建智能合约所需的基础知识。在下一节中,我将和你一起讨论今天的家庭作业。

作业

今天的作业与昨天的格式类似。在其中,你将写一些代码来使一些测试通过,在书的存储库中。这样做将允许你把今天获得的关于函数的知识应用到实践中。你首先要做的事情是:

  1. 为今天的作业搭建好你的环境。

  2. 打开终端并切换到包含你的应用程序代码的目录。

  3. 输入git stash命令。这将把你对应用程序代码所做的任何更改都存储起来,以防你所做的工作干扰我为你设置的情景。

  4. 将你的工作存储起来后,输入git checkout -b dayThree来获取今天作业的代码,你还需要确保 Ganache 在运行,所以在开始作业前确保你已经启动它。

  5. 运行truffle test,你应该会看到类似于以下截图的内容:

要让所有这些测试通过,你需要做以下任务 - 在determineWinner()函数中,我们缺少一些逻辑。如果玩家猜测神秘数字将低于他们屏幕上显示的数字,他们就应该赢,但这个函数目前不会执行此操作,所以我需要你编写代码来实现这一点。

  1. 接下来,我们将创建两个新事件:一个是playerWon,另一个是playerLost。事件应该接受玩家的地址、他们押注的金额、他们所押注的神秘数字以及向玩家显示的数字的参数。

  2. 现在你有了你的事件,每当玩家赢了或输了,我们就会省略出正确的事件。

最后,还有一个名为players的映射,将玩家的地址映射到一个Player结构中,其中我们可以存储关于他们的胜利和失败的信息。

  1. 我们将从该映射中获取正确玩家的Player结构,并且如果他们赢了,则增加赢的计数器,如果他们输了,则增加输的计数器。

当你完成所有工作后,你应该会拥有这六个通过的测试,这些测试为第四章 第四天创建测试做好了铺垫。

这里有个很酷的地方,就是在过去的几天里,为了确保你的代码正常运行,你一直在依赖测试,所以你可能会对它们的重要性以及它们如何能够帮助你编写更少 bug 的更好的代码有所认识。

摘要

在本章中,我们学习了有关函数的所有知识。我们还看了函数是如何创建的,它们是如何工作的,以及它们如何用于使智能合约生效。我们首先学习了 Solidity 中的函数,然后我们看到如何向这些函数中添加代码。然后,我们学习了如何修改函数的可见性。然后,我们看到了如何使用这些函数来执行业务逻辑。最后,我们学习了如何在函数中使用修饰符。

在下一章中,我们将学习如何创建测试,以帮助调试代码,使游戏功能无故障!

第四章:第四天 - 创建测试

你一直依赖测试来确保你的代码正常运行,所以你可能很欣赏它们的重要性,以及它们如何帮助你写出更好、更少错误的代码。在本章中,我们将看看如何创建适当的测试,以帮助我们的游戏顺利运行,没有任何问题。本章将涵盖以下主题:

  • 理解单元测试和集成测试

  • 不同应用程序的测试策略

  • 在 Solidity 中创建单元测试

  • 同一个要测试的函数有多个测试

  • 在 JavaScript 中创建集成测试

  • 运行测试套件

理解单元测试和集成测试

今天,我们将讨论有关测试的所有内容。在过去的几天里,你实际上体验到了测试的一些好处。我编写了一个测试,检查了我们智能合约中的特定行为,它失败了,然后你编写了一段代码,当测试通过时,你知道它提供了预期的结果。

为什么要写测试?

现在,想象一下规模更大的情况。你正在作为一个庞大的区块链开发团队的一部分编写代码,向世界交付最新的去中心化应用程序。作为团队的一部分,你如何知道你团队中其他开发人员编写的代码是否按照预期执行?当出现错误时,你如何确保你的代码不是导致错误的代码?或者这样想:如果有人更新了你的代码,你如何确保它仍然执行其预期的功能?请记住,当我们处理区块链应用程序时,我们正在处理人们的金钱,所以所有这些都是重要的问题,而所有这些问题的答案都是一样的:测试。

单元测试

今天我们将讨论两种不同类型的测试,所以我想先向你介绍它们,以便你了解每种测试的作用。单元测试是由开发人员编写的,用于测试相对较小的代码片段,以确保其按预期执行。想象一个将两个数字相加的函数:

function sum(unit a, unit b) returns
(unit) {
return a + b;
}

一个测试可能看起来像这样:

function testAddCorrect() public {
     unit expected = 4;
     unit result = myContact.sum(1, 4);
     Assert.equal(expected, result);
}

它给函数两个数字,然后测试正确的结果。

以下是单元测试的一些常见特征:

  • 范围较窄

  • 易于阅读和编写

  • 无依赖关系

这意味着测试本身完全自包含,不需要数据库、网络连接、手动干预或除了测试和被测试代码之外的任何其他东西。在 Solidity 中,我们实际上没有真正的单元测试,因为即使是基本的测试也需要 Ganache 或本地区块链网络才能正常运行。尽管如此,我们仍然以这种方式编写测试来确保我们的代码的正确运行。让我们来看看我已经编写的一个单元测试,以便更好地了解一个真正运行的测试是什么样子的:

在上述屏幕截图中,左侧是我们过去几天一直在使用的determineWinner函数。右侧是其测试。我们使用function关键字,为测试赋予以小写test单词开头的名称,然后在内部声明一个名为expected的变量,这是当函数正常工作时我们期望的结果,然后我们有另一个名为result的变量,这是函数的实际结果。然后最后一部分是我们使用一个断言或创建一个assert语句,比较预期结果或预期答案和实际结果,以确保它们相等,如果不相等,则显示消息,以便运行测试的人知道出了什么问题。

集成测试

另一方面,集成测试用于证明系统的不同部分如何协同工作。为了更好地说明集成测试,让我向您展示我为作业编写的测试代码:

it('Should record player losses', async() => {
    const gameRound = await gaming.winOrLose(10, true, {
        from: player1,
        value: web3.utils.toWei('1', 'ether')
    })
    const playerStats = await gaming.players(player1)
    assert.equal(playerStats[1].toNumber(), 1, 'The player should have 1 toss')
})

此测试以 JavaScript 编写。我们首先调用winOrLose函数,模拟从我们的 React 应用程序中使用所需参数进行调用。我们使用await关键字等待该调用写入区块链。此测试正在检查是否正确记录了玩家的统计信息。当他的代码完成时,玩家应该有一次记录的失败。因此,现在我们调用区块链并执行函数以获取玩家的统计信息,并验证记录的损失值是否等于一。

因此,在这个测试中,我们进行了两次区块链访问,并且我们依赖区块链网络正确运行以使测试通过。这是一个集成测试。以下是集成测试的一些常见特征。

它们表明了系统的不同部分是如何一起工作的。通常,它们涵盖整个应用程序,您可能还会听到它们被称为端到端测试。它们需要比单元测试更多的工作量来组合,而且它们还需要外部资源,例如数据库、硬件或在我们的情况下是区块链网络,并且它们更接近我们的用户预期执行的操作。

在下一节中,您将学习使用 Solidity 和 JavaScript 创建单元测试和集成测试的基础知识。有了这些技能,您将能够创建测试来确保您的合约确实执行了它应该执行的操作。

各种应用程序的测试策略

您知道应该编写测试,但当您面对空白屏幕时,有时很难知道从哪里开始。因此,在本章中,我们将探讨一些策略,帮助制定测试内容以及测试方法。我们将讨论四个不同的事项:

  • 测试成功情况

  • 测试失败情况

  • 使用 Solidity 进行测试

  • 使用 JavaScript 进行测试

在进行测试时,有很多不同的方法,但我们不要陷入信息的海洋中,让我们保持简单。任何测试总比没有测试好,所以让我们专注于为我们的合约编写一些测试,稍后我们可以随时在学到更多知识的情况下改进方法和测试。

测试成功

最容易入手的地方是测试成功,我指的是编写测试来确保你的组件在提供正确输入时执行其预期功能。考虑以下代码片段:

function sum(uint a, uint b) returns
(uint) {
return a + b;
}

这里,我们有一个函数,它将两个数字相加。你如何检查操作是否完全按预期工作?让我们编写一个测试来确保如果它被提供两个数字,它会产生正确的答案。为此,我们将创建一个名为testAddCorrect()的函数,考虑一个预期值和合约的结果值,并交叉检查它们以确保函数产生正确的答案。以下代码片段进一步说明了这个测试的工作原理:

function testAddCorrect() public {
    uint expected = 4;
    uint result = myContract.sum(1, 4);
    Assert.equal(expected, result);
}

编写这些测试应该成为你开发工作流程的自然一部分。事实上,有一种称为测试驱动开发的策略,你首先编写测试,看到它失败,然后编写代码使该测试通过。一旦它通过了,你再编写另一个失败的测试,然后跟着编写使其通过的代码。通过编写每一个失败的测试,你确保代码做了它应该做的事情,然后通过使每一个测试通过,你专注于编写使你的应用程序工作所需的最少代码量。这是我最经常使用的策略,效果很好,但测试失败也是有意义的。

测试失败

如果你的函数在提供无效输入时不执行正确操作呢?考虑以下 JavaScript 代码片段:

function sum(a, b) {
    return a + b;
}

这里,如果你给函数提供两个数字,它会把它们相加,但是,如果我们给它两个字符串呢?

我们可能要求它向调用者返回一个错误,说明只有数字是有效输入,但实际上,它会返回两个输入字符串连接在一起的结果。这就是我所说的测试失败。你需要一些额外的测试来覆盖当你的组件被提供无效输入时会发生什么。

大多数时候,错误和安全漏洞都来自于以一种未曾预期的方式使用组件。

使用 Solidity 进行测试

我们的第三个主题是使用 Solidity 进行测试。如果这本书是你对编程世界的第一次介绍,这可能是你最舒适的地方。

在 Solidity 中编写测试几乎与编写合约相同,因为它是相同的编程语言,事实上,您的测试只是由 Truffle 使用的另一个 Solidity 合约,用于执行您的测试。使用 Solidity 编写的测试类似于单元测试。每个测试套件或测试合约都在干净的环境中运行。这意味着,在运行每个测试套件之前,合约将重新部署到测试网络,这样您就知道您是从已知状态开始的。

由于每次运行测试时都会进行部署,因此对本地网络进行测试是有意义的,这是我们使用 Ganache 的主要原因之一。如果我们必须部署到实时网络,然后等待矿工挖掘每个交易,那么获得我们的测试结果将需要大量时间,如果我们对自己诚实,我们不会像应该那样经常运行测试。

Solidity 测试使用 Chai Assertion 库,用于编写逻辑以通过或失败我们的测试。您将在即将到来的部分在 Solidity 中创建单元测试中看到如何做到这一点。但从测试的角度来看,Solidity 的功能相当有限,它非常适用于测试单个函数,并确保函数返回正确的响应,并测试异常,但对于测试合约的整体行为来说效果不佳。为此,我们将使用 JavaScript 测试。

使用 JavaScript 进行测试

JavaScript 测试为我们提供了一种完全测试合约行为的方法,正如客户端将看到的那样。我们可以访问测试帐户,这要归功于注入到测试运行器中的web3提供程序,您将在第六章中了解到web3是什么,第六天:使用钱包

Truffle 使用 Mocha 测试框架和 Chai 断言来进行 JavaScript 测试。如果您之前已经写过 JavaScript,您可能对 Mocha 比较熟悉;这里唯一的区别是 Truffle 使用contract函数而不是 Mocha 的described函数。这使得前面提到的干净环境特性成为可能,以确保我们每个测试套件都从一个新的合约开始。

使用 Chai Assertion 库是一个不错的选择,因为它是我们 Solidity 测试中使用的相同断言库,这使得我们作为区块链开发者的生活稍微容易一些。现在,让我们深入探讨将合约部署到测试网络中。

在 Solidity 中创建单元测试

我们的第一个测试将使用 Solidity 编写。从 Solidity 开始可能会通过使用您已经了解的语言提供对这个陌生概念的熟悉度。

Solidity 测试约定

Solidity 中有一些关于 Solidity 测试的约定:比如文件必须具有.sol扩展名,合约名称必须以大写字母T开头的单词Test开头,函数必须以小写字母t开头,而test应该放在应用程序代码的test文件夹中。

要编写测试,我们必须首先进行一些清理工作。我们定义我们的合同然后导入truffle/Assert.sol库:我们将导入truffle/DeployedAddresses.sol库。如果你习惯编写 node 应用程序,这两者可能有点奇怪,因为通常这意味着库被导入并且是在node modules文件夹中找到的文件,但是你不会在那里找到它,因为它是由 Truffle 直接导入的。我们还需要导入我们将要测试的合同。对于我们的应用程序,这是我们的游戏合同。这真的是您想要停止导入东西的地方,因为我们希望保持我们的测试简约:

pragma solidity 0.5.0;
    import "truffle/Assert.sol";
    import "truffle/DeployedAddresses.sol";
    import "../contracts/Gaming.sol";

在此之后导入其他库只会引入复杂性和错误的潜在可能性,因此就像我们之前编写的合同一样,我们将定义一个新的合同。因为这是一个测试合同,我们将以 test 开头的名字开始。

现在我们可以做的一个很酷的事情是创建一个名为initialBalance的变量并为其分配一些以太。当我们的合同部署时,它将被资助指定金额,使其可供在我们的合同中使用。这很酷,对吧?然后我们创建一个名为gaming的变量,这是我们的大写字母 G 的Gaming合同的实例。看一下下面的代码:

contract TestGaming {
    uint public initialBalance = 10 ether;
    Gaming gaming;
}

我们可以定义一个名为beforeAll()的函数,注意它并不以单词test开头,尽管我刚才说函数必须以它开头。这是因为这是一个特殊的函数:它将在我们的测试套件中的任何测试之前运行。在其中,我们将获取部署的合同实例,并在我们的其余测试中使用它:

function beforeAll() public {
    gaming = Gaming(DeployedAddresses.Gaming());
 }

我们可以使用的其他特殊函数包括beforeEachafterAllafterEach。那么,现在,让我们开始编写一些测试。我们的第一个测试将测试我们的determineWinner函数。它被winOrLose函数调用,但我们将单独测试它以确保它正好完成其预期的工作,这样我们就知道winOrLose函数可以依赖它返回正确的响应。我们首先定义我们的函数,并以以test开头的单词给出名称,然后在我们的函数内部,我们将声明一个名为expected的变量。这是我们期望作为我们测试的结果找到的结果。结果通过调用determineWinner函数进行填充,并传递一些参数给它以执行所需功能:

function testPlayerWonGuessHigher() public {
    bool expected = true;
    bool result = gaming.determineWinner(5, 4, true);
}

现在,我们要介绍的是我们的 Chai 断言库。我们调用assert库,然后调用equal函数,传递我们的expected值,结果和当测试失败时要显示的消息。现在,这个消息非常重要:它将是您或任何其他开发人员在此测试失败时得到的唯一线索。确保它清晰而具体。这也是进行代码审查的好地方,因为让其他人对这些消息进行评审可以帮助使它们清晰易懂:

Assert.equal(expected, result, "The player should have won");

相同测试函数的多个测试

再看几个例子,除了我们刚写的测试之外,我们还有另外三个例子。在每个测试中,存在不同变量参数的不同变体,我们可以将它们提供给determineWinner函数,这使我们能够检查我们的determineWinner函数可能遇到的每种情况。这就是为什么 Solidity 的测试器受欢迎的原因:它们很容易编写,很容易阅读,并且它们使用与我们的合同完全相同的编程语言。不幸的是,如果您尝试做的事情超出了这些,乐趣和兴奋很快就会消失:

要进入测试的下一个级别,我们将使用 JavaScript。它具有许多 Solidity 不可用的功能。访问这些功能将赋予我们使用 JavaScript 进行端到端测试的能力和灵活性,这很方便,因为我们也将使用 JavaScript 编写我们的 UI。

在 JavaScript 中创建集成测试

在 JavaScript 中编写测试时,我们不仅可以模拟合同调用,就像我们在 Truffle 测试中所做的那样,还可以选择不同的帐户,检查余额等等。让我们深入了解一些测试,你会对我所说的有更好的理解。我们将从创建一个新的测试文件开始。对于我们的 Truffle 测试,文件名以大写的 T 字母开头,并以.sol扩展名结尾。

我们的 JavaScript 测试以被测试的合同名称开头,后跟一个以大写 T 开头的单词test,并以.js扩展名结尾。它仍然放在与我们的 Solidity 测试相同的test文件夹中,这意味着无论这些测试使用哪种语言编写,都只需查看一个地方。

在我们的文件中,我们将创建一个与要测试的合同同名的常量,并使用需要该合同内容的 artifacts。从这里开始,我们的测试看起来很像 Mocha 测试,如果您熟悉的话;不过,我们将使用 Truffle 关键字contract而不是使用笔:

const Gaming = artifacts.require('./Gaming.sol')

这启用了 Truffle 的清洁房间功能,这意味着每次将此文件作为测试运行时,Truffle 框架都会将合同的新实例部署到网络上,确保我们从已知状态开始:

contract('Gaming', async (accounts) => {

现在我要声明一些变量。gaming 变量将代表部署到网络上的我们合约的版本,然后我将创建两个常量,ownerplayer 1。这两个常量都从一个叫做 accounts 的数组中获取。accounts 变量是通过 Truffle 框架免费提供的,数组中的项目代表了应用程序启动时 Ganache 为我们创建的 accounts,所以这个叫做 owner 的变量被设置为帐户数组中的第一项,也就是你在 Ganache 中查看时看到的第一个帐户,而 player 1 变量则是第二个列出的帐户。这相当强大,因为访问这些帐户允许我们以这些帐户的身份采取行动,然后与 Ganache 回顾以确保事情按照我们的期望发生。它允许我们测试一些只适用于特定帐户而不适用于其他帐户的功能,比如我们的 is owner 函数:

 let gaming
 const owner = accounts[0]
 const player1 = accounts[1]

现在我们将有一个 before 函数,和 Truffle 中的对应函数一样,这个函数将在这个文件中编写的任何测试之前运行。如果你有其他测试文件,它们将被视为单独的运行,这里的 before 函数不会应用:

before(async () => {
 gaming = await Gaming.deployed()
})

然后我们将使用 asyncawait 来从以太坊网络中获取我们合约的部署版本。所以,让我给你解释一下 asyncawait,以防你之前没有见过。假设我们有一行 JavaScript 代码:它将使用我们导入的代表合约的 artifact 来获取部署在以太坊网络上的实际合约实例,但 JavaScript 的工作方式是一旦我们调用了这个函数,它就认为已经完成并且从这里继续了:它是异步的。所以,即使我们调用了部署函数并且它还没有返回值,JavaScript 也会继续向前移动。直到这个调用完成,gaming 变量实际上是未定义的,这会在你尝试弄清楚为什么这个变量有时有值,有时没有值时带来很多头疼:

gaming = Gaming.deployed()
const fundGame = gaming.fundGame()

为了避免这种痛苦,我们使用 asyncawait。它的工作方式是我们在这里使用 async 关键字声明这个匿名函数,然后在函数内部,每当我们需要等待的函数或调用时,我们使用 await 关键字。现在,在幕后有比这更复杂的事情,但这是你需要了解的最基本的知识。除了 asyncawait,你可能会看到的其他模式包括回调和 promises。

现在我们又声明了一个变量,一个名为fundGame的常量。这个函数让我可以向合约发送一些初始的以太币,这样当我们开始测试我们的合约时,合约就有一些资金来支付任何赢家的奖金。如果没有这些以太币,任何导致获胜场景的测试都会失败,因为合约没有足够的资金来支付奖金。看看这个:它也使用了await关键字,因为一旦我们调用这个函数,执行并不意味着完成。我们需要等待该块被挖掘,然后操作才被视为成功:

const fundGame = await gaming.fundGame({from: owner, value: web3.utils.toWei('10', 'ether')})
 })

现在,我们终于准备好编写一些 JavaScript 测试了。我们的测试以单词it开头,然后是一句描述应该发生什么的句子。这里通常惯例是实际使用单词should,这样它就像一句句子一样阅读;在这种情况下,它应该记录玩家的损失,所以让我们看看我们如何做到这一点。我们声明了一个名为gameRound的常量,然后我们再次使用await调用我们游戏合约中的winOrLose函数。请记住,这是我们的 UI 将要调用的同一个函数,因为我们的玩家在玩游戏,所以我们实际上在这里模拟真实的用户行为。我们的winOrLose函数接受两个参数:显示给玩家的屏幕上的数字以及他们对于神秘数字是更高还是更低的猜测。我希望这个测试确保当玩家输掉时记录的损失数量增加;这意味着我需要确保当winOrLose函数返回时,这是一个输掉的回合。我可以通过向用户显示数字为 10,并指示他们猜测神秘数字将更高来实现这一点。

嗯,因为我们的神秘数字是一个从09的个位数,所以它不可能比十更高,确保我们的测试玩家总是会输。这个函数调用的下一个重要部分是一个可选的第三个参数。前两个参数在我们的函数调用中定义。这第三个参数来自 Solidity,并且采用 JavaScript 对象的形式。在其中,我们指定我们的from账户,表示我希望这个交易来自哪个账户,这就是我们的玩家 1。我还可以附加一个代表玩家赌注的值。现在所有发送到以太坊网络的资金都以 Wei 为单位,如果你还记得第一天,这意味着十的十八次方 Wei 等于一个以太币。但是与其自己计算这些数学,Truffle 在测试时为我们提供了一个 Web3 实例来使用。

Web3 是一个用于与以太坊网络上的智能合约交互的 JavaScript 实用程序库,所以我们可以使用web3.utils.toWei函数将一个以太币转换为 Wei,并保持可读的代码。这启动了游戏的一轮与我们的玩家。由于这个await关键字,我们的代码的执行将在这里等待该轮完成,一旦完成,我们就可以创建一个新的常量叫做player stats。这是您昨天创建的用于增加胜利和失败次数的结构:

it('Should record player losses', async() => {
    const initialBalance = await gaming.winOrLose(10, true, {
        from: player1,
        value: web3.utils.toWei('1', 'ether')
 })

Players是一个将地址映射到player结构的映射,这意味着它以以太坊地址作为参数获取正确玩家的筹码。我们可以在这里使用player1变量名,Truffle 会自动将其转换为所需的地址参数。现在,我们可以最终使用assert来验证我们预期的数字是否等于 1。我们还可以在这里包括一条消息,如果测试失败,它将被显示。在这里,你可能会对这个感到好奇。我们的 players 映射返回一个包含玩家胜利和失败的结构,但 JavaScript 对结构一无所知,并且它从结构转换为数组,根据在结构声明中列出的变量的顺序进行处理。因此,我们知道当这个数组返回时,数组的第一项将是胜利,第二项将是失败:

const postBalance = await gaming.players(player1)
assert.equal(playerStats[1].toNumber(), 1, 'The player should have 1 loss')
 })

在 Solidity 和 JavaScript 中,数字也有一些类型差异。当我们从 Solidity 中获取一个数字时,无论它是有符号还是无符号整数,它都是一个大数。这实际上是 JavaScript 类型,不是我说它是一个大数,所以我们需要将其转换为 JavaScript 数字,这样我们就可以在我们的应用程序中使用它,我们使用toNumber函数进行转换。

所以,让我们再做一件事。既然我们在这里,让我们验证一下,当这个玩家输掉时,我们拿走了他们的钱。这是经营赌博业务的重要部分,我希望有一些测试来确保它工作正确。在我们玩这一轮之前,让我们获取玩家的账户余额;我们将使用web3.eth.getBalance函数,并提供我们玩家的地址:

const initialBalance = await web3.eth.getBalance(player1).toNumber()

现在,在我们玩这一轮之后,我们知道玩家已经输了,我们可以使用以下代码再次获取余额:

const postBalance = await web3.eth.getBalance(player1).toNumber()

现在,我们可以使用isAtLeast函数进行断言。我使用isAtLeast是因为除了玩家刚刚输掉的 10 个以太币外,他们还必须支付一些 gas 作为交易费用。因此,初始余额应该大于最终余额加上下注金额。他们的余额应该减少了些许,因为他们下注了 10 个以太币加上了 gas。这不是一个确切的数字,但足够接近,以确认玩家确实失去了我们期望他们失去的金额:

assert.isAtLeast(initialBalance, postBalance + 10, 'some message here')

我们可以在我们一直在使用的同一个函数中执行这个操作。在同一个函数中有多个断言是完全可以接受的,只要它们在测试你代码中的同一组件或函数。现在我们可以测试我们的函数,评估和断言结果,并在我们的测试网络中检查不同账户的余额。在下一节中,让我们看看如何让它们都协同工作。

运行测试套件

今天到目前为止,我们花了很多时间编写测试,但没有时间运行测试。当我编写代码时,我通常采取的方法是编写一个单独的测试,运行测试套件以确保它失败,然后编写必要的代码使其通过;这意味着我经常运行测试,而且很重要的是它们能够快速完成。你已经在运行测试了:你每天都使用它们来验证你的作业。当你运行 Truffle 测试时,它会运行测试,现在你知道这些测试是从哪里来的。

你也意识到,为了使这些测试通过,你必须让 Ganache 运行,所以必须存在 Truffle 测试和 Ganache 之间的某种通信:

好吧,让我们来看看幕后的情况。Truffle 能够与 Ganache 通信的原因在于这个文件——truffle.js——特别是Network部分。当你运行 Truffle 测试时,除非另有说明,它会假定我们的开发配置为开发网络:我们指定了 localhost 的地址和端口为 7545,这是 Ganache 运行的端口。

最后,我们告诉它使用 Ganache 提供的任何网络 ID,这将不同于你将在第七章中了解到的其他配置,第七天 - 部署到网络。如果你更改了 Ganache 中的任何设置,或者决定使用其他本地以太坊客户端,你需要在这里更新这些设置,以确保 Truffle 知道如何与其通信。至于运行测试套件,就是这样了。键入以下 Truffle 测试:

module.exports = {
    migrations_directory: "./migrations",
    solc: {
    optimizer: {
        enabled: true,
        runs: 2000
        }
 },
 networks: {
     development: {
         host: "127.0.0.1",
         port: 7545,
         network_id: "*" // Match any network id
     },
     ropsten: {
         host: "127.0.0.1",
         port: 8545,
         network_id: 3,
         from: "0xe7d6c3f43d7859d7d6d045f9ac460eedffd3eae6"
     }
  }
};

但既然我们在这里,让我问你一下:如果测试失败了,你会怎么做?在你考虑如何处理时,让我向你展示使用 Truffle 时我最喜欢的功能之一,即调试器。

让我们快速看一下我们的winOrLose函数。我要在这里添加一个新的require语句,require(1 != 1),对于正常的情况来说,是一个愚蠢的事情,但这将确保我们的函数调用失败,让我可以向你展示如何调试它:

function winOrLose(unit display, bool guess) extrenal payable returns (bool, unit) {
     /* Use true for a higher guess, false for a lower guess*/
     require(online == true, "The game is not online");
     require(msg.sender.balance > msg.value, "Insufficient funds");
     require(1 != 1);
     unit mysteryNumber_ = mysteryNumber_, display, guess);
     if (isWinner == true) {
          /* Player won */
          msg.sender.transfer(msg.value * 2);
          return (true, mysteryNumber_);
     } else if (isWinner == false) {
          /* Player lost */
          return (false, mysteryNumber_);
     }
}

我将切换到一个终端会话并启动一个truffle develop控制台。当你这样做时,Truffle 将启动,并且会带着它自己的以太坊网络,你可以在这里看到它每次启动时的情况。我们会得到一些账户私钥和助记词,如果我们想要将钱包连接到它,我们可以使用这些信息:

我也将打开第二个控制台窗口,并在其中运行truffle develop --log命令。在我们的开发控制台内,我们将compile我们的合约,然后我们将键入migrate --reset,将其迁移到此本地以太坊网络:

现在,我将执行这个命令,调用我们合约上的一个函数;但是当所有内容都在一行时,这看起来很混乱,所以让我们一块一块地拆开来,这样我们就能理解这里发生了什么。所以,这是同样的事情 - 它只是在控制台上写成一行,但在这里,我们将把它拆分成多行来演示每个部分是什么。我们有我们的游戏合约,这是我们的以太坊合约,我们正在调用deployed方法,就像我们在测试中做的那样。然后我们有一个 promise,所以当承诺实现时,我们调用 doc,或者我们有一个函数,接收合约作为实例的变量名,并在该函数内部返回instance.winOrLose函数,该函数执行我们智能合约中的 winOrLose 函数。当完成时,我们将有另一个点,然后或者我们调用另一个函数来将来自以太坊网络的响应写入我们的控制台:

Gaming.deployed()
.then(function(instance) {
   return instance.winOrLose(5, true);
})
.then(function(value) {
   console.log(value)
});

所以,现在我们可以执行它。每当我们执行它时,我们都会得到 VM 异常处理事务回滚,所以它失败了:

但我们想要看到的部分是,这就是我在这里使用开发控制台的原因,我们得到了我们的交易 ID。所以,现在我可以拿到那个交易 ID,输入debug,然后粘贴那个交易 ID,它将引导我浏览该交易执行的一切。您在这里看到的是,它将逐行地浏览代码;我们有一些命令可以在这里输入,我们将一次逐行浏览它,要跳过,我们还可以设置断点和监视表达式。它实际上是一个功能齐全的调试器:

它在这里显示给我,下划线表示将执行的代码行;所以当我们准备执行它时,我们可以按Enter,然后它会继续执行我们的winOrLose函数。现在它将评估该函数中的每个参数。我们已经进入了我们函数内的第一行代码,我们的 require 语句。它正在评估变量online,这是true,并将变量评估为true,现在它将评估语句作为一个整体,现在它将评估require语句,我们将做同样的事情来确保msg.sender.balance大于消息值。这里非常冗长对吧:

现在我们重定向到1!=1,所以它刚刚将我们踢出来,并显示出一个运行时错误。非常酷的一点是,现在我们确切地知道了合约中的哪一行代码导致执行失败。

现在你知道如何用 Solidity 和 JavaScript 编写你的测试了。你知道如何运行它们,也学会了在出现问题时如何使用 Truffle 交互式调试器。让我们开始今天的作业吧。

作业

在过去几天里,你一直在编写代码,并依靠测试来告诉你代码何时正确。今天,我们将改变这种情况。合约中有一个名为withdrawFunds的函数。它不带参数,将合约的余额转移到消息发送者。我希望你编写一个测试,获取我们测试中定义的所有者的合约余额,调用withdrawFunds函数,然后验证所有者的余额增加了 10 以太币。

作为额外的作业,你还可以编写一个附加断言,以确保提款后合约余额为零。现在,你会想在 JavaScript 中进行这些测试,因为不能使用 Solidity 测试访问以太坊账户。如果遇到困难,可以看看我们 JavaScript 测试文件中已经编写的一些现有测试。实际上,使用这些测试作为灵感并没有错:阅读他人编写的代码是增加自己对特定主题理解的好方法。

总结

我们已经到达了本章的末尾!我们看到了单元测试与集成测试之间的比较。然后我们看了写测试背后的原因。之后,我们测试了各种应用的策略,例如,Solidity 和 JavaScript。接下来,我们创建了一个单元测试,并了解了 Solidity 测试约定。我们学会了如何为同一个函数创建多个测试,并在 JavaScript 中创建了集成测试。最后,我们运行了测试套件。

在下一章中,我们将为我们的应用构建用户界面。

第五章:第五天 - 构建用户界面

在这一章中,我们将专注于构建用户界面UI)。这是我们的应用程序的一部分,我们的最终用户将会看到和使用它。如果你过去构建过网站,你会看到一些熟悉的东西在这里发生,你将学习与区块链应用程序交互的复杂性。如果你以前从未建过网站,那也没关系,因为你将了解我们将要做什么,以及我们为什么需要这样做。

在本章中,我们将涵盖以下主题:

  • 理解 JavaScript 和 React 在 DApp 中的作用

  • 从模板创建一个 React 应用程序

  • 将游戏状态展示给玩家

  • 从 UI 获取玩家输入

  • 为玩家提供反馈

  • 在网络应用程序中实现 JavaScript 的 promise

  • 使用 Web3.js 与以太坊网络通信

  • 在 UI 中实现 JavaScript 函数

理解 JavaScript 和 React 在 DApp 中的作用

以下屏幕截图显示了我们今天将要构建的 UI。我们将使用 React,一个流行的 JavaScript 框架来构建它:

我们将构建 UI 中看到的组件,然后编写代码,允许玩家将他们的赌注和猜测提交给 Ganache。然后,Ganache 将执行我们的智能合约函数,并确定玩家是赢了还是输了,根据结果,它要么保留玩家发送的资金,要么支付该轮比赛的赢利。在任何情况下,我们都将向用户提供反馈,让他们知道他们是赢了还是输了。

在我们的 Web 应用程序中,我们将使用 React,但我们还将使用 Redux。React是一个用于构建 UI 的 JavaScript 库,它很好地允许我们设计构成我们的应用程序的组件和应该影响它们的状态。Redux是一个可预测的状态容器,这意味着我们将从以太坊网络获取大量数据,例如,每轮比赛的结果。当其中一些信息发生变化时,我们需要更新 UI 以让玩家知道这一点。Redux 提供了一个模式来做到这一点,今天你将学习如何使用它。

当你考虑组件时,想象一下构成 UI 的离散元素。所以我们的应用程序是一个大组件,但它由其他组件组成,比如显示玩家数字的组件,显示下注控件的组件,以及显示玩家游戏历史的组件。玩家的游戏历史组件还由更小的组件组成。有容器瓷砖组件和每个游戏历史项目的列表重复组件。所有这些都有状态。历史组件状态存储玩家历史,下注窗口状态存储我们的玩家和他们的猜测的赌注金额,React 处理所有这些。

现在,有些事情会在我们的应用程序之外发生,我们需要更新状态并让应用知道已经发生了,并据此响应 React。后者很容易解决,因为我们选择了 React 作为我们的工具。React 在跟踪状态方面做得很好,当状态改变时,它会更新受其影响的屏幕上的内容。

为了更新状态,我们使用了 Redux。在我们的 React 组件中,我们将执行一些触发动作的操作,比如点击“LET'S PLAY!”按钮。在点击该按钮时,它会使用 web3.js 库调用我们的合约,并执行我们之前创建的 winnerLose() 函数。当我们的合约函数执行该函数时,它会返回一些数据,这些数据可能是成功响应或错误消息。我们将获取该响应并将其分派到一个 reducer 中,该 reducer 将使用新信息更新 Redux 存储。

更新的信息以 props 或属性的形式发送回我们的应用程序,在那里 React 可以评估需要在 UI 中更新的内容。整个过程可以总结如下图所示:

因此,我们清楚地将组成 DApp 的不同部分进行了分离。

我们有以太坊网络,我们的合约在**以太坊虚拟机(EVM)**上运行,然后我们的 React 应用在 Web 服务器上运行。我们应用的这部分可以是任何东西;我选择了一个 React 应用,但它同样可以是一个安卓或 iOS 应用,一个 Python 应用,甚至是一个老旧的 COBOL 主机应用。

React 应用程序与以太坊网络之间的连接是通过一个名为 web3.js 的实用程序库来完成的,以进行通信。该库提供了允许我们与以太坊节点通信的实用工具,如 Ganache,以及以太坊主网和测试网络上的节点。web3.js 有四个主要模块,分别是:

  • web3-eth:以太坊区块链和合约

  • web3-shh:点对点和广播的 Whisper 协议

  • web3-bzz:用于去中心化文件存储的 Swarm 协议

  • web3-utils:辅助函数

在本书中我们将使用的模块是用于与区块链和我们的合约通信的 web3-eth,以及一些实用函数的 web3-utils,例如转换以太币的不同面额。

Web3 允许您使用回调和 promises,并提供事件发射器来尝试提供您需要的所有选项。我们将广泛使用 promises,并在接下来的内容中详细介绍它们。在您可以编写实际向您的客户、公司或客户端交付值的第一行代码之前,必须放置大量的样板代码。幸运的是,有一些快捷方式可以减少这段时间,我将在下一节中向您展示它们。

从模板创建 React 应用

在这一部分,我们将看一些快捷方式来启动一个新的 React 应用。这是一件好事,因为启动一个新应用是一项繁重的工作,需要很多时间,本该用来编写代码来完成你的应用。

一种方法是使用 Facebook 创建的一个名为create-react-app的工具。它只是使用一些预先配置的选项快速创建一个 React 项目的板块或空白项目。你也可以只是复制另一个项目。如果你有一个类似的项目,里面的一切都按照你需要的方式设置好了,你可以克隆、复制或 fork 该应用程序,删除你不需要的部分,然后从那里开始。

还有第三种方法,那就是从零开始构建所有东西。如果你真的想要了解 React 的内部工作,这是一个很好的练习,但如果你在工作期限前工作,我不建议这样做。

优缺点

每种方法都有其优缺点。create-react-app工具使用 React JSX 和 ESX 创建一个新的项目。

使用这个工具启动一个新项目就像输入以下命令一样简单:

npx create-react-app my-app

其中my-app应该被你的应用程序的名称替换。

这创建了应用程序,并预先配置了babelwebpack,这样你就不必自己配置,更新通常很简单并有很好的文档说明,这样你就可以轻松地使你的应用程序保持最新功能和安全补丁。

不过,它的观点很明确,为了实现这样一个项目,必须如此。你添加的任何额外依赖项必须符合项目的格式,否则你会遇到挑战。你可以定制任何预先配置的工具,但这样做可能会使其处于你要负责维护的状态,因为它将不再接收官方包的更新。

复制或 fork 另一个项目有时可能是开始的好方法,特别是如果新项目与许多相同的依赖关系,它可能已经为你的使用案例配置好了,还可以访问支持资源,比如团队中的其他开发人员,如果你在使用相同的代码库。

这确实意味着你也会继承该项目的所有问题,比如过时的依赖项。你可能会不得不删除任何不被你的应用程序使用的不需要的代码,有时会导致一些问题。经常发现自己 fork 了一个项目吗?那么你最终会在多个项目中复制相同的代码,这可能会导致在更新依赖项或修补安全漏洞时需要进行大量额外的工作。

对我来说,当我学习新东西时,很容易看到一个似乎在解决同样问题的项目,并将其作为起点。我觉得这对于在一边尝试不同的事情来说是很好的。然而,随着时间的推移,这些项目几乎总是最难维护的。所以,如今,如果我正在构建一个将与实际用户发布的项目,几乎总是从 Facebook 工具开始。唯一的例外是当有一个具体的用例需要一个与其父项目共享代码库的高度定制组件时。

现在你对如何为 DApp 创建自己的 React 应用有了一些背景信息,让我们继续在我们的应用程序上工作。是时候开始构建用户界面了,这样我们就可以开始看到与我们的 Solidity 合同进行交互的视觉界面了。

向玩家显示游戏状态

到目前为止,我们一直在讨论 React 的工作原理。现在让我们把这些知识付诸实践,开始构建我们的用户界面,同时学习 React 如何使用状态来更新页面上的组件。

在我们的应用中,有一个名为 index.html 的文件;你可能知道这是当用户访问网站时,网页服务器默认提供的文档。在我们的索引页内部,有一个名为 index.js 的 JavaScript 文件被调用,它又添加了一个名为 app 的组件,这就是我们的 React 应用程序。下面的图表显示了应用的样子:

React 的一个主要目标是构建独立的组件。在 app 内部,它获取组成我们游戏的组件。这些组件包括头部和游戏组件。这些组件在文件系统上是单独的文件,让我们进入代码编辑器来感受一下它们的样子。让我们看看下面的截屏:

在我们的应用中,有一个 src 源文件夹、一个 index.html 文件和一个 index.js 文件。当你查看 index.js 文件时,你可以看到我们通过导入 configureStore 来创建 Redux store,然后我们从 containers/App 文件夹中导入我们的 App 组件,并在页面上创建该 app 的实例,如下代码片段所示:

import React          from 'react';      
import ReactDOM       from 'react-dom';
import { Provider }   from 'react-redux';
import configureStore from 'core/store/configureStore';
import App            from 'containers/App';

const store = configureStore();

ReactDOM.render(
    <Provider store={store}>
 <App/>
 </Provider>
    document.getElementbyId('root')
);

App 组件存储在我们的 containers 文件夹中;当你转到那里时,会看到一个名为 App 的文件夹和一个名为 index.js 的文件,其中存放着它的代码。它导入了头部和游戏,游戏本身是 containers 下的另一个文件夹,它有自己的 index.js 文件,在这个文件中,我们将定义游戏的所有组件。

这是我们如何为您的 UI 定义这些组件的方法。将显示的整个屏幕部分是我们的游戏容器,它是app.js文件中引用的组件。它由三个较小的组件组成——显示玩家数字的组件,显示投丨注控件的组件,以及包含玩家游戏历史记录的组件,如以下示意图所示:

为了创建所有这些容器和控件,我使用了 Material-UI 库。这个库使创建高质量、专业外观的 UI 组件变得非常容易。

我们的游戏组件首先从几个库开始导入:

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'

如这里所示,我们需要React本身以及组件库来创建组件类,还需要PropTypesconnectbindActionCreators用于 Redux,我们将在接下来的章节中详细讨论它们的角色。

然后我们创建一个名为Game的类,在其中我们有一个constructor函数。这与我们在 Solidity 合约中创建的构造函数的工作方式类似,它在类初始化时运行一次,以设置初始状态。以下代码片段显示了该类包含的内容:

export class Game extends Component {
    constructor(props) {
        super(props)
        this.state = {
            playerNumber: '';
            highLow: '';
            wager: '';
            history: []
        }
    }
    render() {
        return (
        )
    }
}

如前面的代码块所示,我们将为一些我们需要的变量设置初始状态,例如本轮显示给玩家的数字,他们对更高或更低的猜测,他们在本轮下注的金额,以及我们将存储之前几轮结果的数组。React 组件有一个必需的函数,render()。该函数在组件渲染时被调用。

现在我们准备开始布置这个组件的 UI 元素了。我们将从index.html页面上构建我们的第一个组件开始,即显示给玩家本轮数字的显示窗口。我们将从 Material-UI 库中定义一个卡片,然后定义一个带有标题和副标题的卡片头部,为玩家提供有关他们正在查看的内容的一些信息,最后我们有一个标题元素来显示数字本身。可以用以下代码片段来概括:

<Card style={style}>
    <CardHeader
        title="Player's Number"
        subtitle="Will the mystery number be higher or lower than this number?"
    />
    <h1 style={displayNumber}>{this.state.playerNumber}</h1>
</Card>

这应该生成一个看起来与以下截图类似的窗口:

卡片中显示的数字是我们在构造函数中定义的状态变量。由于我们在组件中添加了来自 Material-UI 库的卡片和卡片头部,我们必须将其一起导入,以便 React 知道从哪里获取这些组件。我们通过将卡片和卡片头部作为导入内容添加到我们在文件顶部声明的其他导入中来实现这一点。

让我们回到在h1中使用的命令。它是如何从一串文本变成你在屏幕上看到的数字的?在 React 中,当你用花括号括起一个字符串时,它就具有了特殊的含义,但在那一点上,它实际上只是 JavaScript,所以我们可以做任何我们可以在 JavaScript 中做的事情。这意味着this被创建为一个变量,该变量的结果就是显示在屏幕上的内容。

有一个特殊的 React 生命周期函数叫做componentDidMount(),在我们的组件挂载后由 React 调用。挂载意味着它已经在 DOM 中呈现,并且可以通过编程方式调用。将我们的代码放在这里,确保我们在实际存在于 DOM 中之前不会尝试访问组件。我们将调用this.setState()并分配playerNumber状态变量。该函数的结果生成当前玩家号码。generatePlayerNumber()函数如下所示,只返回 0 到 9 之间的一个随机数。最终结果是,我们页面上的组件呈现了玩家看到的随机数字。以下是代码片段:

componentDidMount() {
    this.setState({
        playerNumber: this.generatePlayerNumber()
    })

    generatePlayerNumber() {
        return Math.floor(Math.random() * 10)
    }
}

接下来,我们有我们的下注窗口,它是另一个带有卡头的卡片,就像我们的玩家显示组件一样。它有一个单选按钮组,其中包含两个单选按钮,供玩家选择更高还是更低:

<RadioButtonGroup
    name="highLow"
    defaultSelected={this.state.highLow}
    onChange={this.handleChange('highLow')}
    >
    <RadioButton
        value="higher"
        label="Higher"
        style={elementStyle}

    />
    <RadioButton
        value="lower"
        label="Lower"
        style={elementStyle}
    />
</RadioButtonGroup>

注意所选值如何读取highLow变量的状态值,并且在更改时调用handleChange()函数。

我们还有一个文本字段,供玩家指示他们想要下注多少,以及一个按钮,当他们准备好开始游戏时执行。我们已经导入了卡片和卡头,所以现在我们将以相同的方式导入单选按钮组、单选按钮、文本字段和凸起按钮。

无论你在哪一步感到困惑,你都可以随时参考你在书中得到的源代码。对于单选按钮组和文本字段,我们调用handleChange()函数,其代码类似于以下内容:

handleChange = name => event => {
    this.setState({
        [name]: event.target.value
    })
}

这个函数接收要更新的状态变量的名称作为参数,然后用调用该函数的控件的值更新状态。所有这些都归结为类似于这样的东西:

我们的最终组件是历史窗口,就像其他组件一样,它是一个带有卡头的卡片,这真正突显了使用库的好处之一。我们在这个组件中多次重复使用了这个库,因为它很容易定义,而且我们不需要编写任何代码来完成它。接下来,我们有一个列表,我们从状态中获取历史记录,这是一个数组,数组中的每个项目都是以前游戏中的结果。因此,我们对其进行映射,并为数组中的每个元素创建一个列表项。这在下面的代码片段中进行了总结:

<CardHeader
    title="History"
    />
    <List>
        {this.state.history.map((round) =>
        <ListItem key={round.transactionHash}
        primaryText={`Result:\t${round.result}`}
        secondaryText={`You ${round.result} ${round.wager} by guessing ${round.playerNumber} would be ${round.guess} than ${round.mysteryNumber}!`}
        />
        )}
    </List>
</Card>

这导致以下输出:

现在,让我们再次跳转到我们的代码编辑器,并看看所有这些部分如何组合成一个单一的 React 类。所以,在我们的src/containers/Game文件夹中,我们有我们的index.js文件,现在让我们来看一下。

在顶部,我们从 React、Redux 和 Material-UI 库导入构建此页面所需的所有内容。我们在这里有一点 CSS,使页面的格式看起来漂亮,然后我们有一个扩展了 React 组件的游戏类。在其中,您将找到设置初始状态变量的constructor,然后是 React 生命周期组件。然后,有我们的渲染方法,其中返回在页面上呈现的所有元素。

我们在玩家编号的卡片标题中有一个卡片;我们有第二个卡片代表投丨注窗口,然后我们有第三个卡片代表玩家的游戏历史记录。在此之后,我们有处理更改事件、启动游戏并生成随机数的用户定义函数。

所以,一切开始变得像一个真正的应用程序了。让我们再推进一点,跳到下一节,在那里我们将剖析“开始游戏!”按钮,看看我们如何从 UI 获取输入,并将其转换为用户的游戏。

从 UI 获取玩家输入

在这里,我们将继续执行代码,当我们的玩家点击“开始游戏!”按钮时。下面的代码片段展示了它的工作原理:

playGame = () => {
    const { actions } = this.props
    actions.game.playRound(
        this.state.wager,
        this.state.playerNumber,
        this.state.highLow
    )
    this.setState({
        playerNumber: this.generatePlayerNumber()
    })
}

在这里,我们正在定义一个从this.props获取的动作,然后我们调用该动作中存在的一个函数,最后,我们使用一个新的随机数更新playerNumber状态变量。

接下来,我们有从游戏组件中调用的函数,即playRound()。这个函数接受赌注、玩家编号和猜测作为参数,然后返回一个dispatch参数。dispatch()函数返回一个 JSON 对象,其中 type 是我们常量之一,并且参数是我们传递给函数的参数。我们的类型在types.js中定义。在其中,我们定义了我们的初始状态,它表示应用程序启动时设置的状态变量,这样当应用程序启动时就不会出现变量未定义错误。

然后,我们导出我们的游戏 reducer 函数,其中包含基于我们在动作中提供的动作类型的 switch 语句。当在我们的 reducer 中找到匹配类型时,它将返回定义给 Redux 存储的对象。

这个谜题的最后一部分是rootReducer。这个函数将我们应用程序中的所有 reducer 组合起来。下面的代码片段展示了它的内容:

import { combineReducers } from 'redux'
import { providerReducer } from 'core/reducers/reducer-provider'
import { gameReducer } from 'core/reducers/reducer-game'

const rootReducer = combineReducers({
  provider: providerReducer,
  game: gameReducer
})

export default rootReducer

因此,让我们再次将这个问题概括一下,这次引用我们刚刚学到的内容。从我们的游戏组件中,我们调用playRound()函数,这是由props提供的。playRound()函数接受来自玩家的参数并将它们分派到core/reducers/reduce-game.js中的 reducer。reducer 使用core/types.js中提供的常量将接收到的分派与要执行的工作进行匹配,并将结果发送到 Redux 存储。然后,Redux 存储将新数据作为props发送到我们的应用程序,当props发生变化时,React 注意到并使用新数据更新我们的历史组件。可以用下面的图表来概括这个过程:

让 React 如此强大的是,当发生这种情况时,屏幕上唯一发生变化的是历史组件更新,而其他任何东西都没有发生变化。在下一节中,我们将实时地逐步进行所有这些,因此如果现在还不清楚,我认为在下一节中,当您自己测试它时,您会理解的。

向玩家提供反馈

我们现在已经布置好了代码,将我们的 React 组件连接到 Redux,以管理我们应用程序的状态。这可能是一个难以理解的概念,因此在本节中,我们将使用 Visual Code 调试器来玩我们的游戏,并实时地逐步执行代码。这样做将使您能够在执行时准确地看到应用程序的行为。

知道如何调试可能是您可以学习的最有用的技术之一,因为它可以在您最需要时为您提供新的信息。使用 Visual Code 设置它非常容易。您只需在 Visual Code 的扩展面板中安装微软的Debugger for Chrome工具:

然后,您将在调试菜单中创建一个启动配置,其中包含以下代码:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "chrome",
            "request": "launch",
            "name": "Launch Chrome against localhost",
            "url": "http://localhost:3000",
            "webRoot": "${workspaceFolder}"
        }
    ]
}

现在,我们将导航到index.js文件中playGame()函数内的第一行。由于playGame()是一个函数,当玩家点击游戏中的“Let's Play!”按钮时,它将被执行。我们现在在这里设置一个调试点。

现在我们将切换到 Visual Code 中的调试菜单,并点击那里的绿色播放按钮。当浏览器启动时,我们应该会看到类似这样的东西:

我们有一个玩家编号窗口,编号为 3,和我们的投丨注窗口。

我们可以输入赌注,所以让我们去做吧,然后点击“Let's Play!”。所以,立即,这会将我们切换回 Visual Code 并触发我们设置的调试点。我们可以使用顶部的控件播放到下一个调试点或逐个跳过每个函数。

当我们逐步执行每个函数时,您会看到在调试窗口中,这些函数的参数实时填充其值。您还可以在代码窗口中将鼠标悬停在它们上面,会弹出一个小窗口,告诉您相关信息。因此,您可以实时检查变量。

当我们从actions对象的playRound()函数到达时,我们不是要跳过它,而是要跟进它,以便通过该函数跟踪代码执行,这将带我们到我们的actions-game.js文件,我们在其中有我们的playRound()函数,返回dispatch参数。然后我们进入其中,现在它已经准备好返回我们将要分派到 reducer 的 JSON 对象。

然后我们进入我们游戏 reducer 的函数,有 switch 语句,如果我们悬停在操作类型上,我们可以看到该变量的结果等于Round_Results。所以我们将这些键值对分配给 Redux 存储。我们的 Redux 存储然后将这些更新后的属性推送回我们的游戏index.js文件作为props,然后我们将返回到我们的浏览器,看到我们更新的历史记录填充到历史窗格中,如下截图所示:

现在循环已经全面完成,我们的应用刚刚从 React 收到了新的props。那么我们如何利用它们?我们将通过使用另一个 React 生命周期方法componentDidUpdate()来实现。它是在发送新 props 后触发的函数。它有一个叫做 previous props 或prevProps的参数,这样您就可以引用刚刚收到的 props 之前的 props,并确定是否需要做出响应。这是非常重要的,因为您的应用程序将一直收到新的 props,但并非所有 props 都需要做出响应,所以我们将使用条件来识别我们关心的那些。

我们将比较前一个游戏对象的时间戳和当前游戏对象的时间戳;如果它们不同,我们就知道我们收到了数组的新历史项。在这种情况下,我们将复制当前的历史和状态,然后将props游戏对象的数据推送到其中。完成此操作后,我们将用刚刚创建的新历史数组来替换历史对象,现在我们将回到显示游戏历史记录的卡片。

请记住,我们将所有项映射到状态历史数组。React 将注意到我们刚刚更新了该状态变量,并重新渲染此组件,使其更新并显示我们上一轮的结果。在这里,我们只是立即发送了游戏发送的参数,但这并不是我们真正想要的。我们真正想要的是调用我们的智能合约,将这些游戏参数传递给它,并让它确定我们的玩家是赢了还是输了。为了做到这一点,我们需要我们的动作调用智能合约,然后等待这些结果。所以,在下一节中,我们将探讨 promises,它们的作用,工作原理以及我们将如何使用它们来与我们的以太坊网络进行通信。

在网络应用程序中实现 JavaScript Promise

在我们的最后一节中,我们看到了如何从我们游戏的玩家那里获取输入,并将其通过 Redux 发送以更新应用程序状态,然后刷新用户界面。但正如我们在第一天学到的,当我们提交调用以太坊网络时,我们并不会立即得到响应,会有一段延迟。不幸的是,我们在没有得到响应之前无法继续进行,那么我们该怎么办呢?

在我们回答这个问题之前,让我向你展示一下潜在的问题是什么。它源自 JavaScript 的工作方式。让我们看看这段代码:

console.log('1')
setTimeout(() => {console.log('2')}, 3000)
console.log('3')
setTimeout(() => {console.log('4')}, 1000)

在上面的代码中,我们有四行,每行都打印出数字 1 到 4。那么当我们运行这个代码时会发生什么呢?如果你预测它会打印出 1,3,4,2,那么你是对的,因为 JavaScript 执行第一行时,它打印出数字 1,然后执行第二行。这一行是问题的起点,因为它执行了 setTimeout() 函数,但并不知道我们希望它等待定时器完成。所以它调用了 setTimeout() 函数,然后移到下一行,打印出 3,然后移动到最后一行,有另一个定时器。一秒钟后,它打印出数字四,两秒钟后,当指定的 3000 毫秒延迟过期时,它最终打印出数字 2。

现在,想象一下,如果我们不是将一个数字写到控制台日志中,而是等待来自我们的 Solidity 合约返回的数据,以便在我们的应用程序中使用它,那么会不会导致问题,因为我们会在数据实际存在之前尝试使用它呢?那么,我们如何使用 JavaScript 的 Promise 来解决这个问题呢?

Promise 的工作原理

JavaScript 的 Promise 是一个对象,本质上是在未来产生一个值或输出,就像现实生活中的承诺一样。一个 Promise 可以有三种状态:

  • 已完成

  • 已拒绝

  • 待定

我们将通过上一个代码块中的场景作为示例进一步理解这一点。

我们将创建一个返回 promise 的新函数,以处理我们延迟的事件。promise 接受一个参数,这是一个接受两个参数resolvereject的函数。在我们的 promise 中,我们有我们的setTimeout()函数。下面的代码片段展示了我们如何使用 promise 来解决我们之前的定时器问题:

function waitForMe (input, delay) {
    return new Promise ((resolve, reject) => {
        setTimeout(() => {
            resolve(input)
        }, delay)
    })
}

此函数确保编译器等待计时器完成,打印出数字 2,然后继续打印数字 3。

大多数情况下,它会是一些不会立即返回的东西,比如在 Solidity 合同上执行一个函数。当我们的setTimeout函数完成后,我们将调用我们 promise 的一个参数resolve()函数;这表示 promise 已成功完成。同样,如果出现问题,我们可以调用reject函数。

现在,我们可以重写我们的原始代码。我们的第一行保持不变,但对于我们的第二行,我们将使用waitForMe()函数,并将两个参数传递给它,即输入 (2) 和延迟 (3000)。这确保当函数被调用时,编译器会等待三秒钟,然后打印出数字 2,最后继续下一步。就像之前一样,我们会直接从控制台打印出 3,然后我们将再次使用相同的waitForMe()函数来获取数字 4。下面的代码片段展示了修改后的代码:

console.log('1')
waitForMe('2', 3000).then(result => {console.log(result )
console.log('3')
waitForMe('4', 1000).then(result => {console.log(result )})

让我们快速回顾一下 promise。promise 提供了某事将会被做的保证,但并不确定何时会被完成。不像真实的承诺,JavaScript promise 通过解决 promise 来实现它,或者通过拒绝来结束它。这样可以让你决定在任何情况下该做什么。现在,我们已经准备好解决与以太坊网络上的合同通信的问题了。

使用 Web3.js 与以太坊网络通信

在之前,我们看到了如何从我们游戏的玩家那里获取输入,并通过 Redux 来更新应用程序状态,然后刷新用户界面。在前一节中,我们了解了 JavaScript promise。现在,我们将学习如何让这两者共同工作,来调用我们在合同中创建的函数。

我们将重新设计我们应用程序的actions-game.js文件中的playRound()函数。

我们将使用dispatch函数,并将其移出到自己的函数中。然后,我们将从以太坊网络中获取我们合同的一个实例,并创建一个调用我们合同中函数并等待响应的 promise。当它获得响应后,它将调用我们的新dispatch函数。

要开始,我们需要将一些新的依赖项引入到我们的应用程序中,比如从 Truffle 合同中获取的contract库,我们从contract/Gaming.json中的Gaming文件,以及web3库,如下面的代码片段中所示:

import contract from 'truffle-contract'
import Gaming from '../../../build/contracts/Gaming.json'
import web3 from 'web3'

我们现在将修改我们的playRound()函数。

我们将dispatch函数移入两个新函数,dispatchRoundComplete()dispatchRoundFailed()。我们用一个常数web3Provider替换它,该常数来自 Redux 存储中的状态。然后,通过调用我们的contract库并传递给它我们的合约的 JSON 表示来声明我们的游戏合约。我们在这里做了一些设置;我们将合约的提供者设置为web3Provider中指定的提供者。这是 MetaMask 被注入的地方,我们的应用程序会自动检测 MetaMask 是否被注入并设置提供者。下面的截图展示了我们的playRound()函数现在的样子:

明天您将学习更多关于 MetaMask 的知识。MetaMask 允许您选择不同的账户,就像我们在编写单元测试时所做的一样。使用web3Provider.eth.defaultAccount对象可以确保我们从当前选择的账户发送交易。我们创建一个新的 promise 并定义两个参数resolvereject。当我们在以太坊合约中完成我们的函数调用时,我们将检查其状态。如果成功,我们将调用 resolved 函数,如果失败,我们将调用 reject 函数。这样,当这个 promise 返回时,我们就知道它是否成功。在 promise 内部,我们调用winOrLose()函数,这是一个新函数,但您可能会认识到这个名称。

让我们来看看它,然后我们将回来看看前面函数中的其余部分发生了什么:

如果你觉得函数名称winOrLose听起来很熟悉,那么你是对的:这是我们在 Solidity 合约中创建的函数名称。在我的 React 应用中,我给调用智能合约的函数取和智能合约中的函数相同的名称。因此,现在看着我们的 UI 代码,我们知道这将调用winOrLose()函数,而这个函数在我们的合约中已经存在了。这只是帮助我们追踪事物。

我们传递给这个函数的一个参数是GamingContract,这是我们刚刚创建的在线合约的实例。它有一个deployed方法,返回一个 promise。它的作用是获取我们合约的部署版本,这意味着它获取与以太坊网络通信所需的信息。它找出它所在的网络、它的部署地址是什么,以及如何与它通信。由于它是一个 promise,我们可以通过使用then等待其完成;这意味着我们应该执行这段代码,而每当它完成时,继续执行。

gameContract.winOrLose()函数是与我们的 Solidity 合同进行通信的实际函数。请记住,在 Solidity 中,我们始终以Wei为单位处理货币。以太坊的大多数用户都熟悉ether单位,因此在我们的界面中,我们允许他们用以太币下注,然后我们使用web3工具将其从以太币转换为 Wei,然后将其附加到我们的交易中。这整个部署的函数也是一个承诺。

当这个完成时,我们会进入then函数,将交易的结果作为名为results的变量传递。当它解析时,我们将调用我们在调用函数时传递的resolve函数。在那个函数中,我们将返回result.logs[0],看起来非常具体。让我们仔细看看发生了什么:

这是我们 Solidity 合同的核心部分。我在上图中突出了两行具体的内容,这就是我们现在要看的内容。当我们的玩家赢了或输了,我们就会发出RoundComplete()事件,并提供交易的详细信息,赢了或输了多少钱,向玩家显示的数字,我们合同生成的神秘数字,玩家猜测的高或低,以及他们赢了还是输了这一轮。请记住当交易被写入区块链时,事件会被发出。下图显示了我们从交易中得到的实际结果:

这里涉及了很多内容,让我们来看一下日志数组。数组中的第 0 项具有一个args键,如果你仔细看,就会发现这是我们正在发出的事件,包含了我们刷新玩家界面所需的所有细节。所以,当我们解析这个函数时,我们会剥离一切,然后将这个键返回给我们的resolve函数。

winOrLose()函数中的最后一部分是一个catch语句,如果承诺中的任何内容失败,它就会被执行。当发生这种情况时,我们捕捉错误并使用reject函数将其发送回原始承诺,从而回到我们的playRound()函数。根据我们调用resolve还是reject函数,我们将执行then函数或catch函数。

当它成功解析时,我们就会从包含我们事件的日志中返回该对象,并将其传递给另一个新函数dispatchRoundComplete(),并调用我们的参数事件。这个函数也许会让你感到熟悉:它就是我们在上一节创建的派发函数,但我们稍微修改了一下,如下图所示:

我们仍然有相同的类型,但我们将时间戳键替换为 transactionHash。这个字段存在的原因是因为我们在 UI 中映射数组以填充游戏历史记录表。React 要求数组中的每个元素都有一个唯一的键,以便在更新时知道要更新哪一个。

以前,我们没有一个唯一的键,所以我只是把一个时间戳作为唯一值放在那里,但现在我们有了我们交易的交易哈希,这绝对保证是唯一的,所以我们不再需要那个时间戳。我们有了我们的赌注和玩家号码,并且我们添加了玩家下注的神秘数字。然后我们有他们的猜测以及该轮的结果。

我们这里还有一个布尔值 success 键,我们可以用它来切换此次交易是否成功。当 promise 成功完成时,我们将 success 设置为 true,如果 promise 被拒绝,那么调度往返失败函数将会将 success 设置为 false。我们的 dispatch 的行为和以前一样,并通知接收 dispatch 动作并将其发送到 Redux 存储的 reducer,而 Redux 存储现在将通过一个新的 props 文件通知我们的 UI,我们将在下一节中使用它来更新用户界面。

在 UI 中实现 JavaScript 函数

到目前为止,您已经了解了如何从 UI 上的用户控件获取输入,将它们通过 Redux 生命周期进行处理,并将最终产品显示回 UI 中作为新的 props。在本节中,我们将深入研究与用户控件相关的 UI 中的函数。

当我们的 reducer 接收到 dispatch 函数的动作时,它会创建并发送此对象到 Redux 存储,并且还会发送所有其他可能存在的 reducer,因此我们有一个 rootReducer 常量,它组合了所有这些 reducer。以下截图显示了这是什么样子:

在这里,您可以看到我们在 rootReducer 中定义了一个名为 game 的新键,并且它将用来填充来自 gameReducer 的数据。这就是发送到 Redux 存储的内容,我们知道 Redux 存储将把这个内容作为 props 发送到我们的组件中。因此,在我们的游戏 UI 的 JavaScript 文件中,我们有这个函数 mapStateToProps(),正如你从名称中猜到的那样,这是将 Redux 状态映射到组件中的 props 的函数。

因此,我们发送到 Redux 存储的所有值都可以在我们的组件中按如下方式访问和读取:

this.props.game.wager
this.props.game.playerNumber
this.props.game.mysteryNumber

所有的 props 都是只读的,您不能更改它们。我们将在一分钟内看看如何更改它们,但是当它们在后端更改时呢?如果这些键中的一个值在 Redux 存储中更改了,我们的 UI 如何更新?

嗯,这取决于你是使用 React 15 还是 React 16,有两种方法可以使用,行为都相似,但是从名称可以看出,它们在不同的时间触发。在 React 16 中,我们使用componentDidUpdate(),它接收一个名为prevProps的参数,允许我们将 props 的值与它们的先前值进行比较,然后相应地采取行动。在 React 15 中,我们将使用componentWillReceiveProps(),它在组件接收新属性之前触发。参数名称也反映了这一点。它是一个名为nextProps的参数,其中包含 props 的新传入值。您可以在 React 16 中使用componentWillReceiveProps(),因此,如果您将现有的 React 15 应用程序升级到 React 16,这仍将起作用。

在下一个版本的 React 中,componentWillReceiveProps()将被标记为已弃用,然后在其后的版本中将被删除。当我们向我们的组件添加这些函数时,我们需要评估为什么调用它。它会被很多东西调用,而且是你不关心的东西,所以你需要评估条件来看你是否关心它。让我们看看以下例子:

我们将以一个if语句开始,检查nextProps参数中的game.transactionHash是否与this.props中的不同。如果是,那么告诉我们我们有一个新的交易哈希,而由于交易哈希特定于一个交易,我们知道玩家已经完成了一轮游戏。接下来,我们设置我们的success键;记住,这是我们设置为true的键,如果包裹我们的合同交易的 promise 成功完成,我们设置为false,如果 promise 被拒绝。这给了我们一个机会来传递错误消息,如果我们需要的话。如果我们的 promise 成功解决,那么我们将有一些新的交易细节添加到我们的游戏历史窗口中。我将该游戏历史存储为组件状态中的一个数组。

所以,让我们为我们的历史创建一个新变量,然后将我们最新交易的所有细节推送到该数组中。我们不能直接更新组件状态,所以我们调用this.setState并将history值设置为我们刚刚创建的新历史数组。最后,我们有一个snackbar控件,这是一个小型的弹出式控件,其值也存储在组件状态中,因此我们可以更新它们并为它们分配值。

当这个渲染时,它会转化为一个句子,类似于失去十个以太币,或者正确的值是什么,然后将该轮结果添加到历史窗口中,如下面的截图所示:

除了更新状态变量之外,我们不需要做任何操作就可以使我们的 UI 组件处理页面上的更新,因为我们将控件绑定到了状态变量上。当它们发生变化时,React 知道重新渲染它们。因为 React 只想渲染发生变化的变量,所以在映射我们的历史数组时有这个键是很重要的。这个键允许 React 唯一地识别数组中的每个项目,并且只渲染那些已经改变的项目。现在,是我们今天的最后一个部分了,作业。

作业

好了!是时候将今天学到的知识付诸实践了。这个作业将强化我们使用 React 和 Redux 进行状态管理以及使用 promises 处理潜在网络请求的概念。你将在我们的游戏 UI 中添加一个记分牌,显示玩家当前的记录,如下面的截图所示:

你可以在页面上创建一个新组件来显示它,也可以将其内联添加到类似于前面组件的现有组件中。现在,回想一下第二天,我们为我们的玩家创建了一个包含此信息的结构体,并创建了一个将玩家地址映射到结构体的映射。请记住,公共变量和 Solidity 自带一个免费的 getter 函数,如果该变量是一个映射,则映射键是一个必需的参数。

在我们的情况下,这是我们玩家的地址。解决这个问题有多种方法,只要你的解决方案满足每次变化时更新玩家分数的条件,那就是一个很好的解决方案。如果是我,我会在我们的代码中找到一些已经在做某些事情的地方,因为分数已经改变了。你可能会在下面的代码片段中找到一些灵感:

componentWillReceiveProps (nextProps) {
    if (nextProps.game.transactionHash !==
    this.props.game.transactionHash) {
        if (nextProps.game.success == false){
            console.log(nextProps.game.error)
        }else {
            const newHistory = this.state.history
            newHistory.push({
            transactionHash: nextProps.game.transactionHash;
            wager: nextProps.game.wager;
            playerNumber: nextProps.game.mysteryNumber;
            guess: nextProps.game.guess;
            result: nextProps.game.result
        })  
        this.setState({
            history: newHistory,
            snackbar: true,
            message: '${nextProps.game.result}
                    ${nextProps.game.wager} ether'    
        })

    }
}

你可以使用上面的代码块,并按照应用程序中已经存在的代码的模式和示例来使用。你从 UI 调用一个动作,该动作将返回一个 promise,调用一个包含我们统计数据的 Solidity 函数;该 promise 将解析,然后我们可以调度我们的合同函数结果,将它们发送到一个 reducer,该 reducer 将更新 Redux 存储,这将更新呈现给 UI 的 props,UI 将更新以反映新的 props。

在明天之前你将无法完成这个作业,因为我们缺少一个测试它的部分,但是今天要写好代码,明天我们会检查剩下的部分,因为你需要一个钱包来测试这个。

总结

在本章中,我们学习了为我们的应用程序实现 UI 的所有内容。我们学习了如何使用 React 来创建我们的 UI,以及 JavaScript 如何在 DApp 中发挥重要作用。我们学习了如何从模板创建 React 应用程序并应用它们,以及如何修改应用程序中的各种状态,使一切都按照预期运行。我们学会了如何接受用户输入并向他们提供反馈,以及如何使用 Web3.js 将这些操作传达到以太坊网络。最后,我们学会了如何将所有功能部署到 React UI 中。

在下一章中,我们将学习不同类型的钱包,并且学习如何配置 MetaMask 来与我们的应用程序一起使用。

第六章:第六天 - 使用钱包

欢迎来到第六天。今天,我们将实现我们区块链应用的最后并且最关键的部分,钱包。钱包不仅为我们游戏的玩家提供存储资金的地方,还提供了发送和接收这些资金的方式,以及使用密码签名的交易来确保我们可以验证这些资金和交易来自于那个钱包。

本章将作为开始此项目的基石,涵盖以下主题:

  • 理解钱包和安全性

  • 钱包简介

  • MetaMask

  • 理解燃气价格和燃气限制

  • 在以太坊网络上查看区块链交易

  • 理解在线和离线钱包

  • 注入 Web3 提供程序

理解钱包和安全性

让我们来看一下今天典型的在线交易。消费者会访问一个网站,将一些商品添加到购物车中,然后启动结账过程。他们需要在该网站上输入他们的信用卡号,但这个信用卡号会怎样处理呢:它安全地发送到服务器了吗?商家会存储那个信用卡号吗?如果他们存储了,他们是否在遵循良好的安全实践?现在有谁可以访问这个信用卡号,因为任何持有这个卡号的人都可以使用它?我们有机制来处理所有这些问题,并且在发生问题时,大多数商家和发卡行都会与您合作解决问题,但这并不是出于他们的善意;您每天都在支付这项服务的费用和交易成本:

让我们使用区块链技术再次看一下相同的交易过程。购物过程看起来一样。您将商品添加到购物车,然后开始结账过程,但在结账时,您不需要输入信用卡号,而是使用钱包对包含购买指令的交易进行加密签名。那么,这是安全进行的吗?希望是,但如果不是,那也没关系,因为您发送的唯一内容是包含购买详细信息的已签名交易,如果有人在途中截取并更改该交易,那个签名就不再有效。

那么,关于存储详细信息呢?商家可以存储它们,而且它也会存储在公共区块链上,这没问题,因为它只是交易的详细信息;里面的内容无法用来创建额外的交易。商家的安全实践呢?你与之交互的合约位于公共区块链上,可以查看和审计其安全性,最后你的私钥永远不会离开你的钱包,因此唯一让您的账户用于创建恶意交易的方式就是失去了控制私钥。因此,没有第三方参与收取您账户维护和监控费用:

钱包介绍

钱包是一个存储公钥和私钥的软件程序,并允许您与区块链交互以签署可能包括发送和接收货币或执行智能合约的交易。它们实际上并不存储任何货币;它们只能引用与该地址相关的区块链上的交易。

让我们谈一分钟关于公钥和私钥,因为这是一个重要的概念要理解,因为如果你泄露了你的私钥,你将泄露与之关联的一切。每个钱包都有一个私钥和公钥:将公钥视为你房子的街道地址,将私钥视为可以解锁邮箱的钥匙。任何人都可以使用你的公钥向你的邮箱发送东西,但只有持有私钥的人才能打开邮箱:

让我们再举几个类似的例子,然后总结一个关键点。你的电子邮件地址是公开的,你电子邮件账户的密码是私密的:

你的银行信息相对是公开的,但你必须提供某种形式的身份证明才能使用它:

你的车是公开可见的,但你必须有钥匙才能打开它;也许这辆特定的车是个不好的例子,你知道那甚至可能不是它的正确钥匙,但我的观点仍然是有效的:

在所有这些情况下,如果你丢失了你的私钥,你可以通过打电话给锁匠、与技术支持交谈或去银行换新的借记卡来重新获得对你的资产的访问。但在区块链技术中,如果你丢失了私钥,就完了;没有办法恢复丢失的私钥,这是设计上的。任何一种在丢失后仍然可以恢复访问的情况都是因为中间人仍然具有访问权限,无论是银行、锁匠还是电子邮件提供者,你都要为这种便利支付费用。在区块链上,没有中间人,你拥有自己账户的责任;这种所有权的好处是除非你允许,否则无法入侵你的账户,而且你不需要支付第三方交易费用来方便地使用你的钱。

钱包类型

有不同类型的钱包可用:

  • 桌面钱包:这个钱包是下载并安装在你的电脑上的,只能从下载它的电脑上访问。它具有非常高的安全性,但如果你被黑客入侵或者你有病毒,它就容易受到攻击。MetaMask 是桌面钱包的一个例子,我们今天将更多地与它一起工作。

  • 在线钱包:该钱包在第三方服务器上运行,非常方便使用。私钥由钱包提供者在线存储,并由第三方控制。Coinbase 是在线钱包提供者的一个非常流行的例子。

  • 移动钱包:该钱包是安装在手机上的应用程序,非常适用于例如在零售商店进行支付。它们通常比桌面钱包具有更少的功能,因为手机上的空间有限。Mycelium 是移动钱包的一个非常流行的例子。

  • 硬件钱包:该钱包将私钥存储在硬件上,具有非常高的安全级别,事实上,即使在感染的 PC 上也可以使用硬件钱包。现在,即使使用硬件钱包,仍然需要备份,以防硬件损坏。Ledger Nano 是硬件钱包的一个例子。

  • 纸钱包:该钱包非常易于使用,并具有很高的安全级别,因为没有技术可能出现故障。实际上,它只是一张纸;等一会儿我会向你展示它是如何工作的。

那么,哪种钱包对您来说最好?答案取决于您想要实现什么目标。在本书中,我们将使用 MetaMask,因为它提供了安全性和易用性的良好平衡。就我个人而言,我使用多个钱包,每个钱包都有不同的角色。我在手机上有一个移动钱包,上面只有一点货币,可以用于随时进行交易;我还有一个硬件钱包,我将大部分加密货币存储在这里;我将其锁在防火安全箱中,并备份在存放在另一座独立建筑物中的纸钱包中,这样如果有什么问题发生,我仍然可以访问这些资金。现在,您已经知道了什么是钱包及如何使用它们,让我们跳到下一节,并为本书的最后部分配置我们的钱包。

MetaMask

我们正在使用 MetaMask 作为与我们的 DApp 交互的钱包。我们将学习如何安装和配置它;首先,让我们回顾一些 MetaMask 的关键点。

MetaMask 将 Google Chrome、Firefox、Opera 和 Brave 变成了以太坊浏览器。它允许网站从区块链中检索信息,还允许用户安全地签署交易。账户信息存储在安装 MetaMask 的本地计算机上,并在硬盘上加密。我们今天最感兴趣的功能是它允许浏览器与以太坊应用程序进行交互。每次交易都会提示用户确认和签署交易。

下列步骤将帮助您安装和配置它:

  1. 让我们访问网站,metamask.io/,并单击“GET CHROME EXTENSION”链接。现在,如果你使用的是其他浏览器,如 Firefox、Opera 或 Brave,你需要单击相应的链接。看一下以下的截图:

  1. 现在,它将打开 Chrome 网上应用店。点击“添加到 Chrome”按钮:

  1. 这将在工具栏上添加 MetaMask 图标,当我们点击该图标时,它将为 MetaMask 打开一个新标签:

  1. 现在,点击“继续”按钮,你就可以创建一个新账户了:

  1. 现在,我们将使用种子短语进行导入,因为我们将连接到本地 Ganache 安装。因此,在这里,我们切换到 Ganache 并复制这个种子短语:

  1. 点击“使用种子短语导入”链接:

  1. 将种子短语粘贴到“钱包种子”字段中,创建一个新密码,然后点击“导入”按钮:

现在,我们必须接受使用条款和隐私通知,然后有一个钓鱼警告,因为有很多攻击企图劫持 MetaMask 以获取私钥的行为要你阅读。

  1. 现在,MetaMask 已连接,它将我们连接到主以太坊网络;但是,我们需要连接到本地 Ganache 安装,所以我们在这里选择网络下拉菜单,你会看到一些不同的选项:

在此,对于我们连接的主网络,以及一些测试网络,如 Ropsten、Kovan 和 Rinkeby,让我们使用自定义 RPC。

  1. 如果我们切换回 Ganache,你会看到 RPC 服务器地址:

  1. 将其复制并粘贴到“New RPC URL”字段中,然后点击“保存”:

  1. 关闭设置窗口,现在它已经连接到 Ganache。你可以看到通过 Ganache 可用的 100 个以太币:

  1. 我们还可以进入账户 1 的详细信息,并给它取一个有意义的名字,这样当我们查看账户列表时,它就有了我们无法识别的东西:

  1. 我们还可以在这里点击顶部并创建一个新账户,如下图所示:

  1. 给它取一个名字,然后点击“创建”,现在看发生了什么。以太坊地址就在这里列出,以 C7CD 结尾:

  1. 所以,如果我们回到 Ganache,我们可以看到第二个账户是由 Ganache 提供的,因此使用 MetaMask 连接到本地 Ganache 的方式允许我们使用在 Ganache 安装中列出的所有账户:

  1. 既然我们在这里,我们将从 Player1 账户发送资金到 Owner 账户,然后将金额设置为 15 以太币;点击“下一步”按钮并确认交易:

以下屏幕截图显示了反映了发送的资金和交易费用,或者说是气体费用的余额:

而且,如果我们切换到所有者账户,那么这 15 个以太币已经添加到那里,总计为 115 个以太币:

  1. 而且,如果我们回到 Ganache,你会看到余额反映在 Ganache 网络本身中:

现在我们已经安装了 MetaMask,我们可以创建并签署我们游戏所需的交易。尽管其中有一些微妙之处,比如气体,起初这似乎没有任何意义:气体与以太坊交易有什么关系呢?好吧,我们将在下一节中找到答案。

理解气体价格和气体限制

本周早些时候,我们了解到我们的智能合约函数在以太坊虚拟机EVM)内运行。这个概念的有趣之处在于,你的交易、函数和代码在计算机上运行,但不是你的计算机。这给了你无限的可扩展性概念,对吗?好吧,惊喜的是,这并不是免费的。这就像你租用你居住或工作的地方,并向房东支付这样做的能力一样,同样地,当我们的合约在以太坊网络上的 EVM 中执行时,我们必须支付我们所使用的计算资源。这被称为气体

在传统应用程序中,你要么托管你自己的数据库服务器,要么利用第三方 SAS 应用程序,并且你会自己支付费用。然而,在以太坊交易中,用户支付这笔费用,因为他们是执行交易的人,即使你可能从中受益:

以太坊气体

这可以被视为类似于交易费用,但它不是一个固定费率,这是因为我们正在为在区块链上执行交易所需的计算资源付费。这个交易可以是简单的货币转账,也可以是构建到你的合约中的复杂业务逻辑的复杂处理,对于这些情况,所需的计算资源量是不同的,因此支付的气体量也是不同的。

气体价格

使用的气体量被跟踪,然后以以太币支付给矿工,这就带来了气体价格。气体价格是你愿意为每单位消耗的气体支付的以太币数量;你可以将其设置为任何你想要的值,同样地,矿工可以选择忽略低于其设定气体价格的任何交易,这使得他们能够跳过对他们而言不会有利可图的任何交易的处理。如果你将你的气体价格设置得太低,矿工们将会忽略它,你的交易将永远得不到确认并被写入区块链。

下面的步骤向你展示了实际情况:

  1. 让我们从一个账户向另一个账户发送以太币:

  1. 在这里进入气费部分,将气价调到 1 确认:

  1. 在这里,你可以看到它已经提交,但实际上还没有得到确认:

并且,它的状态会停留在已提交,因为没有矿工会接受它,因为设定的气价比他们愿意接受的要低。如果我们点击 MetaMask 图标,然后在这里的交易中,它会告诉我,如果我想让那笔交易成功的机会更高,我可以增加气价。那么你应该设置多少气价呢?嗯,MetaMask 实际上做了估算气价的很好的工作,你应该在这里设置一个合理的值,因为气价乘以气费就是你要支付的交易费。

如果你设置的气价太低,就像我们刚刚看到的那样,没有人会处理你的交易,如果你设置得太高,你将支付过多的交易费。还有一个气限,那就是你的交易可以消耗的最大气量。所以,实际上可以设置得更高,因为你只支付实际完成的工作;如果你设置了高气限,而没有全部消耗,剩下的气会退还给你,但如果你的交易达到了气限,但在你的交易中仍然有更多的工作要做,你的气用完了,这算作一笔失败的交易。所以,矿工实际上已经做了你所要求的工作,当他们达到你愿意花费的最大气量时停下来;结果,交易失败了,你不会得到支付给矿工的以太币返还,因为他们做了他们说要做的工作。

所以,实际上有一个非常酷的网站叫做eth gas 站信息,你可以用它来查看当前的气价。使用这个,你可以很好地了解你应该支付多少气价,以及矿工要花多长时间以该价格确认你的交易:

所以,现在,我们可以成功地进行交易,并且可以做出好的财务决策来控制我们的成本,但是在我发送了那笔交易之后会发生什么?一切顺利吗?嗯,可能是可能不是。在接下来的部分,让我们学习如何通过在以太坊网络上验证区块链交易来区分,从而了解每种情况之间的区别。

查看以太坊网络上的区块链交易

好了,我们已经看到如何使用钱包签署交易,现在我们知道了燃气、燃气价格和燃气限制是什么,但还有一个小问题。假设我卖了一些东西得到了 10 个以太币,但买家设置的燃气价格太低,以至于我永远无法得到那笔交易。我们在上一节看到买家会在他们的钱包中看到交易未确认,但我呢?我怎么能确定这笔交易会成功呢?好吧,我可以使用网站 etherscan.io 来查看交易的区块高度。区块高度是指写入区块链的区块数,自包含您的交易的区块以来。

让我们在这里停下来一分钟,回顾一下区块链的工作原理。所以,交易由矿工验证,然后写入一个区块,后续区块写入到那个区块的末尾,包含您的交易的区块之后的区块数称为 区块高度。区块高度越大,您的交易被回滚的可能性就越小。目前每 12 到 15 秒挖掘一个区块,所以很快就可以得到一些确认。但是对于大额交易,建议至少等待六次确认才考虑交易成功。

让我们看看通过交易 ID 就能在以太坊区块链上获取的其他信息。现在,这是来自网站 etherscan.io 的信息,它是以太坊区块浏览器。在这里,您可以通过以太坊地址、交易哈希、区块和一些其他参数进行搜索:

我用这个的其中一项用途是追踪一笔交易。例如,我们知道以太坊网络上的每笔交易都会产生一个唯一的交易 ID,我们可以输入该 ID 并查看该交易的详细信息。我们有交易状态,无论成功与否,区块高度(你现在知道这是自这笔交易被包含在区块链中以来已写入的区块数),以及交易的发起方和接收方;在这种情况下,交易是发送到了 CryptoKiddiesCore 合约,我们还可以看到发送了多少以太币,燃气限制,已使用的燃气,燃气价格和成本:

这里有一件有趣的事情:每个以太坊地址都是一个超链接,所以我可以点击这个链接,查看 CryptoKiddiesCore 合约的所有交易。我可以看到合约余额,合约所有者,甚至 ERC 20 和 ERC 721 代币,以及合约代码本身:

你也可以点击任何发送交易给合约的地址,查看从该地址发出的所有其他交易。这就是为什么以太坊被称为伪匿名而不是匿名的原因;所有的交易都是公开的,但我们不知道这个个人的真实身份。Etherscan 还为常见的测试网络 Ropsten、Kovan 和 Rinkeby 提供了网站,这使得在解决自己本地 Ganache 网络之外的交易时,它成为一个非常有用的工具。有了这些信息,你能够在采取任何交易行动之前验证以太坊网络上的任何交易是否有效和确认。所以现在,让我们更深入地了解在线和离线钱包,这样你就能更好地理解它们的工作原理。

理解在线和离线钱包

在这一部分,我们将学习关于在线和离线钱包的知识,因为这是一个相当令人困惑的概念,我想确保在把你培养成区块链开发者之前,你对它有扎实的理解。

分布式账本

我想引用区块链中用来描述技术的术语,分布式账本,特别是“账本”这个词。账本并不新鲜;它们已经存在了几个世纪,用来记录人与人之间的交易,它们只是对交易的书面描述,资金来自谁,去向谁,以及多少。通过查看这个账本,你可以清楚地看到 1836 年这家摄影企业发生了什么,如果这个人把所有的钱都记录在同一个钱包里,我们就能从这个账本中确切地知道他的钱包在任何一天有多少钱:

多个账本

区块链是一个分散的账本,这意味着我们不是有一个小破旧的笔记本,而是在世界各地的计算机上有多个数字副本存储着这个账本。你能够向这个账本写入新的条目的唯一方式是拥有私钥来为你的账户签署交易。这个账本的管理者,矿工,不知道你的私钥,但他们可以验证签名来自你使用的公钥:

纸钱包

您的钱包是保存您的私钥的技术部件,而这个技术部件可以简单到一张纸,因为您只需要使用您的钱包的种子短语就能进行交易。您已经看到了如何在 MetaMask 中操作,因此您完全可以将种子短语离线保存在一张纸上,在需要进行交易时将其输入到 MetaMask 中,然后从 MetaMask 中删除该账户。没有将您的信息放在线上比不上这种方式提供的安全性更高。同时,如果这是您的在线业务使用以太坊的账户,您的业务可以在线上持续运作,全天候工作,您业务赚取的所有资金将被记录在去中心化的分类账中,您可以放心这些资金在您需要时会在那里。离线钱包的缺点是方便性;每次想要进行交易都需要设置一个钱包,这肯定不方便,并且您可能不想随身携带这张小纸条到处走。我是说,如果您在雨天跌进了水坑,它湿了会怎么办:

硬件钱包

硬件钱包(像是 Ledger Nano)提供了与纸钱包类似的安全级别,但使用起来更加方便。它们是受 PIN 码保护的加密设备,必须输入 PIN 码才能解锁。它连接到您的计算机,您可以使用软件钱包与硬件钱包进行交互。现在,这可能会让人感到困惑,因为术语“硬件钱包”似乎表明资金存储在钱包上,但实际上并不是。实际上存储在钱包上的是用于签署与钱包地址相关的交易的私钥。创建的任何交易都会发送到区块链,其中您的账户的分类账会得到更新;这意味着一旦您将硬件钱包从计算机上拔下来,就没有任何其他交易可以从您的账户上进行。硬件钱包也有一个种子短语,就像我们在 Ganache 中使用的那样,所以您可以将种子短语写在纸钱包上,如果此设备损坏,可以使用种子短语将其恢复到新设备上:

完全相反的是在线钱包。正如我在第一节中提到的,如今 Coinbase 在这一领域是无可争议的领导者;看一下他们的网站,这是全球每个互联网用户都熟悉的界面。互联网上的任何人都曾经填写过类似于此的表单来创建账户,使用像 Coinbase 这样的在线钱包还带有其他一些功能:

如果您忘记了您的凭据,您可以重置它们:

在这里,您还可以免费获得一些高级功能,例如每次交易后都会获得一个新的钱包地址,以防止有人建立您账户的完整财务档案,就像我们在上一节中使用区块链浏览器看到的那样:

尽管如此,您并不拥有也无法访问您账户的私钥;这意味着您账户的安全最终由钱包提供者控制,如果发生了什么,您完全依赖于他们来解决。 现在,这并不是一个全新的概念,也不是加密货币独有的; 它与您当前拥有的其他每个金融账户的工作方式完全相同:您的银行账户,您的储蓄账户,股票所有权和退休计划。 其最大的弱点实际上是其最引人注目的特点,因为它的运作方式与我们习惯的其他金融工具相同,这种熟悉感降低了新用户的进入门槛,但以牺牲安全性为代价:

移动和桌面钱包

在纸钱包和在线钱包之间,我们有桌面和移动钱包,比如 MetaMask。 MetaMask 是一个浏览器插件,安装方式与用户熟悉的任何其他软件相同;这降低了新用户的门槛,即使他们不理解底层技术。 与 MetaMask 钱包关联的私钥存储在安装它的计算机上,并且还加密了,只有知道您的密码的人才能访问它。 但是,计算机会崩溃,对吧? 嗯,有了 MetaMask,您可以备份该私钥,这样您就可以将其恢复到另一台计算机上,甚至可以在多台计算机上使用它。 MetaMask 对我们这本书的最重要功能是它与浏览器集成,这为我们创建的 DApp 和用户的以太坊钱包之间提供了无缝交互,以及为用户提供了额外的安全级别,因为我们没有直接访问他们的钱包,而是创建了一个交易,MetaMask 会向用户呈现交易,并提供确认和签名交易或拒绝交易的选项。 好吧,事实是:您不必选择其中一个钱包,您可以同时使用它们并创建一个个性化的钱包系统,以提供所有功能的最佳体验。

您可以拥有一个 Coinbase 帐户,用于购买诸如以太坊之类的加密货币,一旦您拥有了以太,您可以将其中的一部分转移到钱包,例如 MetaMask,在那里可以与 DApp 进行交互,并且您要持有的任何以太均可以发送到硬件钱包进行存储,您可以将 MetaMask 钱包和硬件钱包都备份到纸钱包中,这些纸钱包存储在不同的物理位置。这为您提供了像 Coinbase 这样的在线钱包的便利性,使用 MetaMask 这样的 DApp 轻松交互,使用硬件钱包安全存储大部分加密货币,以及使用纸钱包备份的故障安全性。这是一种全面而彻底的管理策略,也是我使用的确切策略。以下图表描述了 Coinbase 与 MetaMask:

使用您在此处学到的不同类型的钱包,您可以创建一个综合性的钱包策略,平衡适合应用程序角色的安全性和易用性。当您开始自己创建 DApp 时,我鼓励您在您的 DApp 中包含这样的策略和信息。去中心化应用程序还处于起步阶段,对于绝大多数用户来说,您的 DApp 可能是他们与之进行的第一次交互,因此他们不会拥有这种知识。将其作为您的入门流程的一部分包含进去,确保他们知道如何保护他们的资产,并降低了进入门槛。还有一件事情要讲述:我们的应用程序如何知道 MetaMask 的存在?因此,让我们跳到今天的最后一节来找出答案。

注入 Web3 提供程序

因此,我们已经了解了 MetaMask 如何创建和签署交易以及如何管理帐户,但我们还没有讨论我们的应用程序甚至如何知道 MetaMask 的存在。

以下屏幕截图显示了应用程序容器,这是我们应用程序启动时加载的主要容器:

如果我们在这里看一下 componentDidMount,我们会检查 window.web3 是否已定义;如果已定义,这意味着用户已加载 MetaMask 并将 Web3 提供程序注入到浏览器中:

目前,MetaMask 会在此时提示用户允许 MetaMask 注入到浏览器中。他们这样做的原因是因为区块链的核心原则之一是安全性,如果您能够从任何 DApp 中读取他们的账户信息,他们可能不希望这样,因此我们稍微改变了一下,以便在您允许从以太坊账户中读取任何内容之前,必须首先提示他们并获得他们的许可。在这里发生的是,我们检查 MetaMask 是否已将 Web3 提供程序注入到浏览器中;如果是,那么我们将设置一个名为 currentProvider 的变量,该变量等于 Web3 提供的当前提供程序。现在,这里对你来说会看起来非常熟悉,因为我们在整个游戏中都使用了相同的动作减少器模式。

我们将调用提供程序动作。看一下以下截图:

所以,如果我们在这里看一下,ProviderActionCreator 函数来自 core/actions/actions-provider

在这里,我们将调用 setProvider 函数:

在这里,我们有 setProvider,它将返回一个 dispatch,从提供程序调用 getAccounts 方法:

这将分派到 reducer provider。现在,reducer provider 将获取我们从 Web3 中获取的提供程序,并将其保存到 Redux 存储中:

一旦保存到我们的 Redux 存储中,如果我们看看我们的 actions-game 提供程序,每当我们调用 playRound 时,你会发现我们正在调用 Redux 存储中的 web3Provider。然后,我们调用 setProvider 方法,该方法从注入的 Web3 实例中获取当前提供程序,然后我们在应用程序的其余部分中使用它来进行以太坊调用我们的合约:

所有这些都发生在应用程序加载并且组件挂载时,我们检查 web3Provider 是否已经被注入到浏览器中;如果是,我们调用一个动作来获取该 web3Provider,将其分派给减少器,然后减少器将该提供程序保存到 Redux 存储中,这使其在我们的游戏中可用于进行以太坊交易调用:

现在,让我们来看一下今天的作业,你将安装和配置 MetaMask 以与我们的 DApp 交互。

任务

好了,这一章,我们为你的游戏创建了一个记分牌;现在,是时候玩一场游戏并测试一下了。

对于今天的任务,以下是你需要做的事情:

  1. 安装 MetaMask。要这样做,您将转到 MetaMask.io 并按照说明操作,就像我们在之前的章节中所做的一样。请记住,您需要使用 Chrome、Brave、Firefox 或 Opera 浏览器才能使用 MetaMask。

  2. 接下来,启动 Ganache 并复制助记词。

  3. 使用助记词在 MetaMask 中设置您的帐户,并且 MetaMask 应该默认连接到主以太坊网络,所以您需要将其配置为使用 Ganache。

  4. 一旦正确完成了这些步骤,您将会看到来自 Ganache 和 MetaMask 的帐户余额,然后您需要部署您的合约。

当您这样做时,很可能会看到这个错误,让我们看看它的意思:

关键行在于,试图运行交易,但接收地址(...)不是合约地址。这告诉我们的是,您以前使用这个配置部署了此合约,但部署的地址在此网络上不存在。这个错误是为了防止您无意中在同一个网络上部署多个实例的合约,这不是一件好事吗?如果那样做了,您怎么知道网络上哪个合约是正确的呢?

在这种特殊情况下,忽略这个错误是可以的;我们看到这个错误是因为我们重新启动了 Ganache,这会创建一个全新的网络,因此上次运行 Ganache 时部署的合约确实不存在。

所以,要解决这个错误,运行以下命令:

truffle migrate -- reset 

现在,您可以玩几轮游戏了。

检查昨天构建的记分板是否根据正确的变量更新,并检查当您赢得或输掉几轮时,MetaMask 中的帐户余额是否正确更新。我们的应用程序现在已经完成并且正常运行;但是,它只在您的本地工作站上运行,对吧?

总结

在本章中,我们学习了关于钱包的所有知识,它们的类型以及所涉及的安全性。然后,我们了解了 MetaMask,这是我们将在应用程序中使用的钱包。接下来,我们研究了以太坊的燃气工作原理以及如何使用它。之后,我们探讨了不同类型的在线和离线钱包。最后,我们学习了如何为我们的应用程序向以太坊注入 Web3 提供程序。

在下一章中,我们将看看如何将我们的合约部署到公共以太坊网络,并使用 AWS 将我们的用户界面部署到公共服务器上。

第七章:第七天 - 部署到网络

好了,这是第七天,也是我们书的最后一天。我们有一个可用的 DApp,今天,我们将通过将合同部署到 Ropsten 测试网络,并使用亚马逊网络服务AWS公开我们的 UI,来将其推向最终阶段。

本章涵盖以下主题:

  • 理解 UI 和智能合约的作用

  • 将智能合约部署到以太坊网络

  • 在测试网络中获得以太币

  • 将 UI 部署到 AWS

理解 UI 和智能合约的作用

测试网络就像其听起来的那样——它是一个供您使用的公共以太坊网络,但在这些网络上使用的以太币并不真实,有点像纸币。因此,主要有三个以太坊测试网络,它们分别是:

  • Kovan

  • Rinkeby

  • Ropsten

这些测试网络的运行方式各有不同,所以让我们重点介绍一下它们各自的功能。这将帮助您决定在未来的项目中要使用哪个网络。

Rinkeby 使用权威证明POA)共识协议,这意味着不是每个人都可以在该网络上进行挖矿,只有经过批准的矿工才能够。这使其能够抵抗垃圾邮件攻击,结果是更稳定的测试网络。它还支持 Geth 以太坊客户端,但不支持 Parity。到目前为止,我们一直在使用 Ganache,因为它既提供了我们的本地区块链网络,又提供了我们的客户端,这使我们更容易专注于开发。

Ganache 不能作为公共以太坊网络的客户端,这就是 Geth 和 Parity 的作用。我将在本章的后面向您展示如何安装和配置 Geth。

Kovan 也是一个 POA 网络,这意味着您不能在那里启动自己的挖矿服务器并进行以太币挖矿。此外,Kovan 不支持 Geth,而是支持 Parity 客户端。

最后,我们有 Ropsten,它使用工作量证明POW)作为共识算法,任何人都可以在该网络上启动一个挖矿服务器。因此,它容易受到垃圾邮件攻击,因此网络性能有时可能会有些不稳定。但值得使用,因为它与主要以太坊网络完全相同——因此当您在这里进行测试时,您就在一个与您在生产中将要使用的环境完全相同的环境中进行测试。它还同时支持 Geth 和 Parity。

我们可以通过以下表格总结这三个网络的优缺点:

网络 共识协议 Geth Parity
Rinkeby POA Yes No
Kovan POA No Yes
Ropsten POW Yes Yes

我们将使用 Ropsten 网络来部署和测试我们的合同,因此让我们高层次地了解一下我们在谈论部署时在讨论什么。

到目前为止,我们所做的一切都是在您的本地工作站上完成的。UI 是在本地使用 Node.js 运行的,Ganache 在本地提供以太坊网络,它们在您的工作站上进行交互:

当我们谈论部署时,我们谈论的是将你的 Solidity 合约编译并迁移到公共测试网络,并将你的 UI 代码推送到公共服务器。因此,当您在测试网络上测试您的 DApp 时,您使用本地计算机上的浏览器查看部署到 AWS 的 UI,而 AWS 知道如何使用 Geth 客户端与以太坊测试网络上的您的合约进行通信。以下图表显示了这是如何工作的:

将代码从本地工作站推送到公共服务器和网络的过程有时被称为部署流水线。现在,让我们看看如何将我们编写的合约部署到公共以太坊网络。

将智能合约部署到以太坊网络

在前面的部分中,我们了解了三个可用的以太坊测试网络,以及我们的游戏在本地操作与部署时的差异。现在,让我们将我们的合约部署到 Ropsten 测试网络。在我们的代码存储库中,我们有一个名为truffle.js的文件,它当前看起来类似于以下代码片段:

module.exports = {
  migrations_directory: "./migrations",
  solc: {
    optimizer: {
      enabled: true,
      runs: 2000
    }
  },
  networks: {
    development: {
      host: "127.0.0.1",
      port: 7545,
      network_id: "*" // Match any network id
    }
  }
};

network部分是我们的应用程序知道要与哪个网络通信的方式。我们定义了一个开发网络,如果您不告诉 Truffle 其他情况,它将使用此开发网络。主机设置为我们的 localhost,端口设置为我们本地 Ganache 安装运行的端口。最后,我们有一个网络 ID,当您在本地运行 Ganache 时,我们可以将其设置为使用任何网络 ID。

当我们部署到公共网络时,我们需要将 ID 设置为我们要部署到的网络相匹配的 ID。主要的公共网络是编号 1;这是你在将应用程序部署到生产环境时将使用的 ID。要部署到 Ropsten 网络,我们使用网络 ID 3;对于 Rinkeby,我们可以使用网络 ID 4;对于 Kovan,我们可以使用网络 ID 42。

所以,我们将回到我们的truffle.js配置文件中,然后,在网络部分中,我们将添加一个新的条目用于 Ropsten,如下面的代码片段所示:

    ropsten: {
      host: "127.0.0.1",
      port: 8545,
      network_id: 3,
    }

我们为它指定了我们将要使用的网络的 ID。主机将是本地主机的8545端口,我们刚刚了解到 Ropsten 网络的 ID 是 3,所以我们将它输入为network_id

所以,如果您看一下代码,我们只是将其设置为本地主机上的8545端口,但我们没有任何东西在那里运行——怎么回事?

到目前为止,我们一直在使用 Ganache 作为我们的以太坊网络,并且它为我们处理了一切,但现在我们需要一种方法来与不是我们的以太坊网络进行通信。我们需要一个以太坊客户端,我们将使用 Geth。

Geth

Geth 是用 Go 编写的以太坊客户端,您可以从github.com/ethereum/go-ethereum/wiki/Building-Ethereum下载它。对于 OS X、Linux 和 Windows,都有可用的安装程序,以及 Docker 镜像。

有了 Geth 安装好了,我们可以从终端启动客户端。这是您将使用的命令:

$ geth --testnet --syncmode "light" --rpc --rpcapi db,eth,net,web3,personal,admin --cache=1024 --rpcport 8545

让我们解析一下这个命令以理解它。首先,我们有--testnet,它告诉 Geth 我们要使用 Ropsten 测试网络。接下来,我们用参数light指定同步模式。这告诉 Geth 我们只是想获取当前状态,我们不会处理交易或验证元素。如果需要验证任何内容,我们将联系网络上的一个完整节点。我们之所以这样做,是因为这是在您的工作站上运行客户端的最轻量级的方式,而不会消耗您可能需要用于其他应用程序的大量 CPU 和内存。我们使用--rpc标志在客户端中启用 RPC 模式,后跟--rpcapi标志和要启用的 API 列表,如下所示:

  • dbdb API 允许我们读写本地数据库。

  • etheth参数提供对以太账户及相关功能(如账户、余额和交易)的 API 访问。

  • netnet参数提供与网络相关的 API 调用。

  • web3:此参数提供了我们的web3库。

  • personal:此参数提供 API 访问以管理密钥库中的私钥。

  • admin:此参数启用对 Geth 实例的细粒度访问。

我们的选择列表最后一个选项是一个缓存,大小为 1GB,并且我们将客户端设置为监听 TCP 端口 8545,这与 Ganache 不同,Ganache 使用的是 RPC 端口 7545。

然后我们按下Enter,这将启动客户端,并开始通过从测试网络下载当前块头来同步。您可以在输出截图中看到这个过程:

这需要一点时间才能完成。我们可以通过打开另一个终端会话来跟踪其进度,在那个窗口中,我们将输入以下命令:

$ geth attach http://127.0.0.1:8545

运行此命令将我们置于 JavaScript 控制台中,在那里我们可以与此客户端进行交互,如下截图所示:

然后我们将执行以下命令:

web3.eth.syncing

我们将会得到类似以下截图的内容:

在这里,我们可以看到正在下载的当前区块是 3,214,911,而网络上的最高区块是 3,533,621。这意味着我还有大约 300,000 个区块需要下载,这将需要几分钟时间。

因此,目前我们已将 Truffle 配置为能够部署到 Ropsten 网络。我们已安装并启动了 Geth 作为 Ropsten 网络上的客户端,并且让其同步。现在,我们想将我们的 Solidity 合约部署到 Ropsten 网络,但这会创建一个交易,并且交易需要 gas,而目前我们没有花费的钱。这是因为我们的以太币在我们的本地 Ganache 网络上,现在我们使用的是 Ropsten 网络。这两个网络完全不同,因此您在其中一个网络中拥有的以太币与您在另一个网络上拥有的以太币不同。不过我们可以解决这个问题,而且其中的操作简单是我选择部署到 Ropsten 网络的原因之一。因此,让我们进入下一部分,我们将看看如何获取在 Ropsten 网络上使用的以太币。

在测试网络获取以太币

现在我们准备好将我们的合约部署到 Ropsten 网络,但我们没有任何以太币,所以让我们解决这个问题。

让我们回到浏览器,在 MetaMask 中,您可能连接到您的私人网络,因此请单击下拉菜单,并切换到 Ropsten 网络,如下所示:

然后,在浏览器标签中,转到faucet.ropsten.be。这称为水龙头,就像廉价旅馆内漏水的浴室水龙头一样,这个水龙头也会漏水。

不过,与每隔几秒泄漏水一样,这个水龙头泄漏的是以太币。水龙头会自动将您的以太坊地址从 MetaMask 填入文本框中,并单击“向我发送测试以太币”按钮,将向水龙头提交您的地址,然后水龙头会向您发送 1 个以太币。以下截图显示了水龙头窗口:

在这里需要知道的一件事是,水龙头每 30 秒只会滴下 1 个以太币,因此如果有排在您前面的请求,可能需要几分钟时间以才能在您的账户中显示以太币。您应该不断检查,因为它会显示出来,当它出现时,您现在有了一些零花钱,可以支付交易以将您的合约部署到 Ropsten 网络。

因此,我们现在有了一些以太币,我们想使用 Truffle 来部署我们的合约。并且,我们已经将 Geth 运行为我们的以太坊客户端,因此 Geth 需要访问我们在 Ropsten 网络上的账户,以便支付该交易。

为此,我们将单击 MetaMask 上的“详细信息”按钮,并导出我们的私钥:

这里,我们将点击“导出私钥”按钮,这会提示您输入密码,这样会显示私钥。您应该将私钥复制到一个易于访问的文件中。

现在我们将运行以下命令:

web3.eth.accounts

这会产生以下输出:

您可以看到当前在 Geth 中没有列出任何账户。所以,在一个新的终端窗口中,我们将导航到包含包含私钥的文本文件的代码目录,并在那里运行以下命令:

$ geth account import private.txt --keystore `/Library/Ethereum/testnet/keystore/

这将产生以下输出:

注意:上述命令适用于 OSX,如果你使用的是 Windows 或 Linux,请使用以下命令:

对于 Windows:geth account import private.txt --keystore %APPDATA%\Ethereum\testnet\keystore,以及

对于 Linux:geth account import private.txt --keystore /.ethereum/testnet/keystore`。

如果我们看一下那个地址的最后几位,3eae6,我们会发现它与我们在 MetaMask 中列出的账户的最后几位相匹配。

所以,现在我们将回到我们的 JavaScript Geth 控制台并再次运行账户命令。这次,我们会在这里看到我们的账户列出来:

这是一个受密码保护的账户,所以我们需要为 Geth 解锁它。为此,请运行以下命令:

personal.unlockAccount(eth.accounts[0])

这将产生以下输出:

由于口令返回true,我们知道账户已成功解锁。这里的一个重要步骤是删除该私有文件,因为它包含私钥,你不想让它留在任何地方。

打开 truffle.js 文件,在 Ropsten 配置中,我们将添加一行新的内容,即 from,然后指定我们刚刚导入到 Geth 中的账户的地址。这将告诉 Truffle 在部署合约时使用哪个账户。

切换回终端,我们将运行以下命令:

$ truffle compile
$ truffle migrate --network ropsten

这将产生以下输出:

它看起来就像在 Ganache 上一样,它已成功迁移到网络上。

如果你想要测试一下,此时可以键入以下命令:

npm start

这将启动我们的 web 服务器,并将使用刚部署的合约,这样我们就可以对我们的神秘数字下注。以下截图显示了网页:

当我们下注时,我们会收到一个弹出通知:

此窗口显示我们已连接到 Ropsten 测试网络,因此我们可以确认,并且如前面的截图所示,我们输掉了那笔赌注,所以,我的账户被扣除了我下注的 0.1 以太币。因此,我们成功地将我们的合约部署到了 Ropsten 网络,并且我们能够从我们的本地 UI 与之交互。本章的最后一个任务是将 UI 部署到 AWS。

将 UI 部署到 AWS

要将我们的游戏发布到世界上,我们需要将 UI 提供给互联网。我们将使用 Docker 和 AWS 来做到这一点。

如果您对 Docker 不熟悉,那么它是一个在操作系统级别执行虚拟化的工具,这被称为容器化。这意味着它将您的代码及其运行所需的一切打包到一个单独的容器中,该容器可以在 Docker 主机上执行。我们将使用 AWS 弹性容器服务ECS)作为我们的 Docker 主机。这使我们能够运行我们的容器,而无需构建或管理底层基础结构。

要构建 Docker 镜像,您需要安装 Docker 版本 18 或更高版本。如果您没有安装,可以从 docker.com 下载,并确保您下载的是社区版,因为那是免费版本,但它包含我们所需的所有内容。

要创建我们的 Docker 镜像,我们将使用 Docker 文件。这是一组指令,指定如何构建容器。我们将逐步分解我们在此 Docker 文件中添加的内容。

我们首先定义我们的基础镜像。使用 Docker,您可以使用另一个 Docker 镜像作为您的镜像的基础,从而使您能够在其他人已经完成的工作基础上构建。在我们的情况下,我们需要 Node.js,所以我们将使用官方的 Node.js Docker 镜像。在 Linux 服务器上安装和运行 Node.js 的所有步骤都已经为我们准备好了。以下代码片段显示了我们在这里所做的事情:

FROM node:8.11.3-stretch

然后,我们使用COPY命令将当前目录中的所有内容复制到容器中名为app的文件夹中,并切换到该目录。之后,我们将设置一个名为NODE_ENV development的环境变量。当 Node 启动时,Node.js 将使用它。对于生产构建,您将其设置为production而不是development。以下代码片段显示了此操作:

COPY ./app
WORKDIR /app

ENV NODE_ENV development

接下来,我们将运行apt-get install,这是 Debian 命令来更新基础镜像并安装git-core,它允许从 GitHub 存储库安装一些我们的节点依赖项。然后,我们运行npm install命令来安装我们的 Node 应用程序的所有依赖项,如下代码片段所示:

RUN apt-get update && apt-get install -y git-core
RUN npm install

随后,EXPOSE命令在我们的 Docker 容器上打开 TCP 端口 3000,这允许我们在端口 3000 上连接到我们的网站。最后,我们有一个在容器启动时执行的命令,如下所示:

EXPOSE 3000

CMD ["npm", "start"]

所以,这相对来说很简单,因为我们基本上只是输入了设置应用程序的相同命令的 Docker 版本。所有这些都会被执行并保存到我们的 Docker 镜像中,当我们启动镜像时,将执行npm start命令。

现在,我们可以使用以下命令构建我们的 Docker 镜像:

docker build -t gaming:latest

我们提供-t标志以使用gaming:latest标签标记我们的镜像,末尾的点告诉 Docker 在当前目录中查找我们的 Docker 文件。这将执行我们放入 Docker 文件中的所有命令,并将输出存储为 Docker 镜像。

另一种构建镜像的方法是,如果你使用的是 Visual Code,你可以右键单击 Docker 文件,然后选择“构建镜像”。它会提示你输入镜像名称,即gaming:latest,然后按Enter,这将运行先前提到的相同命令。完成后,我们将登录 AWS,选择 ECS。这将带领您进入以下页面:

这里有几个不同的部分,我们将逐个讨论。让我们首先从存储库开始;把存储库看作只是一个容纳你构建的 Docker 镜像的桶。我们将它们放在亚马逊或 AWS 上,这样在部署此服务时,亚马逊就有了获取这个镜像的方式:

我们将创建一个新的存储库,并将其命名为我们的应用程序gaming。创建完成后,它将为我们提供 URL 以及我们将需要的命令:

所以,我们要复制第一个命令,然后粘贴到终端中。这将执行 ECS 控制台的登录功能,或者 ECS 存储库的登录,因为除非你经过身份验证,否则无法推送到存储库。这样可以防止外部人员将他们的 Docker 镜像推送到您的存储库。

你可以看到,为了方便起见,它为我们列出了 Docker 构建命令,但我们已经完成了。所以,我们将转到下一个命令,给镜像打标签以供存储库使用。我们将运行这个命令,然后我们将获取最后一个命令,将我们的 Docker 镜像推送到存储库中。

上传完该镜像后,我们可以单击“完成”按钮,然后我们会在存储库中看到我们的镜像已列出:

现在,我们将创建一个任务定义;任务定义是定义我们想要作为服务的一部分运行的所有不同容器的方法。我们将通过进入 AWS 的任务定义选项卡来完成这个步骤:

点击“创建新任务定义”,然后我们给我们的任务定义命名为gaming-staging,然后需要选择一个任务角色。我已经在这里构建了一个,但如果你没有构建,一个基本的 ECS 任务执行规则也可以。以下屏幕截图显示了这是什么样子的:

我们的 ECS 功能内部不需要任何特殊权限。我将给它分配 8GB 内存和 4 个 CPU,因为这足以为 React UI 应用程序和 Geth 客户端节点提供动力。现在,我们需要添加我们的容器:

我们的第一个容器将是我们的gaming-ui,这是我们构建的 React 应用程序,图像来自我们推送到的 ECS 存储库。如果我们查看我们的存储库,那里存在的存储库 URI 就是我们需要的 URL,我们将把它粘贴到图像框中。我们需要公开端口 3000,因为那是我们的应用程序运行的端口。然后,我们需要指定一个健康检查。我们将在健康检查窗口中使用以下命令:

curl http://127.0.0.1:3000 || exit 1

这将使用curl命令检查本地主机,这将确定端口 3000 是否响应。如果没有响应,命令将以值 1 退出,这样 ECS 就知道容器没有健康,并将其拆除并替换。

我们希望每 60 秒检查一次,超时为 15 秒,并在启动前给自己 60 秒进行第一次测试。我们将允许其在失败之前有两次通过的机会:

我们不需要在该容器上设置其他任何东西,因此我们可以点击“添加”。

然后,我们将添加第二个容器。这个将是我们的geth-client容器。它将来自以太坊仓库,使用以太坊提供的client-go映像。对于我们的端口映射,我们正在公开端口 8545,对于我们的健康检查命令,我们将echo "true",这将始终使其通过。我们将像以前一样做:60 秒的间隔时间,15 秒的超时时间,60 秒的启动时间和 2 次重试尝试。

对于命令,我们需要做的唯一事情就是设置--rpc--testnet标志。其他一切都在 Docker 映像内设置。我们将点击“添加”,然后创建它。

现在,我们需要转到 EC2,创建一个负载均衡器。负载均衡器将为我们提供一个前端界面,以便每当我们访问此 URL 时,它将在多个 Docker 容器之间平衡,或者如果该 Docker 映像失败,则将使用新映像启动它并将其放入该负载均衡器中,以便我们的 URL 不会更改。

点击负载均衡器,创建一个,并且我们将在这里使用应用负载均衡器:

我们将给其设置与任务定义相同的名称,即gaming-staging。这将使我们轻松看到各资源与 AWS 中的其他资源相关联时哪些资源相关。它将具有互联网面向的属性,并在端口 80 上使用 HTTP。我们需要设置 VPC 并在两个区域之间平衡,以便在 AWS 中实现一定的容错和冗余:

接下来,我们将将其放在一个安全组中,公开用于 www 服务器的端口,基本上公开端口 80 和端口 443:

然后我们将在 80 端口上创建一个目标组,目标类型为 IP 端口;这是 ECS 将要放入负载均衡器的容器或资源池:

我们还没有任何要注册的目标,所以我们继续点击创建:

是时候回到 ECS,创建实际运行应用程序的服务了。现在,我们选择创建集群,并且创建一个 Fargate 集群的过程非常简单:

完成后,我们应该会看到类似以下截图:

在集群中,我们将点击创建以创建一个新服务,指定 Fargate 为启动类型,然后选择我们刚刚创建的任务定义gaming-staging。我将为服务名称指定与所有这些资源一样的名称,然后指定我想要 1 个任务。如果你将要期望更多的流量,或者如果你想要确保如果其中一个容器失败了,还有另一个可以接管流量,你可以指定更多的任务:

我将指定与我放置负载均衡器的 VPC 相同的 VPC,然后,对于负载均衡器,我将选择我们刚刚构建的负载均衡器:

我们可以查看所有我们的设置,然后点击创建服务:

几分钟后,我们的服务应该已经开始运行起来了。一旦它开始运行,我们就可以切换回到 EC2,进入到我们的负载均衡器,选择我们创建的负载均衡器,并复制那里的 DNS 名称:

我们将把它粘贴到我们的浏览器中,就这样,我们的应用程序就在 AWS 上加载运行了,使用以太坊网络的 Ropsten 网络。

还有一件事情我们需要检查,这也是你最关心的事情之一。我们将返回 ECS 并告诉你如何关闭它,这样你就不会继续为它付费了。进入服务,并选择我们创建的服务,然后点击更新并将任务数设置为 0,然后更新服务。这将关闭运行的容器,以便你不再为其付费:

使用本章学到的步骤,你也可以将你的应用程序发布到以太坊主网络,使你的游戏玩家可以使用真正的以太币。

说了这么多,让我们来进行今天的最后一个部分,也是这本书的最后一个部分,也就是今天的作业。

作业

在本书的最后一项作业任务中,你将使用本章学到的所有步骤在 AWS 上启动你的应用:

  1. 今天你的第一个任务是将你的合约部署到 Ropsten 网络上。记住,你首先需要在 Ropsten 上创建一个账户,并从水龙头获取一些以太币,以支付部署合约的燃气费用。

  2. 一旦你部署了你的合约,你将会构建 Docker 镜像来包含你的用户界面,然后登录 AWS 并创建一个存储库来保存你的镜像,并将你的镜像推送到该存储库。

  3. 我希望你创建一个任务定义,定义你的应用程序如何运行。你需要为你的 UI 应用程序和 Geth 客户端定义一个任务。通过将它们都放在同一个任务定义中,你可以确保你的 UI 应用程序始终在本地运行一个 Geth 客户端,以便与 Robsten 网络通信。

  4. 启动一个新的 ECS 服务来使你的应用上线。一旦它启动,就给你的应用试驾一下。

  5. 确保你在线分享 URL,让其他人也可以尝试。如果你想要成为一个区块链开发者,没有比在线展示你的技能更好的了,让潜在雇主可以看到。

总结

本章标志着学习区块链及如何使用它创建和实现游戏应用的结束。在本章中,我们学习了以太坊网络的工作原理,以及如何创建与之交互的智能合约。我们学会了如何在测试网络中获取以太币。最后,我们学会了如何将我们的应用部署到 AWS,并让全世界的用户都来试试它!

在过去的七天里,我们涵盖了很多内容,还有很多东西要学,但现在你有了开始自己探索更高级主题的技能。我希望你和我一样在区块链上工作时玩得开心,我很想看看你用所学技能建造的东西!

标签:指南,七天,函数,以太,代码,测试,使用,区块,我们
From: https://www.cnblogs.com/apachecn/p/18169380

相关文章

  • 商业中的量子计算和区块链指南(全)
    原文:zh.annas-archive.org/md5/fe0c2b1b6dea032e7c6950de21934dd5译者:飞龙协议:CCBY-NC-SA4.0序言该书提供了对未来的一瞥,即不同行业如何从新一波计算中受益。该书带领读者踏上旅程,首先描述了今天计算机的局限性,并提出了量子计算呈现的可能解决方案,以超越这些限制。这本书......
  • .Net单元测试xUnit和集成测试指南
    引言在现代化的软件开发中,单元测试和集成测试是确保代码质量和可靠性的关键部分。ASP.NETCore社区内提供了强大的单元测试框架,xUnit是其中之一,它提供了简单、清晰和强大的测试功能,编写单元测试有许多优点;有助于回归、提供文档及辅助良好的设计。下面几节我们来深入浅出探讨如......
  • GPT3-探索指南(三)
    GPT3探索指南(三)原文:zh.annas-archive.org/md5/e19ec4b9c1d08c12abd2983dace7ff20译者:飞龙协议:CCBY-NC-SA4.0第九章:构建一个由GPT-3提供动力的问答app到目前为止,我们已经查看了(并编写了)很多代码。但我们实际上还没有创建一个完全可用的app。那就是我们将要做的事情。......
  • Apache SeaTunnel k8s 集群模式 Zeta 引擎部署指南
    SeaTunnel提供了一种运行Zeta引擎(cluster-mode)的方法,可以让Kubernetes在本地运行Zeta引擎,实现更高效的应用程序部署和管理。在本文中,我们将探索SeaTunnelk8s运行zeta引擎(cluster-mode模式)的更多信息,了解如何更好地利用Zeta引擎的优势。将SeaTunnel上传至服务器上。我之前......
  • Qt Creator + MSVC2017编译器配置指南
    QtCreator+MSVC2017编译器配置指南下载和安装MSVC2017编译器下载下载MSVC编译器安装工具:https://docs.microsoft.com/zh-tw/previous-versions/visualstudio/visual-studio-2017/install/use-command-line-parameters-to-install-visual-studio?view=vs-2017安装安......
  • 保姆级指南,从0到1打造你的个人开源项目
    前言各位好久不见,有些小伙伴可能知道大概1年多以前我开始维护log-record项目(Java业务操作日志记录框架)。这期间项目陆陆续续更新迭代、发布新版本,一路走来也踩了不少坑。这篇文章主要是想给希望开始写开源项目的同学们一些开源项目维护的实操建议,也算是给自己梳理一下做一个开源......
  • 洛谷题单指南-动态规划2-P1091 [NOIP2004 提高组] 合唱队形
    原题链接:https://www.luogu.com.cn/problem/P1091题意解读:要挑选一个最长的先上升后下降的序列,求其余的元素数量解题思路:先计算正向的最长上升子序列,设f[i]表示以i结尾的正向最长上升子序列再计算逆向的最长上升子序列,设g[i]表示以i结尾的逆向最长上升子序列再枚举所有的i<j,m......
  • 洛谷题单指南-动态规划2-P1004 [NOIP2000 提高组] 方格取数
    原题链接:https://www.luogu.com.cn/problem/P1004题意解读:从起点走到终点,走两次,计算最大路径和,第一次走过的点数值变为0。解题思路:直观上思考,可以先从起点走到终点,计算最大路径和,并记录走过的所有点,然后把所有点的数值置为0,再从起点走到终点,计算最大路径和,把两次的最大路径......
  • 5分钟教你搭建邮件服务器的实用指南
    今天我写了一篇实用的文章,重点是教你如何免费搭建一个邮件服务器,这个服务器不仅可以用于发送邮件,还可以供我的待办机器人使用。一开始我试图找一些免费的API接口来实现这个功能,但遗憾的是,并没有找到合适的。对于程序员来说,能自己动手实现绝对是最好的选择,幸运的是,我有一台空闲的......
  • Ollama开发指南
    前奏:Ollama是什么,为什么这么好用 安装必备工具确保已安装以下软件的正确版本:CMake3.24或更高版本Go1.22或更高版本GCC11.4.0或更高版本 使用Homebrew安装这些工具(适用于macOS和Linux):brewinstallgocmakegcc 可选:启用调试与详细日志......