从僵尸游戏学Solidity(笔记)

写在前面

学习地址:https://cryptozombies.io/zh/course

智能协议的永固性
(即以太坊上的 DApp 跟普通的应用程序的区别)
在你把智能协议传上以太坊之后,它就变得不可更改, 这种永固性意味着你的代码永远不能被调整或更新。你编译的程序会一直,永久的,不可更改的,存在以太坊上。这就是 Solidity 代码的安全性如此重要的一个原因。如果你的智能协议有任何漏洞,即使你发现了也无法补救。你只能让你的用户们放弃这个智能协议,然后转移到一个新的修复后的合约上。
但这恰好也是智能合约的一大优势。代码说明一切。如果你去读智能合约的代码,并验证它,你会发现,一旦函数被定义下来,每一次的运行,程序都会严格遵照函数中原有的代码逻辑一丝不苟地执行,完全不用担心函数被人篡改而得到意外的结果。

Gas - 驱动以太坊DApps的能源
在 Solidity 中,你的用户想要每次执行你的 DApp 都需要支付一定的 gas,gas 可以用以太币购买,因此,用户每次跑 DApp 都得花费以太币。

一个 DApp 收取多少 gas 取决于功能逻辑的复杂程度。每个操作背后,都在计算完成这个操作所需要的计算资源,(比如,存储数据就比做个加法运算贵得多), 一次操作所需要花费的 gas 等于这个操作背后的所有运算花销的总和。
省 gas 的招数:结构封装 (Struct packing)
除了基本版的 uint 外,还有其他变种 uint:uint8,uint16,uint32等。
通常情况下我们不会考虑使用 uint 变种,因为无论如何定义 uint的大小,Solidity 为它保留256位的存储空间。例如,使用 uint8 而不是uint(uint256)不会为你节省任何 gas。

除非,把 uint 绑定到 struct 里面。
如果一个 struct 中有多个 uint,则尽可能使用较小的 uint, Solidity 会将这些 uint 打包在一起,从而占用较少的存储空间。

struct NormalStruct {
  uint a;
  uint b;
  uint c;
}
struct MiniMe {
  uint32 a;
  uint32 b;
  uint c;
}
// 因为使用了结构打包,`mini` 比 `normal` 占用的空间更少
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30); 

“view” 函数不花 “gas”
这是因为 view 函数不会真正改变区块链上的任何数据 - 它们只是读取。因此用 view 标记一个函数,意味着告诉 web3.js,运行这个函数只需要查询你的本地以太坊节点,而不需要在区块链上创建一个事务(事务需要运行在每个节点上,因此花费 gas)。
注意:如果一个 view 函数在另一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为 view 的函数只有在外部调用时才是免费的。

使用 SafeMath预防溢出
为了防止这些情况,OpenZeppelin 建立了一个叫做 SafeMath 的 (library),默认情况下可以防止这些问题。
一个_库_ 是 Solidity 中一种特殊的合约。其中一个有用的功能是给原始数据类型增加一些方法。
比如,使用 SafeMath 库的时候,我们将使用 using SafeMath for uint256 这样的语法。 SafeMath 库有四个方法 — add, sub, mul, 以及 div。现在我们可以这样来让 uint256 调用这些方法:

using SafeMath for uint256;

uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10

Sodility

合约

一份合约就是以太应币应用的基本模块,所有的变量和函数都属于一份合约, 它是所有应用的起点.

版本指令:所有的 Solidity 源码都必须冠以 “version pragma” — 标明 Solidity 编译器的版本. 以避免将来新的编译器可能破坏你的代码。(etc. pragma solidity ^0.4.19;)

pragma solidity ^0.4.19;
contract ZombieFactory {
}

OpenZeppelin库的Ownable 合约

/**
 * @title Ownable
 * @dev The Ownable contract has an owner address, and provides basic authorization control
 * functions, this simplifies the implementation of "user permissions".
 */
contract Ownable {
  address public owner;
  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

  /**
   * @dev The Ownable constructor sets the original `owner` of the contract to the sender
   * account.
   */
  function Ownable() public {
    owner = msg.sender;
  }

  /**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }

  /**
   * @dev Allows the current owner to transfer control of the contract to a newOwner.
   * @param newOwner The address to transfer ownership to.
   */
  function transferOwnership(address newOwner) public onlyOwner {
    require(newOwner != address(0));
    OwnershipTransferred(owner, newOwner);
    owner = newOwner;
  }
}

1、构造函数:function Ownable()是一个 _ constructor_ (构造函数),构造函数不是必须的,它与合约同名,构造函数一生中唯一的一次执行,就是在合约最初被创建的时候。
2、函数修饰符:modifier onlyOwner()。 修饰符跟函数很类似,不过是用来修饰其他已有函数用的, 在其他语句执行前,为它检查下先验条件。 在这个例子中,我们就可以写个修饰符 onlyOwner 检查下调用者,确保只有合约的主人才能运行本函数。
3、indexed 关键字:

数据类型

状态变量
状态变量是被永久地保存在合约中。也就是说它们被写入以太币区块链中. 想象成写入一个数据库。

无符号整数
uint 其值不能是负数,对于有符号的整数存在名为 int 的数据类型。注: Solidity中, uint 实际上是 uint256代名词, 一个256位的无符号整数。你也可以定义位数少的uints — uint8, uint16, uint32

uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;

结构体

struct Person {
  uint age;
  string name;
}

数组
Solidity 支持两种数组: 静态 数组和动态 数组:

// 固定长度为2的静态数组:
uint[2] fixedArray;
// 固定长度为5的string类型的静态数组:
string[5] stringArray;
// 动态数组,长度不固定,可以动态添加元素:
uint[] dynamicArray;

也可以建立一个结构体类型的数组

Person[] people; // 这是动态数组,我们可以不断添加元素
// 创建一个新的Person:
Person satoshi = Person(172, "Satoshi");
// 将新创建的satoshi添加进people数组:
people.push(satoshi);
//也可以将两步合并
people.push(Person(16, "Vitalik"));
//array.push() 在数组的尾部加入新元素,所以元素在数组中的顺序就是我们添加的顺序

注:
状态变量被永久保存在区块链中。所以在你的合约中创建动态数组来保存成结构的数据是非常有意义的。

公共数组
定义 ==public ==数组, Solidity 会自动创建 ==getter ==方法. 语法如下:

Zombie[] public zombies;

其它的合约可以从这个数组读取数据(但不能写入数据),所以这在合约中是一个有用的保存公共数据的模式。

映射(Mapping)和地址(Address)
Addresses(地址)
以太坊区块链由 _ account _ (账户)组成,一个帐户的余额是 以太 (在以太坊区块链上使用的币种),你可以和其他帐户之间支付和接受以太币,就像银行帐户可以电汇资金到其他银行帐户一样。
在 Solidity 中,有一些全局变量可以被所有函数调用。 其中一个就是 msg.sender,它指的是当前调用者(或智能合约)的 address。
Mapping(映射)
映射本质上是存储和查找数据所用的键-值对。

mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;

Storage与Memory
在 Solidity 中,有两个地方可以存储变量 —— storage 或 memory。
Storage 变量是指永久存储在区块链中的变量。
Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。 你可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。
默认情况下 Solidity 会自动处理它们。 状态变量(在函数之外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。
然而也有一些情况下,你需要手动声明存储类型,主要用于处理函数内的 _ 结构体 _ 和 _ 数组 _ 时

  Sandwich[] sandwiches;
  function eatSandwich(uint _index) public {
    // Sandwich mySandwich = sandwiches[_index];
    // ^ 看上去很直接,不过 Solidity 将会给出警告
    // 告诉你应该明确在这里定义 `storage` 或者 `memory`。

    // 所以你应该明确定义 `storage`:
    Sandwich storage mySandwich = sandwiches[_index];
    // ...这样 `mySandwich` 是指向 `sandwiches[_index]`的指针
    // 在存储里,另外...
    mySandwich.status = "Eaten!";
    // ...这将永久把 `sandwiches[_index]` 变为区块链上的存储

    // 如果你只想要一个副本,可以使用`memory`:
    Sandwich memory anotherSandwich = sandwiches[_index + 1];
    // ...这样 `anotherSandwich` 就仅仅是一个内存里的副本了
    // 另外
    anotherSandwich.status = "Eaten!";
    // ...将仅仅修改临时变量,对 `sandwiches[_index + 1]` 没有任何影响
    // 不过你可以这样做:
    sandwiches[_index + 1] = anotherSandwich;
    // ...如果你想把副本的改动保存回区块链存储
  }

Keccak256 和 类型转换
Ethereum 内部有一个散列函数keccak256,它用了SHA3版本。一个散列函数基本上就是把一个字符串转换为一个256位的16进制数字。字符串的一个微小变化会引起散列数据极大变化。

uint rand = uint(keccak256(_str));

时间单位
变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。
Solidity 还包含秒(seconds),分钟(minutes),小时(hours),天(days),周(weeks) 和 年(years) 等时间单位。它们都会转换成对应的秒数放入 uint 中。

uint lastUpdated;

// 将‘上次更新时间’ 设置为 ‘现在’
function updateTimestamp() public {
  lastUpdated = now;
}

// 如果到上次`updateTimestamp` 超过5分钟,返回 'true'
// 不到5分钟返回 'false'
function fiveMinutesHavePassed() public view returns (bool) {
  return (now >= (lastUpdated + 5 minutes));
}

函数

在 Solidity 中函数定义的句法如下:

function createZombie(string _name, uint _dna) {
 }

注: 习惯上函数里的变量都是以(_)开头 (但不是硬性规定) 以区别全局变量。

require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行。在调用一个函数之前,用 require 验证前置条件是非常有必要的。

require(ownerZombieCount[msg.sender] == 0);

公有/私有函数
Solidity 定义的函数的属性默认为公共。 这就意味着任何一方 (或其它合约) 都可以调用你合约里的函数。
将自己的函数定义为私有是一个好的编程习惯,只有当你需要外部世界调用它时才将它设置为公共。

function _createZombie(string _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }

这意味着只有我们合约中的其它函数才能够调用这个函数,和函数的参数类似,私有函数的名字用(_)起始。

函数的修饰符
1、可见性修饰符::决定函数何时和被谁调用:private 意味着它只能被合约内部调用; internal 就像 private 但是也能被继承的合约调用; external 只能从合约外部调用;最后 public 可以在任何地方调用,不管是内部还是外部。
2、状态修饰符:决定函数如何和区块链交互: view 告诉我们运行这个函数不会更改和保存任何数据; pure 告诉我们这个函数不但不会往区块链写数据,它甚至不从区块链读取数据。这两种在被从合约外部调用的时候都不花费任何gas(但是它们在被内部其他函数调用的时候将会耗费gas)。

//view
function sayHello() public view returns (string) 
//pure
function _multiply(uint a, uint b) private pure returns (uint) {
  return a * b;
}

3、自定义的 modifier,我们可以自定义其对函数的约束逻辑。

// 存储用户年龄的映射
mapping (uint => uint) public age;

// 限定用户年龄的修饰符
modifier olderThan(uint _age, uint _userId) {
  require(age[_userId] >= _age);
  _;
}

// 必须年满16周岁才允许开车 (至少在美国是这样的).
// 我们可以用如下参数调用`olderThan` 修饰符:
function driveCar(uint _userId) public olderThan(16, _userId) {
  // 其余的程序逻辑
}

4、payable 修饰符
payable方法是让 Solidity 和以太坊变得如此酷的一部分 —— 它们是一种可以接收以太的特殊函数。

contract OnlineStore {
  function buySomething() external payable {
    // 检查以确定0.001以太发送出去来运行函数:
    require(msg.value == 0.001 ether);
    // 如果为真,一些用来向函数调用者发送数字内容的逻辑
    transferThing(msg.sender);
  }
}

在这里,msg.value 是一种可以查看向合约发送了多少以太的方法,另外 ether 是一个內建单元。

这里发生的事是,一些人会从 web3.js 调用这个函数 (从DApp的前端), 像这样 :

// 假设 `OnlineStore` 在以太坊上指向你的合约:
OnlineStore.buySomething().send(from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001))

提现

contract GetPaid is Ownable {
  function withdraw() external onlyOwner {
    owner.transfer(this.balance);
  }
}

通过 transfer 函数向一个地址发送以太, 然后 this.balance 将返回当前合约存储了多少以太。

返回值
单个返回值

function _generateRandomDna(string _str) private returns (uint) {
        // 这里开始
 }

多个返回值

function multipleReturns() internal returns(uint a, uint b, uint c) {
  return (1, 2, 3);
}

function processMultipleReturns() external {
  uint a;
  uint b;
  uint c;
  // 这样来做批量赋值:
  (a, b, c) = multipleReturns();
}

// 或者如果我们只想返回其中一个变量:
function getLastReturnValue() external {
  uint c;
  // 可以对其他字段留空:
  (,,c) = multipleReturns();
}

继承(Inheritance)和引入(Import)

import "./zombiefactory.sol";
contract ZombieFeeding is ZombieFactory {
}

可以实现多继承

contract ZombieOwnership is ZombieAttack, ERC721 {
}

For 循环

function getEvens() pure external returns(uint[]) {
  uint[] memory evens = new uint[](5);
  // 在新数组中记录序列号
  uint counter = 0;
  // 在循环从1迭代到10:
  for (uint i = 1; i <= 10; i++) {
    // 如果 `i` 是偶数...
    if (i % 2 == 0) {
      // 把它加入偶数数组
      evens[counter] = i;
      //索引加一, 指向下一个空的‘even’
      counter++;
    }
  }
  return evens;
}

事件

事件是合约和区块链通讯的一种机制。前端应用“监听”某些事件,并做出反应。

与其他合约的交互

如果我们的合约需要和区块链上的其他的合约会话,则需先定义一个 interface (接口)。

contract KittyInterface {
  function getKitty(uint256 _id) external view returns (
    bool isGestating,
    bool isReady,
    uint256 cooldownIndex,
    uint256 nextActionAt,
    uint256 siringWithId,
    uint256 birthTime,
    uint256 matronId,
    uint256 sireId,
    uint256 generation,
    uint256 genes
  );
}

这个过程虽然看起来像在定义一个合约,但其实内里不同:
首先,我们只声明了要与之交互的函数 ,在其中我们没有使用到任何其他的函数或状态变量。
其次,我们并没有使用大括号({ 和 })定义函数体,我们单单用分号(;)结束了函数声明。这使它看起来像一个合约框架。
编译器就是靠这些特征认出它是一个接口的。
在我们的 app 代码中使用这个接口,合约就知道其他合约的函数是怎样的,应该如何调用,以及可期待什么类型的返回值。
只要将合约的可见性设置为public(公共)或external(外部),它们就可以与以太坊区块链上的任何其他合约进行交互。

以太坊上的代币

一个 代币 在以太坊基本上就是一个遵循一些共同规则的智能合约——即它实现了所有其他代币合约共享的一组标准函数,例如 transfer(address _to, uint256 _value) 和 balanceOf(address _owner).
在智能合约内部,通常有一个映射, mapping(address => uint256) balances,用于追踪每个地址还有多少余额。所以基本上一个代币只是一个追踪谁拥有多少该代币的合约,和一些可以让那些用户将他们的代币转移到其他地址的函数。
代币标准:ERC20、ERC721(加密收藏品)
ERC271 代币是不能互换的,因为每个代币都被认为是唯一且不可分割的。 你只能以整个单位交易它们,并且每个单位都有唯一的 ID。

contract ERC721 {
  event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
  event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
  
  function balanceOf(address _owner) public view returns (uint256 _balance);
  function ownerOf(uint256 _tokenId) public view returns (address _owner);
  function transfer(address _to, uint256 _tokenId) public;
  function approve(address _to, uint256 _tokenId) public;
  function takeOwnership(uint256 _tokenId) public;
}

ERC721: 转移标准
ERC721 规范有两种不同的方法来转移代币:

function transfer(address _to, uint256 _tokenId) public;

function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;

1、第一种方法是代币的拥有者调用transfer 方法,传入他想转移到的 address 和他想转移的代币的 _tokenId。
2、第二种方法是代币拥有者首先调用 approve,然后传入与以上相同的参数。接着,该合约会存储谁被允许提取代币,通常存储到一个 mapping (uint256 => address) 里。然后,当有人调用 takeOwnership 时,合约会检查 msg.sender 是否得到拥有者的批准来提取代币,如果是,则将代币转移给他。

function _transfer(address _from, address _to, uint256 _tokenId) private {
    ownerZombieCount[_to]++;
    ownerZombieCount[_from]--;
    zombieToOwner[_tokenId] = _to;
    Transfer(_from, _to, _tokenId);
  }

  function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
    _transfer(msg.sender, _to, _tokenId);
  }

  function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
    zombieApprovals[_tokenId] = _to;
    Approval(msg.sender, _to, _tokenId);
  }

  function takeOwnership(uint256 _tokenId) public {
    require(zombieApprovals[_tokenId] == msg.sender);
    address owner = ownerOf(_tokenId);
    _transfer(owner, msg.sender, _tokenId);
  }

应用前端和 Web3.js

什么是 Web3.js?
以太坊网络是由节点组成的,每一个节点都包含了区块链的一份拷贝。当你想要调用一份智能合约的一个方法,你需要从其中一个节点中查找并告诉它:智能合约的地址、你想调用的方法,以及你想传入那个方法的参数。以太坊节点只能识别一种叫做 JSON-RPC 的语言。这种语言直接读起来并不好懂。
幸运的是 Web3.js 把这些令人讨厌的查询语句都隐藏起来了, 所以你只需要与方便易懂的 JavaScript 界面进行交互即可。

CryptoZombies.methods.createRandomZombie("Vitalik Nakamoto ")
  .send({ from: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", gas: "3000000" })

添加Web3.js工具
可以从 github 直接下载压缩后的 .js 文件 然后包含到项目文件中


Web3 Provider
以太坊是由共享同一份数据的相同拷贝的 节点 构成的。 在 Web3.js 里设置 Web3 的 Provider(提供者) 告诉我们的代码应该和 哪个节点 交互来处理我们的读写。这就好像在传统的 Web 应用程序中为你的 API 调用设置远程 Web 服务器的网址。

Infura
Infura 是一个服务,它维护了很多以太坊节点并提供了一个缓存层来实现高速读取。你可以用他们的 API 来免费访问这个服务。 用 Infura 作为节点提供者,你可以不用自己运营节点就能很可靠地向以太坊发送、接收信息。
你可以通过这样把 Infura 作为你的 Web3 节点提供者:

var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));

Metamask
Metamask 是 Chrome 和 Firefox 的浏览器扩展, 它能让用户安全地维护他们的以太坊账户和私钥, 并用他们的账户和使用 Web3.js 的网站互动。Metamask 默认使用 Infura 的服务器做为 web3 提供者。
使用 Metamask 的 web3 提供者

window.addEventListener('load', function() {

  // 检查web3是否已经注入到(Mist/MetaMask)
  if (typeof web3 !== 'undefined') {
    // 使用 Mist/MetaMask 的提供者
    web3js = new Web3(web3.currentProvider);
  } else {
    // 处理用户没安装的情况, 比如显示一个消息
    // 告诉他们要安装 MetaMask 来使用我们的应用
  }

  // 现在你可以启动你的应用并自由访问 Web3.js:
  startApp()

})

与合约对话
Web3.js 需要两个东西来和你的合约对话: 它的 地址 和它的 ABI
地址:在你部署智能合约以后,它将获得一个以太坊上的永久地址。
ABI:当你编译你的合约向以太坊部署时(我们将在第七课详述), Solidity 编译器会给你 ABI。将编译了的ABI 并放在名为cryptozombies_abi.js文件中,保存在一个名为 cryptoZombiesABI 的变量中。将cryptozombies_abi.js 包含进我们的项目,我们就能通过那个变量访问 CryptoZombies ABI 。
实例化 Web3.js

// 实例化 myContract
var myContract = new web3js.eth.Contract(myABI, myContractAddress);

调用和合约函数
Web3.js 有两个方法来调用我们合约的函数: call and send.
Call
call 用来调用 view 和 pure 函数。它只运行在本地节点,不会在区块链上创建事务。(view 和 pure 函数是只读的并不会改变区块链的状态。它们也不会消耗任何gas。用户也不会被要求用MetaMask对事务签名。)

function getZombieDetails(id) {
  return cryptoZombies.methods.zombies(id).call()
}

// 调用函数并做一些其他事情
getZombieDetails(15)
.then(function(result) {
  console.log("Zombie 15: " + JSON.stringify(result));
});

cryptoZombies.methods.zombies(id).call() 将和 Web3 提供者节点通信,告诉它返回从我们的合约中的 Zombie[] public zombies,id为传入参数的僵尸信息。
注意这是 异步的,就像从外部服务器中调用API。所以 Web3 在这里返回了一个 Promises. (如果你对 JavaScript的 Promises 不了解,最好先去学习一下这方面知识再继续)。
一旦那个 promise 被 resolve, (意味着我们从 Web3 提供者那里获得了响应),我们的例子代码将执行 then 语句中的代码,在控制台打出 result。

获得 MetaMask中的用户账户
MetaMask 允许用户在扩展中管理多个账户。
我们可以通过这样来获取 web3 变量中激活的当前账户:

var userAccount = web3.eth.accounts[0]

因为用户可以随时在 MetaMask 中切换账户,我们的应用需要监控这个变量,一旦改变就要相应更新界面。例如,若用户的首页展示它们的僵尸大军,当他们在 MetaMask 中切换了账号,我们就需要更新页面来展示新选择的账户的僵尸大军。

我们可以通过 setInterval 方法来做:

var accountInterval = setInterval(function() {
          // Check if account has changed
          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];
            // Call a function to update the UI with the new account
            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);

这段代码做的是,每100毫秒检查一次 userAccount 是否还等于 web3.eth.accounts[0] (比如:用户是否还激活了那个账户)。若不等,则将 当前激活用户赋值给 userAccount,然后调用一个函数来更新界面。

Send
send 将创建一个事务并改变区块链上的数据。你需要用 send 来调用任何非 view 或者 pure 的函数。(send 一个事务将要求用户支付gas,并会要求弹出对话框请求用户使用 Metamask 对事务签名。在我们使用 Metamask 作为我们的 web3 提供者的时候,所有这一切都会在我们调用 send() 的时候自动发生。而我们自己无需在代码中操心这一切。)

相对 call 函数,send 函数有如下主要区别:
1、send 一个事务需要一个 from 地址来表明谁在调用这个函数(也就是你 Solidity 代码里的 msg.sender )。 我们需要这是我们 DApp 的用户,这样一来 MetaMask 才会弹出提示让他们对事务签名。
2、send 一个事务将花费 gas
3、在用户 send 一个事务到该事务对区块链产生实际影响之间有一个不可忽略的延迟。这是因为我们必须等待事务被包含进一个区块里,以太坊上一个区块的时间平均下来是15秒左右。如果当前在以太坊上有大量挂起事务或者用户发送了过低的 gas 价格,我们的事务可能需要等待数个区块才能被包含进去,往往可能花费数分钟。

所以在我们的代码中我们需要编写逻辑来处理这部分异步特性。

function createRandomZombie(name) {
  // 这将需要一段时间,所以在界面中告诉用户这一点
  // 事务被发送出去了
  $("#txStatus").text("正在区块链上创建僵尸,这将需要一会儿...");
  // 把事务发送到我们的合约:
  return cryptoZombies.methods.createRandomZombie(name)
  .send({ from: userAccount })
  .on("receipt", function(receipt) {
    $("#txStatus").text("成功生成了 " + name + "!");
    // 事务被区块链接受了,重新渲染界面
    getZombiesByOwner(userAccount).then(displayZombies);
  })
  .on("error", function(error) {
    // 告诉用户合约失败了
    $("#txStatus").text(error);
  });
}

== Web3.js 中需要特殊对待的函数 — payable 函数。==

function levelUp(zombieId) {
        $("#txStatus").text("Leveling up your zombie...");
        return cryptoZombies.methods.levelUp(zombieId)
        .send({ from: userAccount, value: web3.utils.toWei("0.001", "ether") })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Power overwhelming! Zombie successfully leveled up");
        })
        .on("error", function(error) {
          $("#txStatus").text(error);
        });
      }

订阅合约事件
在 Web3.js里, 你可以订阅 一个事件,这样你的 Web3 提供者可以在每次事件发生后触发你的一些代码逻辑:

cryptoZombies.events.NewZombie()
.on("data", function(event) {
  let zombie = event.returnValues;
  console.log("一个新僵尸诞生了!", zombie.zombieId, zombie.name, zombie.dna);
}).on('error', console.error);

注意这段代码将在 任何 僵尸生成的时候激发一个警告信息——而不仅仅是当前用用户的僵尸。如果我们只想对当前用户发出提醒呢?
使用indexed
为了筛选仅和当前用户相关的事件,我们的 Solidity 合约将必须使用 indexed 关键字,就像我们在 ERC721 实现中的Transfer 事件中那样:

event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);

在这种情况下, 因为_from 和 _to 都是 indexed,这就意味着我们可以在前端事件监听中过滤事件

cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
  let data = event.returnValues;
  // 当前用户更新了一个僵尸!更新界面来显示
}).on('error', console.error);

查询过去的事件
我们甚至可以用 getPastEvents 查询过去的事件,并用过滤器 fromBlock 和 toBlock 给 Solidity 一个事件日志的时间范围(“block” 在这里代表以太坊区块编号):

cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: 'latest' })
.then(function(events) {
  // events 是可以用来遍历的 `event` 对象 
  // 这段代码将返回给我们从开始以来创建的僵尸列表
});

你可能感兴趣的:(从僵尸游戏学Solidity(笔记))