以太坊Solidity開發(fā)入門(進階篇)

上一篇:以太坊Solidity開發(fā)入門(基礎篇)

第1章: 第二課概覽

在第一課中,我們創(chuàng)建了一個函數用來生成僵尸玻孟,并且將它放入區(qū)塊鏈上的僵尸數據庫中黍翎。 在第二課里匣掸,我們會讓我們的 app 看起來更像一個游戲: 它得支持多用戶,并且采用更加有趣,而不僅僅使用隨機的方式砰粹,來生成新的僵尸碱璃。

如何生成新的僵尸呢嵌器?通過讓現有的僵尸獵食其他生物蚓让!

僵尸獵食
僵尸獵食的時候历极,僵尸病毒侵入獵物趟卸,這些病毒會將獵物變?yōu)樾碌慕┦校尤肽愕慕┦筌姟O到y(tǒng)會通過獵物和獵食者僵尸的DNA計算出新僵尸的DNA饶囚。

僵尸最喜歡獵食什么物種呢萝风? 等你學完第二課就知道了规惰!

實戰(zhàn)演習
右邊是一個簡單的獵食演示泉蝌。點擊一個“人”歇万,看看僵尸獵食的時候會發(fā)生什么? 可見,新僵尸的DNA是通過從原來的僵尸的DNA, 加上獵物的DNA計算得來的勋陪。

學完這一章贪磺,請點擊“下一章”, 我們該讓游戲支持多玩家模式了诅愚。


第2章: 映射(Mapping)和地址(Address)

我們通過給數據庫中的僵尸指定“主人”, 來支持“多玩家”模式违孝。

如此一來刹前,我們需要引入2個新的數據類型:mapping(映射) 和 address(地址)。

Addresses (地址)

以太坊區(qū)塊鏈由 account (賬戶)組成雌桑,你可以把它想象成銀行賬戶喇喉。一個帳戶的余額是 以太 (在以太坊區(qū)塊鏈上使用的幣種),你可以和其他帳戶之間支付和接受以太幣校坑,就像你的銀行帳戶可以電匯資金到其他銀行帳戶一樣拣技。

每個帳戶都有一個“地址”千诬,你可以把它想象成銀行賬號。這是賬戶唯一的標識符膏斤,它看起來長這樣:

0x0cE446255506E92DF41614C46F1d6df9Cc969183

(這是 CryptoZombies 團隊的地址徐绑,如果你喜歡 CryptoZombies 的話,請打賞我們一些以太幣掸绞!??)

我們將在后面的課程中介紹地址的細節(jié),現在你只需要了解地址屬于特定用戶(或智能合約)的耕捞。

所以我們可以指定“地址”作為僵尸主人的 ID衔掸。當用戶通過與我們的應用程序交互來創(chuàng)建新的僵尸時,新僵尸的所有權被設置到調用者的以太坊地址下俺抽。

Mapping(映射)

在第1課中敞映,我們看到了 結構體數組映射 是另一種在 Solidity 中存儲有組織數據的方法磷斧。

映射是這樣定義的:

//對于金融應用程序振愿,將用戶的余額保存在一個 uint類型的變量中:
mapping (address => uint) public accountBalance;
//或者可以用來通過userId 存儲/查找的用戶名
mapping (uint => string) userIdToName;

映射本質上是存儲和查找數據所用的鍵-值對。在第一個例子中弛饭,鍵是一個 address冕末,值是一個 uint,在第二個例子中侣颂,鍵是一個uint档桃,值是一個 string

實戰(zhàn)演習

為了存儲僵尸的所有權憔晒,我們會使用到兩個映射:一個記錄僵尸擁有者的地址藻肄,另一個記錄某地址所擁有僵尸的數量。

  1. 創(chuàng)建一個叫做 zombieToOwner 的映射拒担。其鍵是一個uint(我們將根據它的 id 存儲和查找僵尸)嘹屯,值為 address。映射屬性為public从撼。

  2. 創(chuàng)建一個名為 ownerZombieCount 的映射州弟,其中鍵是 address,值是 uint低零。

pragma solidity ^0.4.19;

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 _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    function createRandomZombie(string _name) public {
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }

}

第3章: Msg.sender

現在有了一套映射來記錄僵尸的所有權了呆馁,我們可以修改 _createZombie 方法來運用它們。

為了做到這一點毁兆,我們要用到 msg.sender浙滤。

msg.sender

在 Solidity 中,有一些全局變量可以被所有函數調用气堕。 其中一個就是 msg.sender纺腊,它指的是當前調用者(或智能合約)的 address畔咧。

注意:在 Solidity 中,功能執(zhí)行始終需要從外部調用者開始揖膜。 一個合約只會在區(qū)塊鏈上什么也不做誓沸,除非有人調用其中的函數。所以 msg.sender總是存在的壹粟。

以下是使用 msg.sender 來更新 mapping 的例子:

mapping (address => uint) favoriteNumber;

function setMyNumber(uint _myNumber) public {
  // 更新我們的 `favoriteNumber` 映射來將 `_myNumber`存儲在 `msg.sender`名下
  favoriteNumber[msg.sender] = _myNumber;
  // 存儲數據至映射的方法和將數據存儲在數組相似
}

function whatIsMyNumber() public view returns (uint) {
  // 拿到存儲在調用者地址名下的值
  // 若調用者還沒調用 setMyNumber拜隧, 則值為 `0`
  return favoriteNumber[msg.sender];
}

在這個小小的例子中,任何人都可以調用 setMyNumber 在我們的合約中存下一個 uint 并且與他們的地址相綁定趁仙。 然后洪添,他們調用 whatIsMyNumber 就會返回他們存儲的 uint

使用 msg.sender 很安全雀费,因為它具有以太坊區(qū)塊鏈的安全保障 —— 除非竊取與以太坊地址相關聯的私鑰蠢沿,否則是沒有辦法修改其他人的數據的惑灵。

實戰(zhàn)演習

我們來修改第1課的 _createZombie 方法,將僵尸分配給函數調用者吧。

  • 首先河胎,在得到新的僵尸 id 后镊屎,更新 zombieToOwner 映射妥色,在 id 下面存入 msg.sender吨些。

  • 然后,我們?yōu)檫@個 msg.sender 名下的 ownerZombieCount 加 1刁愿。

跟在 JavaScript 中一樣黑低, 在 Solidity 中你也可以用 ++ 使 uint 遞增。

uint number = 0;
number++;
// `number` 現在是 `1`了

修改兩行代碼即可酌毡。

pragma solidity ^0.4.19;

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 _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        
        NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    function createRandomZombie(string _name) public {
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }

}


第4章: Require

在第一課中克握,我們成功讓用戶通過調用 createRandomZombie函數 并輸入一個名字來創(chuàng)建新的僵尸。 但是枷踏,如果用戶能持續(xù)調用這個函數來創(chuàng)建出無限多個僵尸加入他們的軍團菩暗,這游戲就太沒意思了!

于是旭蠕,我們作出限定:每個玩家只能調用一次這個函數停团。 這樣一來,新玩家可以在剛開始玩游戲時通過調用它掏熬,為其軍團創(chuàng)建初始僵尸佑稠。

我們怎樣才能限定每個玩家只調用一次這個函數呢?

答案是使用require旗芬。 require使得函數在執(zhí)行過程中舌胶,當不滿足某些條件時拋出錯誤,并停止執(zhí)行:

function sayHiToVitalik(string _name) public returns (string) {
  // 比較 _name 是否等于 "Vitalik". 如果不成立疮丛,拋出異常并終止程序
  // (敲黑板: Solidity 并不支持原生的字符串比較, 我們只能通過比較
  // 兩字符串的 keccak256 哈希值來進行判斷)
  require(keccak256(_name) == keccak256("Vitalik"));
  // 如果返回 true, 運行如下語句
  return "Hi!";
}

如果你這樣調用函數 sayHiToVitalik(“Vitalik”) ,它會返回“Hi幔嫂!”辆它。而如果調用的時候使用了其他參數,它則會拋出錯誤并停止執(zhí)行履恩。

因此锰茉,在調用一個函數之前,用 require 驗證前置條件是非常有必要的切心。

實戰(zhàn)演習

在我們的僵尸游戲中飒筑,我們不希望用戶通過反復調用 createRandomZombie 來給他們的軍隊創(chuàng)建無限多個僵尸 —— 這將使得游戲非常無聊。

我們使用了 require 來確保這個函數只有在每個用戶第一次調用它的時候執(zhí)行绽昏,用以創(chuàng)建初始僵尸协屡。

createRandomZombie 的前面放置 require 語句。 使得函數先檢查 ownerZombieCount [msg.sender] 的值為 0 而涉,不然就拋出一個錯誤著瓶。

注意:在 Solidity 中联予,關鍵詞放置的順序并不重要

  • 雖然參數的兩個位置是等效的啼县。 但是,由于我們的答案檢查器比較呆板沸久,它只能認定其中一個為正確答案
  • 于是在這里季眷,我們就約定把ownerZombieCount [msg.sender]放前面吧
pragma solidity ^0.4.19;

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 _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    function createRandomZombie(string _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }

}

第5章: 繼承(Inheritance)

我們的游戲代碼越來越長。 當代碼過于冗長的時候卷胯,最好將代碼和邏輯分拆到多個不同的合約中子刮,以便于管理。

有個讓 Solidity 的代碼易于管理的功能窑睁,就是合約 inheritance (繼承):

contract Doge {
  function catchphrase() public returns (string) {
    return "So Wow CryptoDoge";
  }
}

contract BabyDoge is Doge {
  function anotherCatchphrase() public returns (string) {
    return "Such Moon BabyDoge";
  }
}

由于 BabyDoge 是從 Doge 那里 inherits (繼承)過來的挺峡。 這意味著當你編譯和部署了 BabyDoge,它將可以訪問 catchphrase() 和 anotherCatchphrase()和其他我們在 Doge 中定義的其他公共函數担钮。

這可以用于邏輯繼承(比如表達子類的時候橱赠,Cat 是一種 Animal)。 但也可以簡單地將類似的邏輯組合到不同的合約中以組織代碼箫津。

實戰(zhàn)演習
在接下來的章節(jié)中狭姨,我們將要為僵尸實現各種功能,讓它可以“獵食”和“繁殖”苏遥。 通過將這些運算放到父類 ZombieFactory 中饼拍,使得所有 ZombieFactory 的繼承者合約都可以使用這些方法。

  • ZombieFactory 下創(chuàng)建一個叫 ZombieFeeding 的合約田炭,它是繼承自 ZombieFactory 合約的师抄。
pragma solidity ^0.4.19;

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 _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    function createRandomZombie(string _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }

}

contract ZombieFeeding is ZombieFactory {
    
}

第6章: 引入(Import)

哇!你有沒有注意到教硫,我們只是清理了下右邊的代碼司澎,現在你的編輯器的頂部就多了個選項卡欺缘。 嘗試點擊它的標簽,看看會發(fā)生什么吧挤安!

代碼已經夠長了谚殊,我們把它分成多個文件以便于管理。 通常情況下蛤铜,當 Solidity 項目中的代碼太長的時候我們就是這么做的嫩絮。

在 Solidity 中,當你有多個文件并且想把一個文件導入另一個文件時围肥,可以使用 import 語句:

import "./someothercontract.sol";

contract newContract is SomeOtherContract {

}

這樣當我們在合約(contract)目錄下有一個名為 someothercontract.sol 的文件( ./ 就是同一目錄的意思)剿干,它就會被編譯器導入。

實戰(zhàn)演習
現在我們已經建立了一個多文件架構穆刻,并用 import 來讀取來自另一個文件中合約的內容:

1.將 zombiefactory.sol 導入到我們的新文件 zombiefeeding.sol 中置尔。

//新建一個zombiefeeding.sol文件
pragma solidity ^0.4.19;

import "./zombiefactory.sol";

contract ZombieFeeding is ZombieFactory {

}

第7章: Storage與Memory

在 Solidity 中,有兩個地方可以存儲變量 —— storagememory氢伟。

Storage 變量是指永久存儲在區(qū)塊鏈中的變量榜轿。 Memory 變量則是臨時的,當外部函數對某合約調用完成時朵锣,內存型變量即被移除谬盐。 你可以把它想象成存儲在你電腦的硬盤或是RAM中數據的關系。

大多數時候你都用不到這些關鍵字诚些,默認情況下 Solidity 會自動處理它們飞傀。 狀態(tài)變量(在函數之外聲明的變量)默認為“存儲”形式,并永久寫入區(qū)塊鏈诬烹;而在函數內部聲明的變量是“內存”型的砸烦,它們函數調用結束后消失。

然而也有一些情況下绞吁,你需要手動聲明存儲類型幢痘,主要用于處理函數內的 結構體數組 時:

contract SandwichFactory {
  struct Sandwich {
    string name;
    string status;
  }

  Sandwich[] sandwiches;

  function eatSandwich(uint _index) public {
    // Sandwich mySandwich = sandwiches[_index];

    // ^ 看上去很直接,不過 Solidity 將會給出警告
    // 告訴你應該明確在這里定義 `storage` 或者 `memory`掀泳。

    // 所以你應該明確定義 `storage`:
    Sandwich storage mySandwich = sandwiches[_index];
    // ...這樣 `mySandwich` 是指向 `sandwiches[_index]`的指針
    // 在存儲里雪隧,另外...
    mySandwich.status = "Eaten!";
    // ...這將永久把 `sandwiches[_index]` 變?yōu)閰^(qū)塊鏈上的存儲

    // 如果你只想要一個副本,可以使用`memory`:
    Sandwich memory anotherSandwich = sandwiches[_index + 1];
    // ...這樣 `anotherSandwich` 就僅僅是一個內存里的副本了
    // 另外
    anotherSandwich.status = "Eaten!";
    // ...將僅僅修改臨時變量员舵,對 `sandwiches[_index + 1]` 沒有任何影響
    // 不過你可以這樣做:
    sandwiches[_index + 1] = anotherSandwich;
    // ...如果你想把副本的改動保存回區(qū)塊鏈存儲
  }
}

如果你還沒有完全理解究竟應該使用哪一個脑沿,也不用擔心 —— 在本教程中,我們將告訴你何時使用 storage 或是 memory马僻,并且當你不得不使用到這些關鍵字的時候庄拇,Solidity 編譯器也發(fā)警示提醒你的。

現在,只要知道在某些場合下也需要你顯式地聲明 storagememory就夠了措近!

實戰(zhàn)演習

是時候給我們的僵尸增加“獵食”和“繁殖”功能了溶弟!

當一個僵尸獵食其他生物體時,它自身的DNA將與獵物生物的DNA結合在一起瞭郑,形成一個新的僵尸DNA辜御。

  • 創(chuàng)建一個名為 feedAndMultiply 的函數。 使用兩個參數:_zombieIduint類型 )和_targetDna (也是 uint 類型)屈张。 設置屬性為 public 的擒权。

  • 我們不希望別人用我們的僵尸去捕獵。 首先阁谆,我們確保對自己僵尸的所有權碳抄。 通過添加一個require 語句來確保 msg.sender 只能是這個僵尸的主人(類似于我們在 createRandomZombie 函數中做過的那樣)。

注意:同樣场绿,因為我們的答案檢查器比較呆萌剖效,只認識把 msg.sender 放在前面的答案,如果你切換了參數的順序焰盗,它就不認得了璧尸。 但你正常編碼時,如何安排參數順序都是正確的姨谷。

  • 為了獲取這個僵尸的DNA逗宁,我們的函數需要聲明一個名為 myZombie 數據類型為Zombie的本地變量(這是一個 storage 型的指針)映九。 將其值設定為在 zombies 數組中索引為_zombieId所指向的值梦湘。
    到目前為止,包括函數結束符 } 的那一行件甥, 總共4行代碼捌议。

下一章里,我們會繼續(xù)豐富這個功能引有。

pragma solidity ^0.4.19;

import "./zombiefactory.sol";

contract ZombieFeeding is ZombieFactory {

  function feedAndMultiply(uint _zombieId, uint _targetDna) public{
      require(msg.sender == zombieToOwner[_zombieId]);
      Zombie storage myZombie = zombies[_zombieId]; 
  }

}


第8章: 僵尸的DNA

我們來把 feedAndMultiply 函數寫完吧瓣颅。

獲取新的僵尸DNA的公式很簡單:計算獵食僵尸DNA和被獵僵尸DNA之間的平均值。

例如:

function testDnaSplicing() public {
  uint zombieDna = 2222222222222222;
  uint targetDna = 4444444444444444;
  uint newZombieDna = (zombieDna + targetDna) / 2;
  // newZombieDna 將等于 3333333333333333
}

以后譬正,我們也可以讓函數變得更復雜些宫补,比方給新的僵尸的 DNA 增加一些隨機性之類的。但現在先從最簡單的開始 —— 以后還可以回來完善它嘛曾我。

實戰(zhàn)演習

首先我們確保 _targetDna 不長于16位粉怕。要做到這一點,我們可以設置 _targetDna_targetDna%dnaModulus 抒巢,并且只取其最后16位數字贫贝。

接下來為我們的函數聲明一個名叫 newDnauint類型的變量,并將其值設置為 myZombie的 DNA 和 _targetDna 的平均值(如上例所示)。

注意:您可以用 myZombie.name 或 myZombie.dna 訪問 myZombie 的屬性稚晚。

  • 一旦我們計算出新的DNA崇堵,再調用 _createZombie 就可以生成新的僵尸了。如果你忘了調用這個函數所需要的參數客燕,可以查看 zombiefactory.sol 選項卡鸳劳。請注意,需要先給它命名也搓,所以現在我們把新的僵尸的名字設為NoName - 我們回頭可以編寫一個函數來更改僵尸的名字棍辕。

注意:對于 Solidity 高手,你可能會注意到我們的代碼存在一個問題还绘。別擔心楚昭,下一章會解決這個問題的 ;)

pragma solidity ^0.4.19;

import "./zombiefactory.sol";

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

}

第9章: 更多關于函數可見性

我們上一課的代碼有問題!

編譯的時候編譯器就會報錯拍顷。

錯誤在于抚太,我們嘗試從 ZombieFeeding 中調用 _createZombie 函數,但 _createZombie 卻是 ZombieFactoryprivate (私有)函數昔案。這意味著任何繼承自 ZombieFactory 的子合約都不能訪問它尿贫。

internal 和 external

publicprivate 屬性之外,Solidity 還使用了另外兩個描述函數可見性的修飾詞:internal(內部) 和 external(外部)踏揣。

internalprivate 類似庆亡,不過, 如果某個合約繼承自其父合約捞稿,這個合約即可以訪問父合約中定義的“內部”函數又谋。(嘿,這聽起來正是我們想要的那樣S榫帧)彰亥。

externalpublic 類似,只不過這些函數只能在合約之外調用 - 它們不能被合約內的其他函數調用衰齐。稍后我們將討論什么時候使用 externalpublic任斋。

聲明函數 internalexternal 類型的語法,與聲明 privatepublic類 型相同:

contract Sandwich {
  uint private sandwichesEaten = 0;

  function eat() internal {
    sandwichesEaten++;
  }
}

contract BLT is Sandwich {
  uint private baconSandwichesEaten = 0;

  function eatWithBacon() public returns (string) {
    baconSandwichesEaten++;
    // 因為eat() 是internal 的耻涛,所以我們能在這里調用
    eat();
  }
}

實戰(zhàn)演習

  • _createZombie() 函數的屬性從 private 改為 internal 废酷, 使得其他的合約也能訪問到它。

我們已經成功把你的注意力集中在到zombiefactory.sol這個選項卡上啦抹缕。

pragma solidity ^0.4.19;

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 _name, uint _dna) internal {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    function createRandomZombie(string _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }

}

第10章: 僵尸吃什么?

是時候讓我們的僵尸去捕獵澈蟆! 那僵尸最喜歡的食物是什么呢?

Crypto 僵尸喜歡吃的是...

CryptoKitties歉嗓! ??????

(正經點丰介,我可不是開玩笑??)

為了做到這一點,我們要讀出 CryptoKitties 智能合約中的 kittyDna。這些數據是公開存儲在區(qū)塊鏈上的哮幢。區(qū)塊鏈是不是很酷带膀?

別擔心 —— 我們的游戲并不會傷害到任何真正的CryptoKitty。 我們只 讀取 CryptoKitties 數據橙垢,但卻無法在物理上刪除它垛叨。

與其他合約的交互

如果我們的合約需要和區(qū)塊鏈上的其他的合約會話,則需先定義一個 interface (接口)柜某。

先舉一個簡單的栗子嗽元。 假設在區(qū)塊鏈上有這么一個合約:

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

這是個很簡單的合約,您可以用它存儲自己的幸運號碼喂击,并將其與您的以太坊地址關聯剂癌。 這樣其他人就可以通過您的地址查找您的幸運號碼了。

現在假設我們有一個外部合約翰绊,使用 getNum 函數可讀取其中的數據佩谷。

首先,我們定義 LuckyNumber 合約的 interface

contract NumberInterface {
  function getNum(address _myAddress) public view returns (uint);
}

請注意监嗜,這個過程雖然看起來像在定義一個合約谐檀,但其實內里不同:

首先,我們只聲明了要與之交互的函數 —— 在本例中為 getNum —— 在其中我們沒有使用到任何其他的函數或狀態(tài)變量裁奇。

其次桐猬,我們并沒有使用大括號({})定義函數體,我們單單用分號(;)結束了函數聲明刽肠。這使它看起來像一個合約框架溃肪。

編譯器就是靠這些特征認出它是一個接口的。

在我們的 app 代碼中使用這個接口五垮,合約就知道其他合約的函數是怎樣的乍惊,應該如何調用杜秸,以及可期待什么類型的返回值放仗。

在下一課中,我們將真正調用其他合約的函數撬碟。目前我們只要聲明一個接口诞挨,用于調用 CryptoKitties 合約就行了。

實戰(zhàn)演習

我們已經為你查看過了 CryptoKitties 的源代碼呢蛤,并且找到了一個名為 getKitty的函數惶傻,它返回所有的加密貓的數據,包括它的“基因”(我們的僵尸游戲要用它生成新的僵尸)其障。

該函數如下所示:

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

這個函數看起來跟我們習慣的函數不太一樣银室。 它竟然返回了...一堆不同的值! 如果您用過 JavaScript 之類的編程語言,一定會感到奇怪 —— 在 Solidity中蜈敢,您可以讓一個函數返回多個值辜荠。

現在我們知道這個函數長什么樣的了,就可以用它來創(chuàng)建一個接口:

1.定義一個名為 KittyInterface 的接口抓狭。 請注意伯病,因為我們使用了 contract 關鍵字, 這過程看起來就像創(chuàng)建一個新的合約一樣否过。

2.在interface里定義了 getKitty 函數(不過是復制/粘貼上面的函數午笛,但在 returns 語句之后用分號,而不是大括號內的所有內容苗桂。

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 {

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

}

第11章: 使用接口

繼續(xù)前面 NumberInterface 的例子药磺,我們既然將接口定義為:

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`變量做些什么
  }
}

通過這種方式,只要將您合約的可見性設置為public(公共)或external(外部)煤伟,它們就可以與以太坊區(qū)塊鏈上的任何其他合約進行交互与涡。

實戰(zhàn)演習

我們來建個自己的合約去讀取另一個智能合約-- CryptoKitties 的內容吧!

  • 我已經將代碼中 CryptoKitties 合約的地址保存在一個名為 ckAddress 的變量中持偏。在下一行中驼卖,請創(chuàng)建一個名為 kittyContract 的 KittyInterface,并用 ckAddress 為它初始化 —— 就像我們?yōu)?numberContract所做的一樣鸿秆。
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 {

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

}

第12章: 處理多返回值

getKitty 是我們所看到的第一個返回多個值的函數酌畜。我們來看看是如何處理的:

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

// 或者如果我們只想返回其中一個變量:
function getLastReturnValue() external {
  uint c;
  // 可以對其他字段留空:
  (,,c) = multipleReturns();
}

實戰(zhàn)演習

是時候與 CryptoKitties 合約交互起來了!

我們來定義一個函數卿叽,從 kitty 合約中獲取它的基因:

  1. 創(chuàng)建一個名為 feedOnKitty 的函數桥胞。它需要2個 uint 類型的參數,_zombieId 和_kittyId 考婴,這是一個 public 類型的函數贩虾。

  2. 函數首先要聲明一個名為 kittyDna 的 uint。

注意:在我們的 KittyInterface 中沥阱,genes 是一個 uint256 類型的變量缎罢,但是如果你記得,我們在第一課中提到過考杉,uintuint256 的別名策精,也就是說它們是一回事。

  1. 這個函數接下來調用 kittyContract.getKitty函數, 傳入 _kittyId 崇棠,將返回的 genes 存儲在 kittyDna 中咽袜。記住 —— getKitty 會返回一大堆變量。 (確切地說10個 - 我已經為你數過了枕稀,不錯吧Q病)谜嫉。但是我們只關心最后一個-- genes。數逗號的時候小心點哦凹联!

  2. 最后骄恶,函數調用了 feedAndMultiply ,并傳入了 _zombieIdkittyDna 兩個參數匕垫。

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 {

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

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

}

第13章: 獎勵: Kitty 基因

我們的功能邏輯主體已經完成了...現在讓我們來添一個獎勵功能吧僧鲁。

這樣吧,給從小貓制造出的僵尸添加些特征象泵,以顯示他們是貓僵尸寞秃。

要做到這一點,咱們在新僵尸的DNA中添加一些特殊的小貓代碼偶惠。

還記得嗎春寿,第一課中我們提到,我們目前只使用16位DNA的前12位數來指定僵尸的外觀忽孽。所以現在我們可以使用最后2個數字來處理“特殊”的特征绑改。

這樣吧,把貓僵尸DNA的最后兩個數字設定為99(因為貓有9條命)兄一。所以在我們這么來寫代碼:如果這個僵尸是一只貓變來的厘线,就將它DNA的最后兩位數字設置為99

if 語句

if語句的語法在 Solidity 中出革,與在 JavaScript 中差不多:

function eatBLT(string sandwich) public {
  // 看清楚了造壮,當我們比較字符串的時候,需要比較他們的 keccak256 哈希碼
  if (keccak256(sandwich) == keccak256("BLT")) {
    eat();
  }
}

實戰(zhàn)演習

讓我們在我們的僵尸代碼中實現小貓的基因骂束。

  1. 首先耳璧,我們修改下 feedAndMultiply 函數的定義,給它傳入第三個參數:一條名為 _species 的字符串展箱。

  2. 接下來旨枯,在我們計算出新的僵尸的DNA之后,添加一個 if 語句來比較 _species 和字符串 "kitty"keccak256 哈希值混驰。

  3. if 語句中攀隔,我們用 99 替換了新僵尸DNA的最后兩位數字≌穗剩可以這么做:newDna = newDna - newDna%100 + 99;竞慢。

解釋:假設 newDna334455。那么 newDna%10055治泥,所以 newDna - newDna%100 得到 334400。最后加上 99 可得到 334499遮精。

  1. 最后居夹,我們修改了 feedOnKitty 中的函數調用败潦。當它調用 feedAndMultiply 時,增加 “kitty” 作為最后一個參數准脂。
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 {

  address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
  KittyInterface kittyContract = KittyInterface(ckAddress);

  function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    if(keccak256(_species) == keccak256("kitty")){
        newDna = newDna - newDna%100 + 99;
    }
    _createZombie("NoName", newDna);
  }

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

}

第14章: 放在一起

至此劫扒,你已經學完第二課了!

查看下→_→的演示狸膏,看看他們怎么運行起來得吧沟饥。繼續(xù),你肯定等不及看完這一頁??湾戳。點擊小貓贤旷,攻擊!看到你斬獲一個新的小貓僵尸了吧砾脑!

JavaScript 實現

我們只用編譯和部署 ZombieFeeding幼驶,就可以將這個合約部署到以太坊了。我們最終完成的這個合約繼承自 ZombieFactory韧衣,因此它可以訪問自己和父輩合約中的所有 public 方法盅藻。

我們來看一個與我們的剛部署的合約進行交互的例子, 這個例子使用了 JavaScript 和 web3.js:

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)

// 假設我們有我們的僵尸ID和要攻擊的貓咪ID
let zombieId = 1;
let kittyId = 1;

// 要拿到貓咪的DNA畅铭,我們需要調用它的API氏淑。這些數據保存在它們的服務器上而不是區(qū)塊鏈上。
// 如果一切都在區(qū)塊鏈上硕噩,我們就不用擔心它們的服務器掛了夸政,或者它們修改了API,
// 或者因為不喜歡我們的僵尸游戲而封殺了我們
let apiUrl = "https://api.cryptokitties.co/kitties/" + kittyId
$.get(apiUrl, function(data) {
  let imgUrl = data.image_url
  // 一些顯示圖片的代碼
})

// 當用戶點擊一只貓咪的時候:
$(".kittyImage").click(function(e) {
  // 調用我們合約的 `feedOnKitty` 函數
  ZombieFeeding.feedOnKitty(zombieId, kittyId)
})

// 偵聽來自我們合約的新僵尸事件好來處理
ZombieFactory.NewZombie(function(error, result) {
  if (error) return
  // 這個函數用來顯示僵尸:
  generateZombie(result.zombieId, result.name, result.dna)
})

實戰(zhàn)演習

選擇一只你想獵食的小貓榴徐。你自家僵尸的 DNA 會和小貓的 DNA 結合守问,生成一個新的小貓僵尸,加入你的軍團坑资!

看到新僵尸上那可愛的貓咪腿了么耗帕?這是新僵尸最后DNA中最后兩位數字 99 的功勞!

你想要的話隨時可以重新開始袱贮。捕獲了一只貓咪僵尸仿便,你一定很高興吧!(不過你只能持有一只)攒巍,繼續(xù)前進到下一章嗽仪,完成第二課吧!

示例

下一篇:以太坊Solidity開發(fā)入門(高級篇)

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末柒莉,一起剝皮案震驚了整個濱河市闻坚,隨后出現的幾起案子,更是在濱河造成了極大的恐慌兢孝,老刑警劉巖窿凤,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件仅偎,死亡現場離奇詭異,居然都是意外死亡雳殊,警方通過查閱死者的電腦和手機橘沥,發(fā)現死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來夯秃,“玉大人座咆,你說我怎么就攤上這事〔滞荩” “怎么了介陶?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長衬潦。 經常有香客問我斤蔓,道長,這世上最難降的妖魔是什么镀岛? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任弦牡,我火速辦了婚禮,結果婚禮上漂羊,老公的妹妹穿的比我還像新娘驾锰。我一直安慰自己,他們只是感情好走越,可當我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布椭豫。 她就那樣靜靜地躺著,像睡著了一般旨指。 火紅的嫁衣襯著肌膚如雪赏酥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天谆构,我揣著相機與錄音裸扶,去河邊找鬼。 笑死搬素,一個胖子當著我的面吹牛呵晨,可吹牛的內容都是我干的。 我是一名探鬼主播熬尺,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼摸屠,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了粱哼?” 一聲冷哼從身側響起季二,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎皂吮,沒想到半個月后戒傻,有當地人在樹林里發(fā)現了一具尸體税手,經...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡蜂筹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年需纳,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片艺挪。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡不翩,死狀恐怖,靈堂內的尸體忽然破棺而出麻裳,到底是詐尸還是另有隱情口蝠,我是刑警寧澤,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布津坑,位于F島的核電站妙蔗,受9級特大地震影響,放射性物質發(fā)生泄漏疆瑰。R本人自食惡果不足惜眉反,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望穆役。 院中可真熱鬧寸五,春花似錦、人聲如沸耿币。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽淹接。三九已至十性,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間塑悼,已是汗流浹背劲适。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拢肆,地道東北人减响。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親谭确。 傳聞我的和親對象是個殘疾皇子查邢,可洞房花燭夜當晚...
    茶點故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內容