以太坊開發實戰學習-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);
  }
}
相關文章
相關標籤/搜索