昨天已經建立了產生 Zombie 物件的 Contract ZombieFactory
可以透過 createRandomZombie 來根據名字隨機產生 Zombie 物件。
今天要新增使用者與 Zomibe 物件的互動性。
新增一個 feed 的功能,新增一種產生 Zombie 物件的 function 。
可以點入以下連結一起來撰寫 Contract
在 Ethereum 上,主要紀錄的 transaction 都是關於 Account 的紀錄。
每個 Account 至少會有一個 address, Account 會透過 address 紀錄其所擁有的 Balance。
可以把 address 想像成對應該 account 的識別碼。
address 的形式是一個長度 42 個字元的字串如下:
0x0cE446255506E92DF41614C46F1d6df9Cc969183
在 Contract 裡,可以透過紀錄 address 當作 owner 的識別 id 。
Mapping 在 solidity 語言是一種資料結構,類似於 Python 的 Dictionary 或是 java 的 HashMap。用來紀錄對應關係。
語法如下
// 建立紀錄 user account 與 balance 的 mapping
mapping (address => uint) public accountBalance;
// 建立紀錄 userId 與 username 的 mapping
mapping (uint => string) userIdToName;
為了紀錄 zombie 的持有關係,將要建立兩個 mapping
一個用來紀錄 zombie 的持有者 address。
一個用來紀錄每個持有者具有多少 zombie。
建立一個名稱為 zombieToOwner 的 mapping 。
使用 uint 當作 key 對應到持有者的 address。
並且設定讀取權限為 public 。
建立一個名稱為 ownerZombieCount 的 mapping 。
使用 address 當作 key 對應到 uint。
用來紀錄每個 address 具有幾個 zombie 。
更新 ZombieFactory 如下
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
// declare mappings here
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string memory _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
emit NewZombie(id, _name, _dna);
}
function _generateRandomDna(string memory _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string memory _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
msg.sender 在 solidity 是一個特殊的 preserved word
用來存取呼叫 Contract function 的 address 。
舉例來說:
mapping (address => uint) favoriteNumber;
function setMyNumber(uint _myNumber) public {
// 更新 favoriteNumber 這個 msg.sender key 的值為 _myNumber
favoriteNumber[msg.sender] = _myNumber;
}
function whatIsMyNumber() public view returns (uint) {
// 回傳當下 msg.sender 的 number
return favoriteNumber[msg.sender];
}
在 _createZombie 方法裡,加入儲存 zombie 所有權的邏輯
取出 Zombie 的 id ,然後更新 zombieToOwner[id] = msg.sender
更新 ownerZombieCount[msg.sender]++
在 solidity ,可以在 uint 變數使用 ++ 運算元如下:
uint number = 0;
number++; // number = 1
更新 ZombieFactory 如下
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string memory _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
// start here
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
emit NewZombie(id, _name, _dna);
}
function _generateRandomDna(string memory _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string memory _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
在 solidity , 如果需要限制某些條件才能執行功能,就可以使用 require 這個語法。只有在 require 設定條件是 true,才會繼續往下執行,否則會拋出錯誤並且停止執行。
語法如下:
function sayHiToSharon(string memory _name) public returns (string memory) {
// 檢核名稱要是 Sharon 才執行
require(keccak256(abi.encodePacked(_name)) == keccak256(abi.encodePacked("Sharon")));
// If it's true, proceed with the function:
return "Hi!";
}
限制每個 address 只能擁有一個 zombie 物件。
更新 ZombieFactory 如下
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string memory _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
emit NewZombie(id, _name, _dna);
}
function _generateRandomDna(string memory _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string memory _name) public {
// start here
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
隨著功能愈來愈多,原本的 Contract 內容會不斷增加。
這樣在閱讀或是維護起來並不方便。
有時候會透過把邏輯功能做切分來更有效的組織與管理 Contract。
Contract 的 inheritance 能夠達成這個目的。
範例如下:
contract Doge {
function catchphrase() public returns (string memory) {
return "So Wow CryptoDoge";
}
}
contract BabyDoge is Doge {
function anotherCatchphrase() public returns (string memory) {
return "Such Moon BabyDoge";
}
}
BabyDoge 透過 is 語法繼承了 Doge
因此 BadyDoge 可以呼叫 catchphrase 與 anotherCatchphrase 這兩個 method 。
為了比較好管理邏輯,
所以把 Feed zombie 邏輯放到另一個叫作 ZombieFeeding 的 Contract
並且使用 is 做繼承,讓 ZombieFeeding 可以使用 ZombieFactory 的 function 。
建立 ZombieFeeding 如下
contract ZombieFeeding is ZombieFactory {
}
當把兩個 Contract 分開寫,而其中另一個 Contract 需要引用另一個 Contract 時,就需要使用 import 語法如下:
import "./animal.sol";
contract Dog is Animal {
}
更新 ZombieFeeding 如下
pragma solidity >=0.5.0 <0.6.0;
// put import statement here
import "./zombiefactory.sol";
contract ZombieFeeding is ZombieFactory {
}
在 solidity, 有兩個地方可以儲存變數
把變數放在 Storage ,代表這個變數會永遠存在 blockchain 上。把變數放在 Memory ,代表這個變數只是暫時的。在外部函數呼叫時, 使用過的 Memory 變數將被清除。
Storage 變數之於 Memory 變數,就類似於硬碟之於記憶體。
一般來說,撰寫者不需要特別去申明這兩種變數,solidity 會自動處理這種變換。狀態變數(放在 function 之外的變數)會預設被設定成 storage 變數,而在 function 內的變數通常是 memory 變數,一旦呼叫結束,就會消失。
然而當遇到 structs 跟 array 時,就必須要做宣告如下
contract SandwichFactory {
struct Sandwich {
string name;
string status;
}
Sandwich[] sandwiches;
function eatSandwich(uint _index) public {
// 在此因為是直接使用到 struct 所以必須要特別申明
Sandwich storage mySandwich = sandwiches[_index];
// 這個更改會被紀錄在 blockchain 上
mySandwich.status = "Eaten!";
// 如果不想更改 只是要做 copy 就必須宣告 memory
Sandwich memory anotherSandwich = sandwiches[_index + 1];
// 這個只會改變 暫時變數 並不會寫到鏈上
anotherSandwich.status = "Eaten!";
// 然而可以透過以下語法 更新資料到鏈上
sandwiches[_index + 1] = anotherSandwich;
}
}
在 ZombieFeeding Contract 新增 feed 還有 multply 功能
當一個 zombie 被 feed 一位活人,該活人會變感染成新的 zombie 並且 DNA 會變成原本 DNA 加 zombie 的 DNA
實作細節如下
建立 feedAndMultiply function。
feedAndMultiply 需要兩個參數: _zombieId(uint) 與 _targetDna(uint)。 feedAndMultiply的讀取權限是 public 。
限定只有 zombie 擁有者才可以呼叫 feedAndMultiply,新增 require 條件檢查 msg.sender 需要是 zombie 的 owner。
需要取得該 zombie 的 DNA。所以需要建立一個 Zombie 參數叫作 myZombie 必須要是 storage 變數。設定 myZombie = zombies[_zombieId] 。
更新 ZombieFeeding 如下
pragma solidity >=0.5.0 <0.6.0;
import "./zombiefactory.sol";
contract ZombieFeeding is ZombieFactory {
// Start here
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require (msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
}
}
pragma solidity >=0.5.0 <0.6.0;
import "./zombiefactory.sol";
contract ZombieFeeding is ZombieFactory {
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
// start here
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna)/2;
_createZombie("NoName", newDna);
}
}
目前的 ZombieFeeding Contract
使用了 ZombieFactory 的 _createZombie 函數來建立 zombie 物件
然而, _createZombie 宣告是 private 。
也就是說,除了 ZombieFactory 沒有其他 Contract 可以呼叫 _createZombie 。
因此,需要使用其他 visibility 修飾子來修正 _createZombie 的讀取權限。
function 除了 public 與 private 讀取權限外。還有 internal 與 external 。
internal 類似於 private,只有 contract 本身以及繼承 contract 的其他 contract 可以存取,比 private 多了一個可以讓繼承者存取。
external 類似於 public,只能被其他 contract 呼叫,不同於 public 的地方是 contract 自己不能呼叫這個 function 。
範例如下:
contract Sandwich {
uint private sandwichesEaten = 0;
function eat() internal {
sandwichesEaten++;
}
}
contract BLT is Sandwich {
uint private baconSandwichesEaten = 0;
function eatWithBacon() public returns (string memory) {
baconSandwichesEaten++;
// We can call this here because it's internal
eat();
}
}
更新 ZombieFactory 如下
ragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
// edit function definition below
function _createZombie(string memory _name, uint _dna) internal {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
emit NewZombie(id, _name, _dna);
}
function _generateRandomDna(string memory _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string memory _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
為了要讓撰寫的 Contract 與其他不屬於我們的 Contract 互動
必須要先定好與其該 Contract 互動的 interface(介面)。
舉例: 假設有一個 LuckyNumber contract 如下
contract LuckyNumber {
mapping(address => uint) numbers;
function setNum(uint _num) public {
numbers[msg.sender] = _num;
}
function getNum(address _myAddress) public view returns (uint) {
return numbers[_myAddress];
}
}
這個 contract 讓任何人可以紀錄自己的 lucky number 並且讓所有人查詢。
假設我們想要定義一個 function getNum 跟這個外部 contract 互動,
必須要撰寫以下 interface 來做互動。
contract NumberInterface {
function getNum(address _myAddress) public view returns(uint);
}
雖然這個 interface 宣告起來很像 contract , 但是可以注意到這個 function 並沒有 function body 只有他的 function signature 。
透過這樣宣告, solidity 會自動判定這是一個 interface 。
首先,察看到 CryptoKitties Contract 內部有一個 getKitty 的 function
透過這個 function 來拿取 Kitty 的 genes 資料
這個 function 部份代碼如下
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
) {
Kitty storage kit = kitties[_id];
// if this variable is 0 then it's not gestating
isGestating = (kit.siringWithId != 0);
isReady = (kit.cooldownEndBlock <= block.number);
cooldownIndex = uint256(kit.cooldownIndex);
nextActionAt = uint256(kit.cooldownEndBlock);
siringWithId = uint256(kit.siringWithId);
birthTime = uint256(kit.birthTime);
matronId = uint256(kit.matronId);
sireId = uint256(kit.sireId);
generation = uint256(kit.generation);
genes = kit.genes;
}
具體步驟:
更新 ZombieFeeding 如下:
pragma solidity >=0.5.0 <0.6.0;
import "./zombiefactory.sol";
// Create KittyInterface here
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 {
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
_createZombie("NoName", newDna);
}
}
假設 NumberInterface 如下
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}
以下是使用 interface 的範例
contract MyContract {
address NumberInterfaceAddress = 0xab38...
// ^ The address of the FavoriteNumber contract on Ethereum
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// Now `numberContract` is pointing to the other contract
function someFunction() public {
// Now we can call `getNum` from that contract:
uint num = numberContract.getNum(msg.sender);
// ...and do something with `num` here
}
}
透過把 Contract Address 帶入 Interface 就可以使用該 Contract 的 external 或是 public function 了。
具體步驟:
更新 ZombieFeeding 如下
pragma solidity >=0.5.0 <0.6.0;
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 {
address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
// Initialize kittyContract here using `ckAddress` from above
KittyInterface kittyContract = KittyInterface(ckAddress);
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
_createZombie("NoName", newDna);
}
}
因為 getKitty function 回傳多個值。
所以下面說明一下,在 solidity 接收多個值的語法。
// 回傳多值的 function
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();
}
// Or if we only cared about one of the values:
function getLastReturnValue() external {
uint c;
// We can just leave the other fields blank:
(,,c) = multipleReturns();
}
更新 ZombieFeeding 如下
pragma solidity >=0.5.0 <0.6.0;
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 {
address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
KittyInterface kittyContract = KittyInterface(ckAddress);
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
_createZombie("NoName", newDna);
}
// define function here
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna);
}
}
特別讓 KittyZombie 具有一個特別的 DNA 組成
舉例來說在 DNA 最後兩個字元設定為 99。
這個可以透過 if statement 來做到
範例如下:
function eatBLT(string memory sandwich) public {
// Remember with strings, we have to compare their keccak256 hashes
// to check equality
if (keccak256(abi.encodePacked(sandwich)) == keccak256(abi.encodePacked("BLT"))) {
eat();
}
}
更新 ZombieFeeding 如下
pragma solidity >=0.5.0 <0.6.0;
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 {
address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
KittyInterface kittyContract = KittyInterface(ckAddress);
// Modify function definition here:
function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
// Add an if statement here
if (keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("kitty"))) {
newDna = newDna - newDna % 100 + 99;
}
_createZombie("NoName", newDna);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
// And modify function call here:
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}
一旦 deploy ZombieFactory 與 ZombieFeeding 到已太鏈上
就可以使用下面 code 與之互動
var abi = /* abi generated by the compiler */
var ZombieFeedingContract = web3.eth.contract(abi)
var contractAddress = /* our contract address on Ethereum after deploying */
var ZombieFeeding = ZombieFeedingContract.at(contractAddress)
// Assuming we have our zombie's ID and the kitty ID we want to attack
let zombieId = 1;
let kittyId = 1;
// To get the CryptoKitty's image, we need to query their web API. This
// information isn't stored on the blockchain, just their webserver.
// If everything was stored on a blockchain, we wouldn't have to worry
// about the server going down, them changing their API, or the company
// blocking us from loading their assets if they don't like our zombie game ;)
let apiUrl = "https://api.cryptokitties.co/kitties/" + kittyId
$.get(apiUrl, function(data) {
let imgUrl = data.image_url
// do something to display the image
})
// When the user clicks on a kitty:
$(".kittyImage").click(function(e) {
// Call our contract's `feedOnKitty` method
ZombieFeeding.feedOnKitty(zombieId, kittyId)
})
// Listen for a NewZombie event from our contract so we can display it:
ZombieFactory.NewZombie(function(error, result) {
if (error) return
// This function will display the zombie, like in lesson 1:
generateZombie(result.zombieId, result.name, result.dna)
})