以太坊开发实战学习-ERC721标准(七)

从这节开始,咱们将学习 代币, ERC721标准, 以及 加密收集资产等知识。

1、代币

代币

让咱们来聊聊以太坊上的代币数据库

若是你对以太坊的世界有一些了解,你极可能听过人们聊到代币——尤为是 ERC20 代币。数据结构

一个 代币 在以太坊基本上就是一个遵循一些共同规则的智能合约——即它实现了全部其余代币合约共享的一组标准函数,例如 transfer(address _to, uint256 _value)balanceOf(address _owner).app

在智能合约内部,一般有一个映射, mapping(address => uint256) balances,用于追踪每一个地址还有多少余额。函数

因此基本上一个代币只是一个追踪谁拥有多少该代币的合约,和一些可让那些用户将他们的代币转移到其余地址的函数学习

它为何重要呢?

因为全部 ERC20 代币共享具备相同名称的同一组函数,它们均可以以相同的方式进行交互。ui

这意味着若是你构建的应用程序可以与一个 ERC20 代币进行交互,那么它就也可以与任何 ERC20 代币进行交互。 这样一来,未来你就能够轻松地将更多的代币添加到你的应用中,而无需进行自定义编码。 你能够简单地插入新的代币合约地址,而后哗啦,你的应用程序有另外一个它可使用的代币了。编码

其中一个例子就是交易所。 当交易所添加一个新的 ERC20 代币时,实际上它只须要添加与之对话的另外一个智能合约。 用户可让那个合约将代币发送到交易所的钱包地址,而后交易所可让合约在用户要求取款时将代币发送回给他们。加密

交易所只须要实现这种转移逻辑一次,而后当它想要添加一个新的 ERC20 代币时,只需将新的合约地址添加到它的数据库便可。code

其余代币标准

对于像货币同样的代币来讲,ERC20 代币很是酷。 可是要在咱们僵尸游戏中表明僵尸就并非特别有用。继承

首先,僵尸不像货币能够分割 —— 我能够发给你 0.237 以太,可是转移给你 0.237 的僵尸听起来就有些搞笑。

其次,并非全部僵尸都是平等的。 你的2级僵尸"Steve"彻底不能等同于我732级的僵尸"H4XF13LD MORRIS"。(你差得远呢,Steve)。

有另外一个代币标准更适合如 CryptoZombies 这样的加密收藏品——它们被称为ERC721 代币.

ERC721代币不能互换的,由于每一个代币都被认为是惟一且不可分割的。 你只能以整个单位交易它们,而且每一个单位都有惟一的 ID。 这些特性正好让咱们的僵尸能够用来交易。

请注意,使用像 ERC721 这样的标准的优点就是,咱们没必要在咱们的合约中实现拍卖或托管逻辑,这决定了玩家可以如何交易/出售咱们的僵尸。 若是咱们符合规范,其余人能够为加密可交易的 ERC721 资产搭建一个交易所平台,咱们的 ERC721 僵尸将能够在该平台上使用。 因此使用代币标准相较于使用你本身的交易逻辑有明显的好处

实战演练

咱们将在下一章深刻讨论ERC721的实现。 但首先,让咱们为本课设置咱们的文件结构。

咱们将把全部ERC721逻辑存储在一个叫ZombieOwnership的合约中。

  • 一、在文件顶部声明咱们pragma的版本(格式参考以前的课程)。
  • 二、将 zombieattack.sol import 进来。
  • 三、声明一个继承 ZombieAttack 的新合约, 命名为ZombieOwnership。合约的其余部分先留空。

zombieownership.sol

// 从这里开始
pragma solidity ^0.4.19;

import "./zombieattack.sol";

contract ZombieOwnership is ZombieAttack {
    
}

2、ERC721标准与多重继承

让咱们来看一看 ERC721 标准:

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目前是一个 草稿,尚未正式商定的实现。在本教程中,咱们使用的是 OpenZeppelin 库中的当前版本,但在将来正式发布以前它可能会有更改。 因此把这 一个 可能的实现看成考虑,但不要把它做为 ERC721 代币的官方标准。

实现一个代币合约

在实现一个代币合约的时候,咱们首先要作的是将接口复制到它本身的 Solidity 文件并导入它,import ./erc721.sol。 接着,让咱们的合约继承它,而后咱们用一个函数定义来重写每一个方法。

但等一下—— ZombieOwnership 已经继承自 ZombieAttack 了 —— 它如何可以也继承于 ERC721 呢?

幸运的是在Solidity,你的合约能够继承自多个合约,参考以下:

contract SatoshiNakamoto is NickSzabo, HalFinney {
  // 啧啧啧,宇宙的奥秘泄露了
}

正如你所见,当使用多重继承的时候,你只须要用逗号 , 来隔开几个你想要继承的合约。在上面的例子中,咱们的合约继承自 NickSzabo 和 HalFinney。

来试试吧。

实战演练

咱们已经在上面为你建立了带着接口的 erc721.sol 。

  • 一、将 erc721.sol 导入到 zombieownership.sol
  • 二、声明 ZombieOwnership 继承自 ZombieAttackERC721

zombieownership.sol

pragma solidity ^0.4.19;

import "./zombieattack.sol";
// 在这里引入文件
import "./erc721.sol";

// 在这里声明 ERC721 的继承
contract ZombieOwnership is ZombieAttack, ERC721 {

}

3、 balanceOf和ownerOf

如今,咱们来深刻讨论一下 ERC721 的实现。

咱们已经把全部你须要在本课中实现的函数的空壳复制好了。

在本章节,咱们将实现头两个方法: balanceOfownerOf

balanceOf

function balanceOf(address _owner) public view returns (uint256 _balance);

这个函数只须要一个传入 address 参数,而后返回这个 address 拥有多少代币。

在咱们的例子中,咱们的“代币”是僵尸。你还记得在咱们 DApp 的哪里存储了一个主人拥有多少只僵尸吗?

ownerOf

function ownerOf(uint256 _tokenId) public view returns (address _owner);

这个函数须要传入一个代币 ID 做为参数 (咱们的状况就是一个僵尸 ID),而后返回该代币拥有者的 address

一样的,由于在咱们的 DApp 里已经有一个 mapping (映射) 存储了这个信息,因此对咱们来讲这个实现很是直接清晰。咱们能够只用一行 return 语句来实现这个函数。

注意:要记得, uint256 等同于uint。咱们从课程的开始一直在代码中使用 uint,但从如今开始咱们将在这里用 uint256,由于咱们直接从规范中复制粘贴。

实战演练

我将让你来决定如何实现这两个函数。

每一个函数的代码都应该只有1行 return 语句。看看咱们在以前课程中写的代码,想一想咱们都把这个数据存储在哪。若是你以为有困难,你能够点“我要看答案”的按钮来得到帮助。

  • 一、实现 balanceOf 来返回 _owner 拥有的僵尸数量。
  • 二、实现 ownerOf 来返回拥有 ID 为 _tokenId 僵尸的全部者的地址。

zombieownership.sol

pragma solidity ^0.4.19;

import "./zombieattack.sol";
import "./erc721.sol";

contract ZombieOwnership is ZombieAttack, ERC721 {

  function balanceOf(address _owner) public view returns (uint256 _balance) {
    // 1. 在这里返回 `_owner` 拥有的僵尸数
    return ownerZombieCount[_owner];
  }

  function ownerOf(uint256 _tokenId) public view returns (address _owner) {
    // 2. 在这里返回 `_tokenId` 的全部者
    return zombieToOwner[_tokenId];
  }

  function transfer(address _to, uint256 _tokenId) public {

  }

  function approve(address _to, uint256 _tokenId) public {

  }

  function takeOwnership(uint256 _tokenId) public {

  }
}

4、重构

Hey!咱们刚刚的代码中其实有个错误,以致于其根本没法经过编译,你发现了没?

在前一个章节咱们定义了一个叫 ownerOf 的函数。但若是你还记得第4课的内容,咱们一样在zombiefeeding.sol 里以 ownerOf 命名建立了一个 modifier(修饰符)。

若是你尝试编译这段代码,编译器会给你一个错误说你不能有相同名称的修饰符和函数。

因此咱们应该把在 ZombieOwnership 里的函数名称改为别的吗?

不,咱们不能那样作!!!要记得,咱们正在用 ERC721 代币标准,意味着其余合约将指望咱们的合约以这些确切的名称来定义函数。这就是这些标准实用的缘由——若是另外一个合约知道咱们的合约符合 ERC721 标准,它能够直接与咱们交互,而无需了解任何关于咱们内部如何实现的细节。

因此,那意味着咱们将必须重构咱们第4课中的代码,将 modifier 的名称换成别的。

实战演练

咱们回到了 zombiefeeding.sol 。咱们将把 modifier 的名称从 ownerOf 改为 onlyOwnerOf

  • 一、把修饰符定义中的名称改为 onlyOwnerOf
  • 二、往下滑到使用此修饰符的函数 feedAndMultiply 。咱们也须要改这里的名称。
注意:咱们在 zombiehelper.sol 和 zombieattack.sol 里也使用了这个修饰符,因此这两个文件也必须把名字改了。

zombiefeeding.sol

pragma solidity ^0.4.19;

import "./zombiefactory.sol";

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

contract ZombieFeeding is ZombieFactory {

  KittyInterface kittyContract;

  // 1. 把修饰符名称改为 `onlyOwnerOf`
  modifier onlyOwnerOf(uint _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    _;
  }

  function setKittyContractAddress(address _address) external onlyOwner {
    kittyContract = KittyInterface(_address);
  }

  function _triggerCooldown(Zombie storage _zombie) internal {
    _zombie.readyTime = uint32(now + cooldownTime);
  }

  function _isReady(Zombie storage _zombie) internal view returns (bool) {
      return (_zombie.readyTime <= now);
  }

  // 2. 这里也要修改修饰符的名称
  function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal onlyOwnerOf(_zombieId) {
    Zombie storage myZombie = zombies[_zombieId];
    require(_isReady(myZombie));
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    if (keccak256(_species) == keccak256("kitty")) {
      newDna = newDna - newDna % 100 + 99;
    }
    _createZombie("NoName", newDna);
    _triggerCooldown(myZombie);
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna, "kitty");
  }
}

5、ERC721转移标准

如今咱们将经过学习把全部权从一我的转移给另外一我的来继续咱们的 ERC721 规范的实现。

注意 ERC721 规范有两种不一样的方法来转移代币:

function transfer(address _to, uint256 _tokenId) public;

function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
  • 一、第一种方法是代币的拥有者调用transfer 方法,传入他想转移到的 address 和他想转移的代币的 _tokenId
  • 二、第二种方法是代币拥有者首先调用 approve,而后传入与以上相同的参数。接着,该合约会存储谁被容许提取代币,一般存储到一个 mapping (uint256 => address) 里。而后,当有人调用 takeOwnership 时,合约会检查 msg.sender 是否获得拥有者的批准来提取代币,若是是,则将代币转移给他。
你注意到了吗, transfertakeOwnership 都将包含相同的转移逻辑,只是以相反的顺序。 (一种状况是代币的发送者调用函数;另外一种状况是代币的接收者调用它)。

因此咱们把这个逻辑抽象成它本身的私有函数 _transfer,而后由这两个函数来调用它。 这样咱们就不用写重复的代码了。

实战演练

让咱们来定义 _transfer 的逻辑。

  • 一、定义一个名为 _transfer的函数。它会须要3个参数:address _fromaddress _touint256 _tokenId。它应该是一个 私有 函数。
  • 二、咱们有2个映射会在全部权改变的时候改变: ownerZombieCount (记录一个全部者有多少只僵尸)和 zombieToOwner (记录什么人拥有什么)。
  • 咱们的函数须要作的第一件事是为 接收 僵尸的人(address _to)增 加ownerZombieCount。使用 ++ 来增长。
  • 三、接下来,咱们将须要为 发送 僵尸的人(address _from)减小ownerZombieCount。使用 -- 来扣减。
  • 四、最后,咱们将改变这个 _tokenIdzombieToOwner 映射,这样它如今就会指向 _to
  • 五、骗你的,那不是最后一步。咱们还须要再作一件事情。

ERC721规范包含了一个 Transfer 事件。这个函数的最后一行应该用正确的参数触发Transfer ——查看 erc721.sol 看它指望传入的参数并在这里实现。

zombieownership.zol

pragma solidity ^0.4.19;

import "./zombieattack.sol";
import "./erc721.sol";

contract ZombieOwnership is ZombieAttack, ERC721 {

  function balanceOf(address _owner) public view returns (uint256 _balance) {
    return ownerZombieCount[_owner];
  }

  function ownerOf(uint256 _tokenId) public view returns (address _owner) {
    return zombieToOwner[_tokenId];
  }

  // 在这里定义 _transfer()
  function _transfer(address _from, address _to, uint256 _tokenId) private {
   /*错误的写法
    balanceOf(_to)++;
    balanceOf(_from)--;
    ownerOf(_tokenId);
    */
    
    ownerZombieCount[_to]++;
    ownerZombieCount[_from]--;
    zombieToOwner[_tokenId] = _to;
    Transfer(_from, _to, _tokenId);

  }

  function transfer(address _to, uint256 _tokenId) public {

  }

  function approve(address _to, uint256 _tokenId) public {

  }

  function takeOwnership(uint256 _tokenId) public {

  }
}

刚才那是最难的部分——如今实现公共的 transfer 函数应该十分容易,由于咱们的 _transfer 函数几乎已经把全部的重活都干完了。

实战演练

  • 一、咱们想确保只有代币或僵尸的全部者能够转移它。还记得咱们如何限制只有全部者才能访问某个功能吗?
  • 没错,咱们已经有一个修饰符可以完成这个任务了。因此将修饰符 onlyOwnerOf 添加到这个函数中。
  • 二、如今该函数的正文只须要一行代码。它只须要调用 _transfer。
  • 记得把 msg.sender 做为参数传递进 address _from

zombieownership.zol

pragma solidity ^0.4.19;

import "./zombieattack.sol";
import "./erc721.sol";

contract ZombieOwnership is ZombieAttack, ERC721 {

  function balanceOf(address _owner) public view returns (uint256 _balance) {
    return ownerZombieCount[_owner];
  }

  function ownerOf(uint256 _tokenId) public view returns (address _owner) {
    return zombieToOwner[_tokenId];
  }

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

  // 1. 在这里添加修饰符
  function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
    // 2. 在这里定义方法
    _transfer(msg.sender, _to, _tokenId);
  }

  function approve(address _to, uint256 _tokenId) public {

  }

  function takeOwnership(uint256 _tokenId) public {

  }
}

6、ERC721之批准

如今,让咱们来实现 approve

记住,使用 approve 或者 takeOwnership 的时候,转移有2个步骤:

  • 一、你,做为全部者,用新主人的 address 和你但愿他获取的 _tokenId 来调用 approve
  • 二、新主人用 _tokenId 来调用 takeOwnership,合约会检查确保他得到了批准,而后把代币转移给他。

由于这发生在2个函数的调用中,因此在函数调用之间,咱们须要一个数据结构来存储什么人被批准获取什么。

实战演练

  • 一、首先,让咱们来定义一个映射 zombieApprovals。它应该将一个 uint 映射到一个 address
  • 这样一来,当有人用一个 _tokenId 调用 takeOwnership 时,咱们能够用这个映射来快速查找谁被批准获取那个代币。
  • 二、在函数 approve 上, 咱们想要确保只有代币全部者能够批准某人来获取代币。因此咱们须要添加修饰符 onlyOwnerOf 到 approve。
  • 三、函数的正文部分,将 _tokenIdzombieApprovals 设置为和 _to 相等。
  • 四、最后,在 ERC721 规范里有一个 Approval 事件。因此咱们应该在这个函数的最后触发这个事件。(参考 erc721.sol 来确认传入的参数,并确保 _owner 是 msg.sender)

zombieownership.zol

pragma solidity ^0.4.19;

import "./zombieattack.sol";
import "./erc721.sol";

contract ZombieOwnership is ZombieAttack, ERC721 {

  // 1. 在这里定义映射
  mapping (uint => address) zombieApprovals;

  function balanceOf(address _owner) public view returns (uint256 _balance) {
    return ownerZombieCount[_owner];
  }

  function ownerOf(uint256 _tokenId) public view returns (address _owner) {
    return zombieToOwner[_tokenId];
  }

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

  // 2. 在这里添加方法修饰符
  function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
    // 3. 在这里定义方法
    zombieApprovals[_tokenId] = _to;
    Approval(msg.sender, _to, _tokenId);   // 协议事件

  }

  function takeOwnership(uint256 _tokenId) public {

  }
}

7、ERC721之takeOwnership

如今让咱们完成最后一个函数来结束 ERC721 的实现。

最后一个函数 takeOwnership, 应该只是简单地检查以确保 msg.sender 已经被批准来提取这个代币或者僵尸。若确认,就调用 _transfer

实战演练

  • 一、首先,咱们要用一个 require 句式来检查 _tokenIdzombieApprovalsmsg.sender 相等。
  • 这样若是 msg.sender 未被受权来提取这个代币,将抛出一个错误。
  • 二、为了调用 _transfer,咱们须要知道代币全部者的地址(它须要一个 _from 来做为参数)。幸运的是咱们能够在咱们的 ownerOf 函数中来找到这个参数。
  • 因此,定义一个名为 owner 的 address 变量,并使其等于 ownerOf(_tokenId)。
  • 三、最后,调用 _transfer, 并传入全部必须的参数。(在这里你能够用 msg.sender 做为 _to, 由于代币正是要发送给调用这个函数的人)。
注意: 咱们彻底能够用一行代码来实现第二、3两步。可是分开写会让代码更易读。一点我的建议 :)

zombieownership.zol

pragma solidity ^0.4.19;

import "./zombieattack.sol";
import "./erc721.sol";

contract ZombieOwnership is ZombieAttack, ERC721 {

  mapping (uint => address) zombieApprovals;

  function balanceOf(address _owner) public view returns (uint256 _balance) {
    return ownerZombieCount[_owner];
  }

  function ownerOf(uint256 _tokenId) public view returns (address _owner) {
    return zombieToOwner[_tokenId];
  }

  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);
  }
}
相关文章
相关标签/搜索