cryptozombies5
第十章:SafeMath 第二部分
让我们来看看 SafeMath 背后的代码:
1 | |
首先,我们看到的是 library 关键字——库与合约类似,但有一些不同。对于我们的用途,库允许我们使用 using 关键字,这会自动将库中的所有方法附加到另一个数据类型上:
1 | |
请注意,mul 和 add 函数每个都需要 2 个参数,但当我们声明 using SafeMath for uint 时,我们调用函数的 uint(test)会自动作为第一个参数传递进去。
让我们看看 add 函数的代码,了解 SafeMath 是如何工作的:
1 | |
基本上,add 就是像 + 一样加两个 uint,但它还包含一个 assert 语句,确保结果大于或等于 a。这可以防止溢出。
assert 和 require 类似,都会在条件为 false 时抛出错误。assert 和 require 的区别在于,require 会在函数失败时退还用户的剩余 gas,而 assert 则不会。所以,大多数时候你应该在代码中使用 require;assert 通常用于处理代码出现重大问题的情况(比如 uint 溢出)。
简单来说,SafeMath 的 add、sub、mul 和 div 是执行基本的 4 个数学运算的函数,但如果发生溢出或下溢,它们会抛出错误。
在我们的代码中使用 SafeMath
为了防止溢出和下溢,我们可以检查代码中使用 +、-、* 或 / 的地方,并将它们替换为 add、sub、mul、div。
例如,替换:
1 | |
改为:
1 | |
测试
在 ZombieOwnership 合约中,我们有两个地方使用了数学运算。让我们将它们替换为 SafeMath 方法。
- 替换
++为 SafeMath 方法。 - 替换
--为 SafeMath 方法。
第十三章:注释
我们的僵尸游戏的 Solidity 代码终于完成了!
在接下来的课程中,我们将学习如何将代码部署到 Ethereum,并如何使用 Web3.js 与它进行交互。
但在我们结束第 5 课之前,有一件最后的事情要说:让我们来谈谈如何注释你的代码。
注释语法
在 Solidity 中注释的方式和 JavaScript 很相似。在 CryptoZombies 课程中,你已经见过一些单行注释的例子:
1 | |
只需要在你想注释的地方添加 //,就能进行注释。它非常简单,你应该时常使用它。
但我知道,有时候单行注释是不够的。毕竟,你天生就是个写作者,对吧!
所以我们也有多行注释:
1 | |
特别是,注释代码来解释每个函数的预期行为是一个好习惯。这样,其他开发者(或者在你休息了 6 个月后回到项目时的你)可以快速浏览并高层次地理解你的代码,而不需要深入阅读代码本身。
在 Solidity 社区中,标准是使用一种叫做 natspec 的格式,格式如下:
1 | |
@title和@author是直观的。@notice向用户解释合约/函数的功能。@dev用于向开发者解释额外的细节。@param和@return用于描述函数的每个参数和返回值。
请注意,你不必为每个函数都使用所有的标签——这些标签是可选的。但至少,留下一个 @dev 注释来解释每个函数的作用是一个好习惯。
测试
如果你现在还没有注意到,CryptoZombies 的答案检查器会忽略注释,当它检查你的答案时。因此,我们不能真正检查你在这一章中添加的 natspec 代码 ;)
不过,既然你已经是一个 Solidity 大神——我们就假设你已经掌握了!
无论如何,尝试一下,尝试为 ZombieOwnership 合约添加一些 natspec 标签:
@title—— 例如:一个管理僵尸所有权转移的合约@author—— 你的名字!@dev—— 例如:符合 OpenZeppelin 的 ERC721 规范草案
第十四章:总结
恭喜你!这标志着第 5 课的结束。
作为奖励,我们已经将你自己的 10 级 H4XF13LD MORRIS 💯💯😎💯💯 僵尸转交给你了!
(天啊,传说中的 H4XF13LD MORRIS 💯💯😎💯💯 僵尸!!!!111)
现在,你的军队中有 4 只僵尸了。
在你继续之前,如果你愿意,你可以通过点击僵尸并输入新名字来重命名它们。(不过我不明白为什么你会想要重命名 H4XF13LD MORRIS 💯💯😎💯💯,显然这是最棒的名字。)
让我们回顾一下:
在本课中,我们学习了:
- 代币、ERC721 标准以及可交易资产/僵尸
- 库及如何使用它们
- 如何使用 SafeMath 库防止溢出和下溢
- 注释代码及 natspec 标准
这节课结束了我们游戏的 Solidity 代码!(暂时——未来我们可能会添加更多课程)。
在下一节课中,我们将学习如何部署合约并使用 web3.js 与它们交互(这样你就可以为你的 DApp 构建前端了)。
如果你喜欢,可以去重命名你的僵尸,然后继续到下一章以完成课程。
第 1 章:Web3.js 简介
通过完成第 5 课,我们的僵尸 DApp 已经完成。接下来,我们将创建一个基本的网页,让用户能够与它进行交互。
为此,我们将使用一个来自以太坊基金会的 JavaScript 库——Web3.js。
什么是 Web3.js?
记住,以太坊网络由多个节点组成,每个节点都包含一份区块链副本。当你想调用智能合约中的某个函数时,你需要向其中一个节点发送请求,并告诉它以下内容:
- 智能合约的地址
- 你想调用的函数
- 你想传递给函数的变量
以太坊节点仅支持 JSON-RPC 语言,而这种语言并不适合人类阅读。向节点发送的请求可能类似于下面这样的格式:
1 | |
幸运的是,Web3.js 会把这些复杂的查询隐藏在表面之下,你只需要与一个方便且易于阅读的 JavaScript 接口进行交互。
你不需要手动构造上面的请求,调用函数时,你的代码看起来像这样:
1 | |
我们将在接下来的几章中详细解释语法,但首先让我们用 Web3.js 设置你的项目。
开始使用
根据你项目的工作流,你可以通过以下大多数包管理工具将 Web3.js 添加到项目中:
1 | |
或者,你也可以直接从 GitHub 下载 minified 版本的 .js 文件,并将其包含在项目中:
1 | |
由于我们不想对你的开发环境和使用的包管理器做太多假设,本教程中我们将简单地通过上述的 <script> 标签将 Web3.js 引入项目。
测试
我们为你创建了一个 HTML 项目的框架文件 index.html。假设我们将 web3.min.js 文件放在与 index.html 相同的文件夹中。
现在,请将上面的 <script> 标签复制并粘贴到我们的项目中,这样我们就可以开始使用 Web3.js 了。
第二章:Web3 提供者
太好了!现在我们的项目中已经有了 Web3.js,接下来让我们初始化 Web3 并与区块链进行交互。
我们首先需要的是一个 Web3 提供者。
记住,Ethereum 是由多个节点组成的,每个节点都共享一份相同的数据。设置 Web3 提供者会告诉我们的代码,我们应该与哪个节点进行通信,来处理我们的读写请求。这有点像在传统的 Web 应用中,设置 API 调用的远程 Web 服务器 URL。
你可以将自己的 Ethereum 节点作为提供者来托管。然而,有一个第三方服务可以让你的生活变得更轻松,这样你就不需要自己维护一个 Ethereum 节点就能为用户提供 DApp —— Infura。
Infura
Infura 是一个服务,维护了一组 Ethereum 节点,并为快速读取提供了缓存层,你可以通过他们的 API 免费访问。使用 Infura 作为提供者,你可以可靠地向 Ethereum 区块链发送和接收消息,而无需设置和维护自己的节点。
你可以像下面这样设置 Web3,使用 Infura 作为 Web3 提供者:
1 | |
然而,由于我们的 DApp 将被许多用户使用 —— 而且这些用户不仅仅是读取区块链数据,他们还需要向区块链写入数据 —— 我们需要一种方式让这些用户用他们的私钥签署交易。
注意:Ethereum(以及一般的区块链)使用公钥/私钥对来对交易进行数字签名。可以把它看作是一个非常安全的密码,用于数字签名。这样,如果我更改区块链上的某些数据,我可以通过我的公钥证明是我签署了它——但由于没有人知道我的私钥,没人能够伪造我的交易。
加密技术很复杂,所以除非你是安全专家,真的知道自己在做什么,否则最好不要尝试在我们的应用前端自己管理用户的私钥。
幸运的是,你不需要自己管理——已经有服务专门为你处理这些问题。其中最受欢迎的就是 Metamask。
Metamask
Metamask 是一个适用于 Chrome 和 Firefox 的浏览器扩展,允许用户安全地管理他们的 Ethereum 账户和私钥,并使用这些账户与使用 Web3.js 的网站进行交互。(如果你以前没有使用过它,你绝对需要去安装它——这样你的浏览器就启用了 Web3,你现在可以与任何与 Ethereum 区块链通信的网站进行交互!)
作为开发者,如果你希望用户通过网站与你的 DApp 进行交互(就像我们在 CryptoZombies 游戏中做的那样),你肯定希望让它兼容 Metamask。
注意:Metamask 在底层使用 Infura 的服务器作为 Web3 提供者,就像我们上面做的那样——但它也给用户提供了选择自己的 Web3 提供者的选项。所以,通过使用 Metamask 的 Web3 提供者,你给了用户选择权,也减少了你在应用中需要处理的事情。
使用 Metamask 的 Web3 提供者
Metamask 将它的 Web3 提供者注入到浏览器中的全局 JavaScript 对象 web3。所以,你的应用可以检查是否存在 web3,如果存在,就使用 web3.currentProvider 作为提供者。
下面是 Metamask 提供的模板代码,用于检测用户是否安装了 Metamask,如果没有安装,则告诉他们需要安装 Metamask 才能使用我们的应用:
1 | |
你可以在你创建的所有应用中使用这段模板代码,要求用户必须安装 Metamask 才能使用你的 DApp。
注意:除了 Metamask,用户可能还在使用其他私钥管理程序,如 Mist 浏览器。但是,它们都实现了一个共同的模式,即注入 web3 变量,因此我们在这里描述的检测用户 Web3 提供者的方法也适用于这些程序。
测试
我们已经在 HTML 文件的 </body> 标签之前创建了一些空的 <script> 标签。我们可以在这里编写本课的 JavaScript 代码。
请将上面检测 Metamask 的模板代码复制并粘贴到我们的项目中。这段代码从 window.addEventListener 开始。
第 3 章:与合约交互
现在我们已经通过 MetaMask 的 Web3 提供者初始化了 Web3.js,接下来让我们设置它与智能合约进行交互。
Web3.js 需要两样东西才能与合约进行交互:合约地址和合约 ABI。
合约地址
在你编写完智能合约后,你需要将它编译并部署到 Ethereum 上。我们将在下一课讲解如何部署,但由于这是一个与编写代码完全不同的过程,我们决定先讲解 Web3.js。
部署完合约后,它会获得一个在 Ethereum 上永久存在的固定地址。回想一下第 2 课,CryptoKitties 合约在 Ethereum 主网上的地址是 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d。
你需要在部署后复制这个地址,以便与智能合约进行交互。
合约 ABI
Web3.js 需要的另一样东西是合约的 ABI。
ABI 代表应用程序二进制接口(Application Binary Interface)。基本上,它是合约方法的 JSON 格式表示,告诉 Web3.js 如何格式化函数调用,以便合约可以理解。
当你将合约编译并部署到 Ethereum(我们将在第 7 课讲解如何部署)时,Solidity 编译器会生成 ABI,因此你需要在部署合约时同时复制和保存 ABI。
由于我们还没有讲解如何部署,在这一课中,我们已经为你编译好了 ABI,并将其保存在名为 cryptozombies_abi.js 的文件中,并将其存储在名为 cryptoZombiesABI 的变量中。
如果我们将 cryptozombies_abi.js 包含到项目中,就可以通过该变量访问 CryptoZombies 的 ABI。
实例化 Web3.js 合约
一旦你有了合约的地址和 ABI,就可以像下面这样在 Web3 中实例化它:
1 | |
测试
在我们文档的 <head> 中,包含另一个 <script> 标签,引入 cryptozombies_abi.js,这样就能把 ABI 定义导入到我们的项目中。
在 <body> 标签中的 <script> 部分开头,声明一个名为 cryptoZombies 的变量,但不要赋值。稍后我们将用这个变量来存储我们实例化的合约。
接下来,创建一个名为 startApp() 的函数。我们将在接下来的两步中填充这个函数的内容。
startApp() 函数的第一件事是声明一个名为 cryptoZombiesAddress 的变量,并将其设置为字符串 "YOUR_CONTRACT_ADDRESS"(这是 CryptoZombies 合约在主网的地址)。
最后,实例化合约。将 cryptoZombies 设置为一个新的 web3js.eth.Contract,就像我们在上面的示例代码中所做的那样。(使用 cryptoZombiesABI,它通过 <script> 标签导入,并使用上面声明的 cryptoZombiesAddress)。
第4章:调用合约函数calling contract functions
我们的合约已经设置好了!现在我们可以使用 Web3.js 与它进行交互。
Web3.js 有两个方法可以用来调用合约中的函数:call 和 send。
Call
call 用于view和pure函数。它只在本地节点上运行,不会在区块链上创建交易。
回顾:view 和 pure 函数是只读的,不会更改区块链上的状态。它们也不需要支付任何 gas,用户也不会被提示在 MetaMask 中签署交易。
使用 Web3.js,你可以像下面这样调用名为 myMethod 的函数,并传递参数 123:
1 | |
Send
send 会创建一笔交易并更改区块链上的数据。对于任何非 view 或 pure 的函数,你都需要使用 send。
注意:发送交易将要求用户支付 gas,并且会弹出 MetaMask 提示他们签署交易。当我们使用 MetaMask 作为 Web3 提供者时,所有这些都会在调用 send() 时自动完成,我们的代码不需要做任何特殊处理。很酷吧!
使用 Web3.js,你可以像下面这样发送交易,调用名为 myMethod 的函数,并传递参数 123:
1 | |
语法几乎和 call() 一模一样。
获取僵尸数据
现在,让我们看一个使用 call 来访问合约数据的实际例子。
回想一下,我们将僵尸数组声明为 public:
1 | |
在 Solidity 中,当你声明一个变量为 public 时,它会自动创建一个具有相同名称的公共“getter”函数。因此,如果你想查找 ID 为 15 的僵尸,你可以像调用函数一样使用 zombies(15)。
下面是我们如何在前端编写一个 JavaScript 函数,接受一个僵尸 ID,查询合约中的该僵尸,并返回结果:
注意:我们在本节中使用的所有代码示例都是基于 Web3.js 1.0 版本,该版本使用的是 Promise 而非回调函数。许多在线教程使用的是旧版本的 Web3.js。版本 1.0 的语法变化很大,所以如果你从其他教程复制代码,请确保它们与你使用的版本一致!
1 | |
让我们一起分析一下这里发生了什么。
cryptoZombies.methods.zombies(id).call() 会与 Web3 提供者节点通信,告诉它返回我们合约中 Zombie[] public zombies 数组中的 ID 为 id 的僵尸。
请注意,这是异步的,类似于调用外部服务器的 API。因此,Web3 返回一个 Promise(如果你不熟悉 JavaScript 中的 Promise……在继续之前最好做一些额外的学习!)。
一旦 Promise 被解析(这意味着我们从 Web3 提供者那里收到了响应),我们的示例代码会继续执行 then 语句,并将结果打印到控制台。
result 将是一个 JavaScript 对象,类似于下面这样:
1 | |
然后,我们可以在前端逻辑中解析这个对象,并以有意义的方式将它显示出来。
动手实践
我们已经将 getZombieDetails 函数复制到你的代码中。
- 让我们创建一个类似的函数来获取
zombieToOwner。如果你还记得在ZombieFactory.sol中,我们有一个像这样的映射:
1 | |
定义一个名为 zombieToOwner 的 JavaScript 函数。与上面的 getZombieDetails 类似,它将接受一个 id 作为参数,并返回对我们合约中 zombieToOwner 的 Web3.js 调用。
- 接下来,创建第三个函数
getZombiesByOwner。如果你还记得在ZombieHelper.sol中,函数定义看起来像这样:
1 | |
我们的 getZombiesByOwner 函数将接受 owner 作为参数,并返回对 getZombiesByOwner 的 Web3.js 调用。
JavaScript 中的 Promise是什么?
在 JavaScript 中,**Promise** 是一种用于处理异步操作的机制。它表示一个可能在未来某个时间点完成的操作(异步操作)的结果。可以将其理解为一个“承诺”,即某个操作将在未来完成,并且它会有一个结果(成功或失败)。
Promise 的三个状态
一个 Promise 对象可以处于以下三种状态之一:
- Pending(等待中):初始状态,表示异步操作尚未完成。
- Fulfilled(已完成):异步操作已成功完成,并且返回一个结果。
- Rejected(已拒绝):异步操作失败,返回一个错误原因。
Promise 的基本用法
创建一个 Promise
1 | |
使用 .then() 和 .catch() 处理 Promise
.then()用于处理异步操作成功时的结果。.catch()用于处理异步操作失败时的错误。
1 | |
示例:使用 Promise 实现异步操作
1 | |
Promise 链式调用
一个常见的用法是将多个异步操作链式调用。每次 .then() 返回一个新的 Promise,可以继续进行处理。
1 | |
Promise.all() 和 Promise.race()
- **
Promise.all()**:接受一个数组中的多个 Promise,只有所有 Promise 都成功时,才会触发.then()。如果任何一个 Promise 失败,.catch()会被触发。
1 | |
- **
Promise.race()**:接受多个 Promise,并返回第一个完成的 Promise 的结果,无论是成功还是失败。
1 | |
总结
Promise 使得异步操作的代码更加简洁和易于管理,通过 .then()、.catch() 等方法可以更清晰地处理成功或失败的结果,避免了回调地狱(callback hell)问题。