從這節開始,咱們將學習代幣
,ERC721
標準, 以及加密收集資產
等知識。
讓咱們來聊聊以太坊上的代幣
。數據庫
若是你對以太坊的世界有一些瞭解,你極可能聽過人們聊到代幣——尤爲是 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
的合約中。
zombieattack.sol import
進來。ZombieAttack
的新合約, 命名爲ZombieOwnership
。合約的其餘部分先留空。zombieownership.sol
// 從這裏開始 pragma solidity ^0.4.19; import "./zombieattack.sol"; contract ZombieOwnership is ZombieAttack { }
讓咱們來看一看 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
繼承自 ZombieAttack
和 ERC721
zombieownership.sol
pragma solidity ^0.4.19; import "./zombieattack.sol"; // 在這裏引入文件 import "./erc721.sol"; // 在這裏聲明 ERC721 的繼承 contract ZombieOwnership is ZombieAttack, ERC721 { }
如今,咱們來深刻討論一下 ERC721
的實現。
咱們已經把全部你須要在本課中實現的函數的空殼複製好了。
在本章節,咱們將實現頭兩個方法: balanceOf
和 ownerOf
。
function balanceOf(address _owner) public view returns (uint256 _balance);
這個函數只須要一個傳入 address
參數,而後返回這個 address
擁有多少代幣。
在咱們的例子中,咱們的「代幣」是殭屍。你還記得在咱們 DApp 的哪裏存儲了一個主人擁有多少隻殭屍嗎?
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 { } }
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"); } }
如今咱們將經過學習把全部權從一我的轉移給另外一我的來繼續咱們的 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
是否獲得擁有者的批准來提取代幣,若是是,則將代幣轉移給他。你注意到了嗎,transfer
和takeOwnership
都將包含相同的轉移邏輯,只是以相反的順序。 (一種狀況是代幣的發送者調用函數;另外一種狀況是代幣的接收者調用它)。
因此咱們把這個邏輯抽象成它本身的私有函數 _transfer
,而後由這兩個函數來調用它。 這樣咱們就不用寫重複的代碼了。
讓咱們來定義 _transfer
的邏輯。
_transfer
的函數。它會須要3個參數:address _from
、address _to
和uint256 _tokenId
。它應該是一個 私有
函數。ownerZombieCount
(記錄一個全部者有多少隻殭屍)和 zombieToOwner
(記錄什麼人擁有什麼)。address _to
)增 加ownerZombieCount
。使用 ++
來增長。address _from
)減小ownerZombieCount
。使用 -- 來扣減。_tokenId
的 zombieToOwner
映射,這樣它如今就會指向 _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
添加到這個函數中。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 { } }
如今,讓咱們來實現 approve
。
記住,使用 approve
或者 takeOwnership
的時候,轉移有2個步驟:
address
和你但願他獲取的 _tokenId
來調用 approve
_tokenId
來調用 takeOwnership
,合約會檢查確保他得到了批准,而後把代幣轉移給他。由於這發生在2個函數的調用中,因此在函數調用之間,咱們須要一個數據結構來存儲什麼人被批准獲取什麼。
zombieApprovals
。它應該將一個 uint
映射到一個 address
。_tokenId
的 zombieApprovals
設置爲和 _to
相等。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 { } }
如今讓咱們完成最後一個函數來結束 ERC721 的實現。
最後一個函數 takeOwnership
, 應該只是簡單地檢查以確保 msg.sender
已經被批准來提取這個代幣或者殭屍。若確認,就調用 _transfer
;
require
句式來檢查 _tokenId
的 zombieApprovals
和 msg.sender
相等。msg.sender
未被受權來提取這個代幣,將拋出一個錯誤。注意: 咱們徹底能夠用一行代碼來實現第二、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); } }