solidity基础(已完结)

update 3.10 今天在https://cryptozombies.io/学了两个课程,挺有意思。有其他语言的基础入门还是挺简单的。 记一下知识点。

  • Solidity 的代码都包裹在合约里面. 一份合约就是以太币应用的基本模块, 所有的变量和函数都属于一份合约, 它是你所有应用的起点。
  • 所有的 Solidity 源码都必须冠以 "version pragma" — 标明 Solidity 编译器的版本,以避免将来新的编译器可能破坏你的代码。例如: pragma solidity ^0.4.19;
  • uint 无符号数据类型, 指其值不能是负数。Solidity中, uint 实际上是 uint256代名词,一个256位的无符号整数。
  • Solidity 支持两种数组: 静态数组和动态数组。状态变量被永久保存在区块链中,所以在合约中创建动态数组来保存成结构的数据是非常有意义的。
// 固定长度为2的静态数组:
uint[2] fixedArray;
// 固定长度为5的string类型的静态数组:
string[5] stringArray;
// 动态数组,长度不固定,可以动态添加元素:
uint[] dynamicArray;
  • 其它的合约可以从公共数组读取数据(但不能写入数据)。
  • 习惯上函数里的变量都是以(_)开头以区别全局变量 (但不是硬性规定)。和函数的参数类似,私有函数的名字用(_)起始。
  • publicprivateinternalexternal等关键字是放在变量后面的,internal 函数只能在当前合约及其派生合约中调用,external函数只能被外部合约调用。
  • view函数只读取合约状态,不修改任何状态变量;pure函数不读取合约状态,也不修改任何状态变量。
  • keccak256散列函数把一个字符串转换为一个256位的16进制数字,字符串的一个微小变化会引起散列数据极大变化。
  • 事件是合约和区块链通讯的一种机制。你的前端应用“监听”某些事件,并做出反应。
// 这里建立事件
event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public {
  uint result = _x + _y;
  //触发事件,通知app
  IntegersAdded(_x, _y, result);
  return result;
}
YourContract.IntegersAdded(function(error, result) {
  // 干些事
})

  • 以太坊区块链由 account (账户)组成,你可以把它想象成银行账户。每个帐户都有一个“地址”,你可以把它想象成银行账号,这是账户唯一的标识符。一个帐户的余额是 Ether(在以太坊区块链上使用的币种),你可以和其他帐户之间支付和接受以太币。
  • 映射本质上是存储和查找数据所用的键-值对。
//对于金融应用程序,将用户的余额保存在一个 uint类型的变量中:
mapping (address => uint) public accountBalance;
//或者可以用来通过userId 存储/查找的用户名
mapping (uint => string) userIdToName;
  • 在 Solidity 中,有一些全局变量可以被所有函数调用。 其中一个就是 msg.sender,它指的是当前调用者(或智能合约)的 address。在 Solidity 中,功能执行始终需要从外部调用者开始。 一个合约只会在区块链上什么也不做,除非有人调用其中的函数。所以 msg.sender总是存在的。使用 msg.sender 很安全,因为它具有以太坊区块链的安全保障。
  • require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行。因此,在调用一个函数之前,用 require 验证前置条件是非常有必要的。
function sayHiToVitalik(string _name) public returns (string) {
  // 比较 _name 是否等于 "Vitalik". 如果不成立,抛出异常并终止程序
  // (敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较
  // 两字符串的 keccak256 哈希值来进行判断)
  require(keccak256(_name) == keccak256("Vitalik"));
  // 如果返回 true, 运行如下语句
  return "Hi!";
}
  • 当代码过于冗长的时候,最好将代码和逻辑分拆到多个不同的合约中,以便于管理。有个让 Solidity 的代码易于管理的功能,就是合约inheritance (继承)。
  • memory 变量存储在内存中,仅在函数执行期间存在;storage 变量存储在合约的存储空间中,永久存在。状态变量(在函数之外声明的变量)默认为“storage”形式,并永久写入区块链;而在函数内部声明的变量是“memory”型的,它们在函数调用结束后消失。
  • 如果我们的合约需要和区块链上的其他的合约会话,则需先定义一个 interface (接口),在接口中只声明了要与之交互的函数。只要将您合约的可见性设置为publicexternal,它们就可以与以太坊区块链上的任何其他合约进行交互。
contract NumberInterface {
  function getNum(address _myAddress) public view returns (uint);
}
contract MyContract {
  address NumberInterfaceAddress = 0xab38...;
  // ^ 这是FavoriteNumber合约在以太坊上的地址
  NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
  // 现在变量 `numberContract` 指向另一个合约对象

  function someFunction() public {
    // 现在我们可以调用在那个合约中声明的 `getNum`函数:
    uint num = numberContract.getNum(msg.sender);
    // ...在这儿使用 `num`变量做些什么
  }
}

---------------------------

update 3.13 这几天学了第三课,记一下知识点。

  • 把智能协议传上以太坊之后,它就变得不可更改, 这种永固性意味着代码永远不能被调整或更新。因此,我们不能硬编码,而要采用“函数”,以便于 DApp 的关键部分可以以参数形式修改。
  • 我们可以指定合约的“所有权” - 就是说,给它指定一个主人,只有主人对它享有特权。通常用Ownable 合约实现:合约创建,构造函数先行,将其 owner 设置为msg.sender(其部署者);为它加上一个修饰符 onlyOwner,它会限制陌生人的访问,将访问某些函数的权限锁定在 owner 上;允许将合约所有权转让给他人。
/**
 * @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;
  }
}
  • 关键字modifier告诉编译器,这是个modifier(修饰符),而不是个function(函数)。它不能像函数那样被直接调用,只能被添加到函数定义的末尾,用以改变函数的行为。尽管函数修饰符也可以应用到各种场合,但最常见的还是放在函数执行之前添加快速的 require检查。
  • 一个 DApp 收取多少 gas 取决于功能逻辑的复杂程度。每个操作背后,都在计算完成这个操作所需要的计算资源,一次操作所需要花费的 gas 等于这个操作背后的所有运算花销的总和。
  • 由于“去中心化” ,数以千计的节点同时在验证着每个功能的运行,这可以确保它的数据不会被被监控,或者被刻意修改。可能会有用户用无限循环堵塞网络,抑或用密集运算来占用大量的网络资源,为了防止这种事情的发生,以太坊的创建者为以太坊上的资源制定了价格,想要在以太坊上运算或者存储,你需要先付费。
  • uint 定义在一个 struct 中的时候,尽量使用最小的整数子类型以节约空间。 并且把同样类型的变量放一起(即在 struct 中将把变量按照类型依次放置),这样 Solidity 可以将存储空间最小化。
  • Unix时间传统用一个32位的整数进行存储。这会导致“2038年”问题,当这个32位的unix时间戳不够用,产生溢出,使用这个时间的遗留系统就麻烦了。所以,如果我们想让我们的 DApp 跑够20年,我们可以使用64位整数表示时间,但为此我们的用户又得支付更多的 gas。
  • Solidity 使用自己的本地时间单位,变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数),返回类型 uint256。使用 uint32(...) 进行强制类型转换。
  • Solidity 还包含秒(seconds)分钟(minutes)小时(hours)天(days)周(weeks)年(years) 等时间单位,它们都会转换成对应的秒数放入 uint 中。表示一天要用1 days复数形式, 否则通不过编译器。
  • 结构体的存储指针可以以参数的方式传递给一个 privateinternal 的函数,因此结构体可以在多个函数之间相互传递。
function _doStuff(Zombie storage _zombie) internal {
  // do stuff with _zombie
}
  • 必须仔细地检查所有声明为 publicexternal的函数,一个个排除用户滥用它们的可能,谨防安全漏洞。请记住,如果这些函数没有类似 onlyOwner 这样的函数修饰符,用户能利用各种可能的参数去调用它们。
  • 函数修饰符也可以带参数。修饰符的最后一行为 _;,表示修饰符调用结束后返回,并执行调用函数余下的部分。
// 存储用户年龄的映射
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) {
  // 其余的程序逻辑
}
  • 从外部调用一个view函数不需要支付 gas,因为 view 函数不会真正改变区块链上的任何数据,只是读取。用 view 标记一个函数,意味着告诉 web3.js,运行这个函数只需要查询你的本地以太坊节点,而不需要在区块链上创建交易(这需要运行在每个节点上,因此花费 gas)。在只读的函数上标记上表示“只读”的“external view 声明,就能减少用户在 DApp 中 gas 用量。
  • 如果从同一合约中的另一个函数(不是 view 函数)内部调用 view 函数,它仍然会消耗gas。这是因为另一个函数在以太坊上创建交易,并且仍然需要从每个节点进行验证。因此 view 函数只有在外部调用时才是免费的。
  • Solidity 使用storage是相当昂贵的,”写入“操作尤其贵。因为无论是写入还是更改一段数据, 这都将永久性地写入区块链。为了降低成本,不到万不得已,避免将数据写入存储。这也会导致效率低下的编程逻辑 - 比如每次调用一个函数,都需要在 memory中重建一个数组,而不是简单地将上次计算的数组给存储下来以便快速查找。在大多数编程语言中,遍历大数据集合都是昂贵的。但是在 Solidity 中,使用一个标记了external view的函数,遍历比 storage 要便宜太多,因为 view 函数不会产生任何花销。

---------------------------

update 3.17 补充了一下前面的笔记,开始第四课。

update 4.20 停更了一个月真是抱歉...回来了!今天一口气把第四章学完了,大部分是游戏设计逻辑,只记一下Solidity的知识点。

  • 修饰符都可以在函数定义上堆叠在一起,如下所示:
function test() external view onlyOwner anotherModifier { /* ... */ }
  • payable 修饰的函数是是一种可以接收以太币的特殊函数。在以太坊中,因为钱(以太币)、数据(交易有效负载)和合约代码本身都存在于以太坊上,所以你可以在调用函数的同时向合约付款。msg.value 是一种查看有多少以太币被发送到合约的方法, ether 是一个内置单位。
contract OnlineStore {
  function buySomething() external payable {
    // Check to make sure 0.001 ether was sent to the function call:
    require(msg.value == 0.001 ether);
    // If so, some logic to transfer the digital item to the caller of the function:
    transferThing(msg.sender);
  }
}
  • 当你将以太币发送到合约后,它会被存储在合约的以太坊账户中,并且会被困在那里——除非你添加一个函数来从合约中提取以太币。
  • 除非地址的类型为 address payable ,否则您无法将 Ether 传输到该地址。但是 _owner 变量的类型为 uint160 ,这意味着我们必须将其显式转换为 address payable 。可以使用 transfer 函数将以太币转移到该地址,并且 address(this).balance 将返回合约中存储的总余额。
contract GetPaid is Ownable {
  function withdraw() external onlyOwner {
    address payable _owner = address(uint160(owner()));
    _owner.transfer(address(this).balance);
  }
}
  • Solidity 中最好的随机数生成器是 keccak256 哈希函数,下面的代码可以生成0 到 99 之间的随机数。
// Generate a random number between 1 and 100:
uint randNonce = 0;
uint random = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % 100;
randNonce++;
uint random2 = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % 100;
  • 我们很难在以太坊中安全地生成随机数,但可以使用预言机(一种从以太坊外部提取数据的安全方法)从区块链外部生成安全随机数。
  • 我们可以把多次使用的相同逻辑移至自己的 modifier 中,重构代码并避免重复。
  • if - else语句:
if (zombieCoins[msg.sender] > 100000000) {
  // You rich!!!
} else {
  // We require more ZombieCoins...
}

---------------------------

update 4.20 第五课开始ERC721 & NFT的内容,期待!

update 4.23 这课笔记写得真累,不过solidity基础算是告一段落啦,后面还有很长的路要走~

  • 以太坊上的代币(token)基本上就是一个遵循一些共同规则的智能合约——即它实现了所有其他代币合约共享的一组标准函数,例如transferFrom(address _from, address _to, uint256 _amount) 和 balanceOf(address _owner)。在智能合约内部,通常有一个映射,mapping(address => uint256) balances,用于追踪每个地址还有多少余额。所以基本上一个代币只是一个追踪谁拥有多少该代币的合约,和一些可以让那些用户将他们的代币转移到其他地址的函数。
  • 由于所有 ERC20 代币共享具有相同名称的同一组函数,因此它们都可以以相同的方式进行交互。这意味着如果您构建的应用程序能够与一个 ERC20 代币交互,那么它也能够与任何 ERC20 代币交互。只要简单地插入新的代币合约地址,你的应用程序就有另一个它可以使用的代币了。
  • 和 ERC20 代币不同,ERC721 代币不能互换的,每个代币都被认为是唯一不可分割的。 你只能以整个单位交易它们,并且每个单位都有唯一的 ID。 这种代币标准更适合 CryptoZombies 等加密收藏品。
contract ERC721 {
  event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
  event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

  function balanceOf(address _owner) external view returns (uint256);
  function ownerOf(uint256 _tokenId) external view returns (address);
  function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
  function approve(address _approved, uint256 _tokenId) external payable;
}
  • 在Solidity中,你的合约可以继承多个合约。当使用多重继承的时候,你只需要用逗号来隔开几个你想要继承的合约。
contract SatoshiNakamoto is NickSzabo, HalFinney {
  // Omg, the secrets of the universe revealed!
}
  • 当修饰符ownerOf和函数ownerOf名称相同时,编译器会报错。ERC721 代币标准意味着其他合约将期望我们的合约以这些确切的名称来定义函数,因此我们不能修改函数名称,而是必须将修饰符的名称换成别的比如onlyOwnerOf
  • ERC721 规范有两种不同的代币转移方式。第一种方式是代币所有者调用transferFrom,使用ta的地址作为_from参数,想要转移到的地址作为_to参数,以及ta要转移的代币的_tokenId;第二种方式是代币所有者首先调用 approve传入ta想要转移到的地址和_tokenId,合约会存储谁被允许提取代币,通常存储到一个mapping (uint256 => address) 里,然后当有人调用transferFrom时,合约会检查msg.sender是否是代币所有者或经所有者的批准来提取代币,如果是,则将代币转移给ta。
  • 这两种方法都包含相同的转移逻辑,一种情况是代币的发送者调用函数;另一种情况是代币的接收者调用它。将这个逻辑抽象为它自己的私有函数_transfer是有意义的,然后由 transferFrom调用它。
function _transfer(address _from, address _to, uint256 _tokenId) private {
    // 修改两个地址的balance
    ownerZombieCount[_to]++;
    ownerZombieCount[_from]--;
    // 修改代币owner
    zombieToOwner[_tokenId] = _to;
    // 触发转移代币事件
    emit Transfer(_from, _to, _tokenId);
}

function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
    require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender);
    _transfer(_from, _to, _tokenId);
}

function approve(address _approved, uint256 _tokenId) external payable onlyOwnerOf(_tokenId) {
    zombieApprovals[_tokenId] = _approved;
    emit Approval(msg.sender, _approved, _tokenId);
}
  • 把代币转移给0 地址被称作 “烧币”, 基本上就是把代币转移到一个谁也没有私钥的地址,让这个代币永远也无法恢复。
  • 为了防止溢出(overflow)和下溢(underflow),比如一个等于255的 uint8加上1将变成0,一个等于0的uint8减去1将变成255,OpenZeppelin 建立了一个叫做 SafeMath 的(library)。库是 Solidity 中一种特殊类型的合约,它的用处之一是给原始数据类型增加一些方法。
pragma solidity >=0.5.0 <0.6.0;

/**
 * @title SafeMath
 * @dev Math operations with safety checks that throw on error
 */
library SafeMath {

  /**
  * @dev Multiplies two numbers, throws on overflow.
  */
  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
  }

  /**
  * @dev Integer division of two numbers, truncating the quotient.
  */
  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    // assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold
    return c;
  }

  /**
  * @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
  */
  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  /**
  * @dev Adds two numbers, throws on overflow.
  */
  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}
  • 使用 SafeMath 库的时候,我们将使用 using SafeMath for uint256 这样的语法。注意muladd其实都需要两个参数,在我们声明using SafeMath for uint后,用来调用这些方法的 uint 就自动被作为第一个参数传递进去了(在此例中就是 test)。
using SafeMath for uint;

uint test = 2;
test = test.mul(3); 
test = test.add(5); 
  • assertrequire相似,若结果为否它就会抛出错误。assertrequire区别在于,require若失败则会返还给用户剩下的 gas,assert则不会。所以大部分情况下,写代码的时候会比较喜欢requireassert只在代码可能出现严重错误的时候使用,比如uint 溢出。
  • 如果我们在uint8上调用add,它将会被转换成uint256,这意味着我们需要再实现两个库来防止uint16uint32溢出或下溢。我们可以将其命名为 SafeMath16SafeMath32,代码将和 SafeMath 完全相同,除了所有的 uint256 实例都将被替换成 uint32uint16
  • Solidity 里的单行注释和多行注释如下,最好为你合约中每个方法添加注释来解释它的预期行为。
// This is a single-line comment. It's kind of like a note to self (or to others)

contract CryptoZombies {
  /* This is a multi-lined comment. I'd like to thank all of you
    who have taken your time to try this programming course.
    I know it's free to all of you, and it will stay free
    forever, but we still put our heart and soul into making
    this as good as it can be.

    Know that this is still the beginning of Blockchain development.
    We've come very far but there are so many ways to make this
    community better. If we made a mistake somewhere, you can
    help us out and open a pull request here:
    https://github.com/loomnetwork/cryptozombie-lessons

    Or if you have some ideas, comments, or just want to say
    hi - drop by our Telegram community at https://t.me/loomnetworkdev
  */
}
  • Solidity 社区所使用的一个标准是一种被称作 natspec 的格式。@title@author很直接,@notice用户解释这个方法或者合约是做什么的,@dev是向开发者解释更多的细节。@param@return用来描述这个方法需要传入什么参数以及返回什么值。注意并不需要每次都用上所有的标签,它们都是可选的。不过最少写下一个@dev注释来解释每个方法是做什么的。
/// @title A contract for basic math operations
/// @author H4XF13LD MORRIS 💯💯😎💯💯
/// @notice For now, this contract just adds a multiply function
contract Math {
  /// @notice Multiplies 2 numbers together
  /// @param x the first uint.
  /// @param y the second uint.
  /// @return z the product of (x * y)
  /// @dev This function does not currently check for overflows
  function multiply(uint x, uint y) returns (uint z) {
    // This is just a normal comment, and won't get picked up by natspec
    z = x * y;
  }
}

--------------------

update 4.24 第六课开始学前端和web3.js

update 4.25 意外挺头疼的...这章逻辑有点混乱,整理笔记就花了挺久,部分前端代码就不放上来了

  • 以太坊网络是由节点组成的,每一个节点都包含了区块链的一份拷贝。当你想要调用一份智能合约的一个方法,你需要从其中一个节点中查找并告诉它:1. 智能合约的地址 2. 你想调用的方法 3. 你想传入那个方法的参数。以太坊节点只能识别一种叫做 JSON-RPC 的语言,幸运的是 Web3.js 把这些难懂的查询语句都隐藏起来了,你只需要与方便易懂的 JavaScript 界面进行交互即可。你可以用一些常用工具把 Web3.js 添加进来,甚至可以从 github 直接下载压缩后的 .js 文件然后包含到你的项目文件中。
// 用 NPM
npm install web3

// 用 Yarn
yarn add web3

// 用 Bower
bower install web3

// ...或者其他
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
  • 在 Web3.js 里设置 Web3 Provider 告诉我们的代码应该和哪个节点交互来处理我们的读写。Infura 维护了很多以太坊节点并提供了一个缓存层来实现高速读取,你可以用它们的 API 来免费访问这个服务,无需设置和维护自己的节点。
var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
  • 用户不单会从区块链读取信息,还会向区块链写入信息,因此我们需要一个方法让用户能够用自己的私钥给交易签名。以太坊使用一个公钥/私钥对来给交易做数字签名,如果我更改区块链上的某些数据,我可以通过我的公钥证明我是签署它的人 - 但由于没有人知道我的私钥,所以没有人可以伪造我的交易。
  • Metamask 是 Chrome 和 Firefox 的浏览器扩展, 能让用户安全地管理其以太坊账户和私钥, 并用这些账户和使用 Web3.js 的网站互动。Metamask 默认使用 Infura 的服务器做为 web3 provider,但它也为用户提供了选择自己的 web3 provider 的选项。Metamask将其web3 provider注入到浏览器的全局JavaScript对象web3中,因此你的应用可以检查 web3 是否存在,以及是否使用 web3.currentProvider 作为其提供者。
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 为应用二进制接口 Application Binary Interface,它是 JSON 格式的合约方法的表示,告诉 Web3.js 如何以合约理解的方式格式化函数调用。写完智能合约后,你需要编译它并把它部署到以太坊,它将获得一个以太坊上的永久地址,Solidity 编译器将为您提供 ABI。您需要复制并保存合约地址和 ABI 以在 Web3 中实例化它:
// 实例化 myContract
var myContract = new web3js.eth.Contract(myABI, myContractAddress);
  • Web3.js 有两种方法用于调用合约上的函数:callsendcall 用于 viewpure 函数,它只运行在本地节点上,不会在区块链上创建交易;send将创建交易并更改区块链上的数据,对于 view 和 pure之外的任何函数,您需要使用sendsend交易将要求用户支付gas,并且会弹出Metamask以提示签署交易。当我们使用 Metamask 作为 web3 provider 时,这一切都会在我们调用 send() 时自动发生,不需要我们在代码中执行任何特殊操作。
  • 我们在前几课创建了僵尸数组Zombie[] public zombies;在 Solidity 中,当您声明变量 public 时,它会自动创建一个具有相同名称的公共“getter”函数。因此,如果您想查找 id 15 的僵尸,您可以像调用函数一样调用它:zombies(15)
  • 以下是我们如何在前端编写一个 JavaScript 函数,传入僵尸 id在合约中查询该僵尸,并返回结果。注意,这是异步的,所以Web3在这里返回一个Promise。一旦 Promise 解决(意味着我们从 web3 provider 那里得到了答案),代码将继续执行 then 语句,将 result 记录到控制台。
function getZombieDetails(id) {
  return cryptoZombies.methods.zombies(id).call()
}

// Call the function and do something with the result:
getZombieDetails(15)
.then(function(result) {
  console.log("Zombie 15: " + JSON.stringify(result));
});
  • MetaMask 允许用户在其扩展程序中管理多个帐户,我们可以通过var userAccount = web3.eth.accounts[0]查看注入的 web3 变量当前处于活动状态的帐户。由于用户可以在 MetaMask 中随时切换活动帐户,因此我们的应用程序需要监视此变量以查看它是否已更改并相应地更新 UI。可以使用 setInterval 循环来做到这一点,它每 100 毫秒检查一次:
var accountInterval = setInterval(function() {
  // Check if account has changed
  if (web3.eth.accounts[0] !== userAccount) {
    userAccount = web3.eth.accounts[0];
    // Call some function to update the UI with the new account
    updateInterface();
  }
}, 100);
  •  从用户send 交易到该交易实际在区块链上生效,会存在明显的延迟。这是因为我们必须等待交易被包含在区块中,而以太坊的区块时间平均为 15 秒。如果以太坊上有很多待处理的交易,或者用户发送的 gas 价格太低,我们的交易可能需要等待几个区块才能被包含在内,这可能需要几分钟的时间。因此,我们需要处理此代码的异步性质。下面是我们使用 MetaMask 在 Web3.js 中调用createRandomZombie函数的示例:
function createRandomZombie(name) {
  // This is going to take a while, so update the UI to let the user know
  // the transaction has been sent
  $("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
  // Send the tx to our contract:
  return cryptoZombies.methods.createRandomZombie(name)
  .send({ from: userAccount })
  .on("receipt", function(receipt) {
    $("#txStatus").text("Successfully created " + name + "!");
    // Transaction was accepted into the blockchain, let's redraw the UI
    getZombiesByOwner(userAccount).then(displayZombies);
  })
  .on("error", function(error) {
    // Do something to alert the user their transaction has failed
    $("#txStatus").text(error);
  });
}
  • 我们的函数 send 一个交易到 Web3 provider,并链接一些事件监听器。当交易被包含到以太坊上的一个区块中时, receipt 将会触发,这意味着新僵尸已经被创建并保存在我们的合约中;如果存在阻止交易包含在区块中的问题(例如用户没有发送足够的 Gas), error 将触发,我们希望在 UI 中通知用户交易未完成,以便他们可以重试。
  • 当调用 send 时,您可以选择指定 gasgasPrice,例如 .send({ from: userAccount, gas: 3000000 }),如果您不指定此项,MetaMask 将让用户选择这些值。
  • wei 是 Ether 的最小子单元,1 ether 中有 10^18 个 wei。Web3.js 有一个转换工具可以为我们完成此操作。在我们的 DApp 中,我们设置了 levelUpFee = 0.001 ether,因此当我们调用 levelUp 函数时,我们可以使用以下代码让用户发送 0.001 以太币:
// This will convert 1 ETH to Wei
web3js.utils.toWei("1");
cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001", "ether") })
  • 在 Web3.js里, 你可以订阅一个事件,这样你的 Web3 provider 可以在每次事件发生后触发你的一些代码逻辑。请注意,每次在我们的 DApp 中创建任何僵尸时,这都会触发警报——而不仅仅是当前用户。
event NewZombie(uint zombieId, string name, uint dna);
cryptoZombies.events.NewZombie()
.on("data", function(event) {
  let zombie = event.returnValues;
  // We can access this event's 3 return values on the `event.returnValues` object:
  console.log("A new zombie was born!", zombie.zombieId, zombie.name, zombie.dna);
}).on("error", console.error);
  • 为了筛选仅和当前用户相关的事件,我们的 Solidity 合约将必须使用 indexed 关键字,就像我们在 ERC721 实现中的Transfer 事件中那样。这意味着我们可以在前端的事件监听器中过滤它们。
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
// Use `filter` to only fire this code when `_to` equals `userAccount`
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
  let data = event.returnValues;
  // The current user just received a zombie!
  // Do something here to update the UI to show it
}).on("error", console.error);
  • 我们可以用 getPastEvents 查询过去的事件,并用过滤器 fromBlocktoBlock 给 Solidity 一个事件日志的时间范围("block" 在这里代表以太坊区块编号)。因为你可以用这个方法来查询从最开始起的事件日志,所以可以用事件来作为一种更便宜的存储。在区块链上保存数据是 Solidity 中最贵的操作之一,但是用事件就便宜太多了。缺点是事件不能从智能合约本身内部读取,但如果你有一些数据需要永久性地记录在区块链中,以便可以在应用的前端中读取它,这将是一个很好的用例。
cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: 'latest' })
.then(function(events) {
  // events 是可以用来遍历的 `event` 对象 
  // 这段代码将返回给我们从开始以来创建的僵尸列表
});

 --------------------

这一课就结束啦!下一课要学习如何把智能合约部署到以太坊,我会另开一个贴。

学习愉快~

posted @ 2024-03-06 14:03  Aikoin  阅读(33)  评论(0编辑  收藏  举报