以太坊開發(fā)實戰(zhàn)學習-高級Solidity理論 (五)

接上篇文章流妻,這里繼續(xù)學習Solidity高級理論牲蜀。

一、深入函數(shù)修飾符

接下來绅这,我們將添加一些輔助方法涣达。我們?yōu)槟鷦?chuàng)建了一個名為 zombiehelper.sol 的新文件,并且將 zombiefeeding.sol 導入其中君躺,這讓我們的代碼更整潔峭判。

我們打算讓僵尸在達到一定水平后,獲得特殊能力棕叫。但是達到這個小目標林螃,我們還需要學一學什么是“函數(shù)修飾符”。

帶參的函數(shù)修飾符

之前我們已經讀過一個簡單的函數(shù)修飾符了:onlyOwner俺泣。函數(shù)修飾符也可以帶參數(shù)疗认。例如:

// 存儲用戶年齡的映射
mapping (uint => uint) public age;

// 限定用戶年齡的修飾符
modifier olderThan(uint _age, uint _userId) {
  require(age[_userId] >= _age);
  _;
}

// 必須年滿16周歲才允許開車 (至少在美國是這樣的).
// 我們可以用如下參數(shù)調用`olderThan` 修飾符:
function driveCar(uint _userId) public olderThan(16, _userId) {
  // 其余的程序邏輯
}

看到了吧, olderThan 修飾符可以像函數(shù)一樣接收參數(shù)伏钠,是“宿主”函數(shù) driveCar 把參數(shù)傳遞給它的修飾符的横漏。

來,我們自己生產一個修飾符熟掂,通過傳入的level參數(shù)來限制僵尸使用某些特殊功能缎浇。

實戰(zhàn)演練

  • 1、在ZombieHelper 中赴肚,創(chuàng)建一個名為 aboveLevel 的modifier素跺,它接收2個參數(shù), _level (uint類型) 以及 _zombieId (uint類型)誉券。
  • 2指厌、運用函數(shù)邏輯確保僵尸 zombies[_zombieId].level 大于或等于 _level。
  • 3踊跟、記住踩验,修飾符的最后一行為 _;表示修飾符調用結束后返回商玫,并執(zhí)行調用函數(shù)余下的部分箕憾。
pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  // 在這里開始
  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

}

函數(shù)修飾符應用

現(xiàn)在讓我們設計一些使用 aboveLevel 修飾符的函數(shù)。

作為游戲拳昌,您得有一些措施激勵玩家們去升級他們的僵尸:

  • 2級以上的僵尸厕九,玩家可給他們改名。
  • 20級以上的僵尸地回,玩家能給他們定制的 DNA扁远。

是實現(xiàn)這些功能的時候了俊鱼。以下是上一課的示例代碼,供參考:

// 存儲用戶年齡的映射
mapping (uint => uint) public age;

// 限定用戶年齡的修飾符
modifier olderThan(uint _age, uint _userId) {
  require (age[_userId] >= _age);
  _;
}

// 必須年滿16周歲才允許開車 (至少在美國是這樣的).
// 我們可以用如下參數(shù)調用`olderThan` 修飾符:
function driveCar(uint _userId) public olderThan(16, _userId) {
  // 其余的程序邏輯
}

實戰(zhàn)演練

  • 1畅买、創(chuàng)建一個名為 changeName 的函數(shù)并闲。它接收2個參數(shù):_zombieId(uint類型)以及 _newName(string類型),可見性為 external谷羞。它帶有一個 aboveLevel 修飾符帝火,調用的時候通過 _level 參數(shù)傳入2, 當然湃缎,別忘了同時傳 _zombieId 參數(shù)犀填。
  • 2、在這個函數(shù)中嗓违,首先我們用 require 語句九巡,驗證 msg.sender 是否就是 zombieToOwner [_zombieId]
  • 3蹂季、然后函數(shù)將 zombies[_zombieId] .name 設置為 _newName冕广。
  • 4、在 changeName 下創(chuàng)建另一個名為 changeDna 的函數(shù)偿洁。它的定義和內容幾乎和 changeName 相同撒汉,不過它第二個參數(shù)是 _newDna(uint類型),在修飾符 aboveLevel 的 _level 參數(shù)中傳遞 20 √樽蹋現(xiàn)在睬辐,他可以把僵尸的 dna 設置為 _newDna 了。

zombiehelper.sol

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  // 在這里開始
  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;

  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;

  }

}

二宾肺、利用view節(jié)省Gas

現(xiàn)在需要添加的一個功能是:我們的 DApp 需要一個方法來查看某玩家的整個僵尸軍團 - 我們稱之為 getZombiesByOwner溯饵。

實現(xiàn)這個功能只需從區(qū)塊鏈中讀取數(shù)據(jù),所以它可以是一個 view 函數(shù)爱榕。這讓我們不得不回顧一下“gas優(yōu)化”這個重要話題瓣喊。

“view” 函數(shù)不花 “gas”

當玩家從外部調用一個view函數(shù)坡慌,是不需要支付一分 gas 的黔酥。

這是因為 view 函數(shù)不會真正改變區(qū)塊鏈上的任何數(shù)據(jù) - 它們只是讀取。因此用 view 標記一個函數(shù)洪橘,意味著告訴 web3.js跪者,運行這個函數(shù)只需要查詢你的本地以太坊節(jié)點,而不需要在區(qū)塊鏈上創(chuàng)建一個事務(事務需要運行在每個節(jié)點上熄求,因此花費 gas)渣玲。

稍后我們將介紹如何在自己的節(jié)點上設置 web3.js。但現(xiàn)在弟晚,你關鍵是要記住忘衍,在所能只讀的函數(shù)上標記上表示“只讀”的external view 聲明逾苫,就能為你的玩家減少在 DApp 中 gas 用量。

注意:如果一個 view 函數(shù)在另一個函數(shù)的內部被調用枚钓,而調用函數(shù)與 view 函數(shù)的不屬于同一個合約铅搓,也會產生調用成本。這是因為如果主調函數(shù)在以太坊創(chuàng)建了一個事務搀捷,它仍然需要逐個節(jié)點去驗證星掰。所以標記為 view 的函數(shù)只有在外部調用時才是免費的。

實戰(zhàn)演練

我們來寫一個”返回某玩家的整個僵尸軍團“的函數(shù)嫩舟。當我們從 web3.js 中調用它氢烘,即可顯示某一玩家的個人資料頁。

這個函數(shù)的邏輯有點復雜家厌,我們需要好幾個章節(jié)來描述它的實現(xiàn)播玖。

  • 1、創(chuàng)建一個名為 getZombiesByOwner 的新函數(shù)像街。它有一個名為 _owneraddress 類型的參數(shù)黎棠。
  • 2、將其申明為 external view 函數(shù)镰绎,這樣當玩家從 web3.js 中調用它時脓斩,不需要花費任何 gas。
  • 3畴栖、函數(shù)需要返回一個uint []uint數(shù)組)随静。

先這么聲明著,我們將在下一章中填充函數(shù)體吗讶。
zombiehelper.sol

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;
  }

  // 在這里創(chuàng)建你的函數(shù)
  function getZombiesByOwner (address _owner) external view returns (uint []) {

  }

}

三燎猛、存儲非常昂貴

Solidity 使用 storage(存儲)是相當昂貴的,”寫入“操作尤其貴照皆。

這是因為重绷,無論是寫入還是更改一段數(shù)據(jù), 這都將永久性地寫入區(qū)塊鏈膜毁≌炎浚”永久性“啊瘟滨!需要在全球數(shù)千個節(jié)點的硬盤上存入這些數(shù)據(jù)候醒,隨著區(qū)塊鏈的增長,拷貝份數(shù)更多杂瘸,存儲量也就越大倒淫。這是需要成本的!

為了降低成本败玉,不到萬不得已敌土,避免將數(shù)據(jù)寫入存儲镜硕。這也會導致效率低下的編程邏輯 - 比如每次調用一個函數(shù),都需要在 memory(內存) 中重建一個數(shù)組返干,而不是簡單地將上次計算的數(shù)組給存儲下來以便快速查找谦疾。

在大多數(shù)編程語言中,遍歷大數(shù)據(jù)集合都是昂貴的犬金。但是在 Solidity 中念恍,使用一個標記了external view的函數(shù),遍歷比 storage 要便宜太多晚顷,因為 view 函數(shù)不會產生任何花銷峰伙。 (gas可是真金白銀啊8媚)瞳氓。

我們將在下一章討論 for 循環(huán),現(xiàn)在我們來看一下看如何如何在內存中聲明數(shù)組栓袖。

在內存中聲明數(shù)組

在數(shù)組后面加上 memory 關鍵字匣摘, 表明這個數(shù)組是僅僅在內存中創(chuàng)建,不需要寫入外部存儲裹刮,并且在函數(shù)調用結束時它就解散了音榜。與在程序結束時把數(shù)據(jù)保存進 storage 的做法相比,內存運算可以大大節(jié)省gas開銷 -- 把這數(shù)組放在view里用捧弃,完全不用花錢赠叼。

以下是申明一個內存數(shù)組的例子:

function getArray() external pure returns(uint[]) {
  // 初始化一個長度為3的內存數(shù)組
  uint[] memory values = new uint[](3);
  // 賦值
  values.push(1);
  values.push(2);
  values.push(3);
  // 返回數(shù)組
  return values;
}

這個小例子展示了一些語法規(guī)則,下一章中违霞,我們將通過一個實際用例嘴办,展示它和 for 循環(huán)結合的做法。

注意:內存數(shù)組 必須 用長度參數(shù)(在本例中為3)創(chuàng)建买鸽。目前不支持 array.push()之類的方法調整數(shù)組大小涧郊,在未來的版本可能會支持長度修改。

實戰(zhàn)演練

我們要要創(chuàng)建一個名為 getZombiesByOwner 的函數(shù)眼五,它以uint []數(shù)組的形式返回某一用戶所擁有的所有僵尸妆艘。

  • 1、聲明一個名為resultuint [] memory (內存變量數(shù)組)
  • 2弹砚、將其設置為一個新的 uint 類型數(shù)組双仍。數(shù)組的長度為該 _owner 所擁有的僵尸數(shù)量枢希,這可通過調用 ownerZombieCount [_ owner] 來獲取桌吃。
  • 3、函數(shù)結束苞轿,返回 result 茅诱。目前它只是個空數(shù)列逗物,我們到下一章去實現(xiàn)它。

zombiehelper.sol

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;
  }

  function getZombiesByOwner(address _owner) external view returns(uint[]) {
    // 在這里開始
    uint[] memory result = new uint[](ownerZombieCount[_ owner]);

    return result;
  }

}

四瑟俭、For循環(huán)

在之前的博文中翎卓,我們提到過,函數(shù)中使用的數(shù)組是運行時在內存中通過 for 循環(huán)實時構建摆寄,而不是預先建立在存儲中的失暴。

為什么要這樣做呢?

為了實現(xiàn) getZombiesByOwner 函數(shù)微饥,一種“無腦式”的解決方案是在 ZombieFactory 中存入”主人“和”僵尸軍團“的映射逗扒。

mapping (address => uint[]) public ownerToZombies

然后我們每次創(chuàng)建新僵尸時朝刊,執(zhí)行 ownerToZombies[owner].push(zombieId) 將其添加到主人的僵尸數(shù)組中憔维。而 getZombiesByOwner 函數(shù)也非常簡單:

function getZombiesByOwner(address _owner) external view returns (uint[]) {
  return ownerToZombies[_owner];
}

這個做法有問題

做法倒是簡單父腕】撸可是如果我們需要一個函數(shù)來把一頭僵尸轉移到另一個主人名下(我們一定會在后面的課程中實現(xiàn)的)翰守,又會發(fā)生什么商叹?

這個“換主”函數(shù)要做到:

  • 1.將僵尸push到新主人的 ownerToZombies 數(shù)組中估蹄,
  • 2.從舊主的 ownerToZombies 數(shù)組中移除僵尸次绘,
  • 3.將舊主僵尸數(shù)組中“換主僵尸”之后的的每頭僵尸都往前挪一位始锚,把挪走“換主僵尸”后留下的“空槽”填上刽酱,
  • 4.將數(shù)組長度減1。

但是第三步實在是太貴了瞧捌!因為每挪動一頭僵尸肛跌,我們都要執(zhí)行一次寫操作。如果一個主人有20頭僵尸察郁,而第一頭被挪走了衍慎,那為了保持數(shù)組的順序,我們得做19個寫操作皮钠。

由于寫入存儲是 Solidity 中最費 gas 的操作之一稳捆,使得換主函數(shù)的每次調用都非常昂貴。更糟糕的是麦轰,每次調用的時候花費的 gas 都不同乔夯!具體還取決于用戶在原主軍團中的僵尸頭數(shù),以及移走的僵尸所在的位置款侵。以至于用戶都不知道應該支付多少 gas末荐。

注意:當然,我們也可以把數(shù)組中最后一個僵尸往前挪來填補空槽新锈,并將數(shù)組長度減少一甲脏。但這樣每做一筆交易,都會改變僵尸軍團的秩序。

由于從外部調用一個 view 函數(shù)是免費的块请,我們也可以在 getZombiesByOwner 函數(shù)中用一個for循環(huán)遍歷整個僵尸數(shù)組娜氏,把屬于某個主人的僵尸挑出來構建出僵尸數(shù)組。那么我們的 transfer 函數(shù)將會便宜得多墩新,因為我們不需要挪動存儲里的僵尸數(shù)組重新排序贸弥,總體上這個方法會更便宜,雖然有點反直覺海渊。

使用for循環(huán)

for循環(huán)的語法在 Solidity 和 JavaScript 中類似绵疲。

來看一個創(chuàng)建偶數(shù)數(shù)組的例子:

function getEvens() pure external returns(uint[]) {
  uint[] memory evens = new uint[](5);
  // 在新數(shù)組中記錄序列號
  uint counter = 0;
  // 在循環(huán)從1迭代到10:
  for (uint i = 1; i <= 10; i++) {
    // 如果 `i` 是偶數(shù)...
    if (i % 2 == 0) {
      // 把它加入偶數(shù)數(shù)組
      evens[counter] = i;
      //索引加一, 指向下一個空的‘even’
      counter++;
    }
  }
  return evens;
}

這個函數(shù)將返回一個形為 [2,4,6,8,10] 的數(shù)組臣疑。

實戰(zhàn)演練

我們回到 getZombiesByOwner 函數(shù)最岗, 通過一條 for 循環(huán)來遍歷 DApp 中所有的僵尸, 將給定的‘用戶id'與每頭僵尸的‘主人’進行比較朝捆,并在函數(shù)返回之前將它們推送到我們的result 數(shù)組中般渡。

  • 1.聲明一個變量 counter,屬性為 uint芙盘,設其值為 0 驯用。我們用這個變量作為 result 數(shù)組的索引。
  • 2.聲明一個 for 循環(huán)儒老, 從 uint i = 0 到 i <zombies.length蝴乔。它將遍歷數(shù)組中的每一頭僵尸。
  • 3.在每一輪 for 循環(huán)中驮樊,用一個 if 語句來檢查 zombieToOwner [i] 是否等于 _owner薇正。這會比較兩個地址是否匹配。
  • 4.在 if 語句中:
  • 通過將 result [counter] 設置為 i囚衔,將僵尸ID添加到 result 數(shù)組中挖腰。
  • 將counter加1(參見上面的for循環(huán)示例)。

就是這樣 - 這個函數(shù)能返回 _owner 所擁有的僵尸數(shù)組练湿,不花一分錢 gas猴仑。

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;
  }

  function getZombiesByOwner(address _owner) external view returns(uint[]) {
    uint[] memory result = new uint[](ownerZombieCount[_owner]);

    // 在這里開始
    uint counter = 0;
    for(uint i = 0; i < zombies.length; i++) {
      if(zombieToOwner[i] == _owner)
      {
        result[counter] = i;
        counter ++;

      }

    }

    return result;
  }

}

五、可支付

截至目前肥哎,我們只接觸到很少的 函數(shù)修飾符辽俗。 要記住所有的東西很難,所以我們來個概覽:

  • 1篡诽、我們有決定函數(shù)何時和被誰調用的可見性修飾符: private 意味著它只能被合約內部調用崖飘; internal 就像 private 但是也能被繼承的合約調用external 只能從合約外部調用杈女;最后 public 可以在任何地方調用朱浴,不管是內部還是外部吊圾。
  • 2、我們也有狀態(tài)修飾符赊琳, 告訴我們函數(shù)如何和區(qū)塊鏈交互: view 告訴我們運行這個函數(shù)不會更改和保存任何數(shù)據(jù); pure 告訴我們這個函數(shù)不但不會往區(qū)塊鏈寫數(shù)據(jù)砰碴,它甚至不從區(qū)塊鏈讀取數(shù)據(jù)躏筏。這兩種在被從合約外部調用的時候都不花費任何gas(但是它們在被內部其他函數(shù)調用的時候將會耗費gas)。
  • 3呈枉、然后我們有了自定義的 modifiers趁尼,例如在第三課學習的: onlyOwneraboveLevel。 對于這些修飾符我們可以自定義其對函數(shù)的約束邏輯猖辫。

這些修飾符可以同時作用于一個函數(shù)定義上:

function test() external view onlyOwner anotherModifier { /* ... */ }

在這一章酥泞,我們來學習一個新的修飾符 payable.

payable修飾符

payable 方法是讓 Solidity 和以太坊變得如此酷的一部分 —— 它們是一種可以接收以太的特殊函數(shù)。

先放一下啃憎。當你在調用一個普通網站服務器上的API函數(shù)的時候芝囤,你無法用你的函數(shù)傳送美元——你也不能傳送比特幣。

但是在以太坊中辛萍, 因為錢 (以太), 數(shù)據(jù) (事務負載)悯姊, 以及合約代碼本身都存在于以太坊。你可以在同時調用函數(shù) 并付錢給另外一個合約贩毕。

這就允許出現(xiàn)很多有趣的邏輯悯许, 比如向一個合約要求支付一定的錢來運行一個函數(shù)。

示例

contract OnlineStore {
  function buySomething() external payable {
    // 檢查以確定0.001以太發(fā)送出去來運行函數(shù):
    require(msg.value == 0.001 ether);
    // 如果為真辉阶,一些用來向函數(shù)調用者發(fā)送數(shù)字內容的邏輯
    transferThing(msg.sender);
  }
}

在這里先壕,msg.value 是一種可以查看向合約發(fā)送了多少以太的方法,另外 ether 是一個內建單元谆甜。

這里發(fā)生的事是垃僚,一些人會從 web3.js 調用這個函數(shù) (從DApp的前端), 像這樣 :

// 假設 `OnlineStore` 在以太坊上指向你的合約:
OnlineStore.buySomething().send(from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001))

注意這個 value 字段规辱, JavaScript 調用來指定發(fā)送多少(0.001)以太冈在。如果把事務想象成一個信封,你發(fā)送到函數(shù)的參數(shù)就是信的內容按摘。 添加一個 value 很像在信封里面放錢 —— 信件內容和錢同時發(fā)送給了接收者包券。

注意: 如果一個函數(shù)沒標記為payable, 而你嘗試利用上面的方法發(fā)送以太炫贤,函數(shù)將拒絕你的事務溅固。

實戰(zhàn)演練

我們來在僵尸游戲里面創(chuàng)建一個payable 函數(shù)。

假定在我們的游戲中兰珍,玩家可以通過支付ETH來升級他們的僵尸侍郭。ETH將存儲在你擁有的合約中 —— 一個簡單明了的例子,向你展示你可以通過自己的游戲賺錢。

  • 1亮元、定義一個 uint 猛计,命名為 levelUpFee, 將值設定為 0.001 ether
  • 2爆捞、定義一個名為 levelUp 的函數(shù)奉瘤。 它將接收一個 uint 參數(shù) _zombieId。 函數(shù)應該修飾為 external 以及 payable煮甥。
  • 3盗温、這個函數(shù)首先應該 require 確保 msg.value 等于 levelUpFee

然后它應該增加僵尸的 level: zombies[_zombieId].level++成肘。

zombiehelper.sol

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  // 1\. 在這里定義 levelUpFee
  uint levelUpFee = 0.001 ether;

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  // 2\. 在這里插入 levelUp 函數(shù) 
  function levelUp(uint _zombieId) external payable {
    // 檢查以確定0.001以太發(fā)送出去來運行函數(shù):
    require(msg.value == levelUpFee);

    zombies[_zombieId].level++;
  }

  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;
  }

  function getZombiesByOwner(address _owner) external view returns(uint[]) {
    uint[] memory result = new uint[](ownerZombieCount[_owner]);
    uint counter = 0;
    for (uint i = 0; i < zombies.length; i++) {
      if (zombieToOwner[i] == _owner) {
        result[counter] = i;
        counter++;
      }
    }
    return result;
  }

}

六卖局、提現(xiàn)

在上一節(jié),我們學習了如何向合約發(fā)送以太双霍,那么在發(fā)送之后會發(fā)生什么呢砚偶?

在你發(fā)送以太之后,它將被存儲進以合約的以太坊賬戶中洒闸, 并凍結在哪里 —— 除非你添加一個函數(shù)來從合約中把以太提現(xiàn)蟹演。

你可以寫一個函數(shù)來從合約中提現(xiàn)以太,類似這樣:

contract GetPaid is Ownable {
  function withdraw() external onlyOwner {
    owner.transfer(this.balance);
  }
}

注意我們使用 Ownable 合約中的 owneronlyOwner顷蟀,假定它已經被引入了酒请。

你可以通過 transfer 函數(shù)向一個地址發(fā)送以太, 然后 this.balance 將返回當前合約存儲了多少以太鸣个。 所以如果100個用戶每人向我們支付1以太羞反, this.balance 將是100以太。

你可以通過 transfer 向任何以太坊地址付錢囤萤。 比如昼窗,你可以有一個函數(shù)在 msg.sender 超額付款的時候給他們退錢:

uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);

或者在一個有賣家和賣家的合約中, 你可以把賣家的地址存儲起來涛舍, 當有人買了它的東西的時候澄惊,把買家支付的錢發(fā)送給它 seller.transfer(msg.value)

有很多例子來展示什么讓以太坊編程如此之酷 —— 你可以擁有一個不被任何人控制的去中心化市場富雅。

實戰(zhàn)演練

  • 1掸驱、在我們的合約里創(chuàng)建一個 withdraw 函數(shù),它應該幾乎和上面的GetPaid一樣没佑。
  • 2毕贼、以太的價格在過去幾年內翻了十幾倍,在我們寫這個教程的時候 0.01 以太相當于1美元蛤奢,如果它再翻十倍 0.001 以太將是10美元鬼癣,那我們的游戲就太貴了陶贼。
  • 所以我們應該再創(chuàng)建一個函數(shù),允許我們以合約擁有者的身份來設置 levelUpFee待秃。

a. 創(chuàng)建一個函數(shù)拜秧,名為 setLevelUpFee, 其接收一個參數(shù) uint _fee章郁,是 external 并使用修飾符 onlyOwner枉氮。

b. 這個函數(shù)應該設置 levelUpFee 等于 _fee

zombiehelper.sol

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  uint levelUpFee = 0.001 ether;

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  // 1\. 在這里創(chuàng)建 withdraw 函數(shù)
  function withdraw() external onlyOwner {
    owner.transfer(this.balance);
  }

  // 2\. 在這里創(chuàng)建 setLevelUpFee 函數(shù) 
  function setLevelUpFee(uint _fee) external onlyOwner {
    levelUpFee = _fee;
  }

  function levelUp(uint _zombieId) external payable {
    require(msg.value == levelUpFee);
    zombies[_zombieId].level++;
  }

  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;
  }

  function getZombiesByOwner(address _owner) external view returns(uint[]) {
    uint[] memory result = new uint[](ownerZombieCount[_owner]);
    uint counter = 0;
    for (uint i = 0; i < zombies.length; i++) {
      if (zombieToOwner[i] == _owner) {
        result[counter] = i;
        counter++;
      }
    }
    return result;
  }

}

七驱犹、綜合應用

我們新建一個攻擊功能合約嘲恍,并將代碼放進新的文件中足画,引入上一個合約雄驹。

再來新建一個合約吧。熟能生巧淹辞。

如果你不記得怎么做了, 查看一下 zombiehelper.sol — 不過最好先試著做一下医舆,檢查一下你掌握的情況。

  • 1象缀、在文件開頭定義 Solidity 的版本 ^0.4.19.
  • 2蔬将、importzombiehelper.sol .
  • 3、聲明一個新的 contract央星,命名為 ZombieBattle霞怀, 繼承自ZombieHelper。函數(shù)體就先空著吧莉给。

zombiebattle.sol

pragma solidity ^0.4.19;
import "./zombiehelper.sol";

contract ZombieBattle is ZombieHelper {

}

八毙石、隨機數(shù)

優(yōu)秀的游戲都需要一些隨機元素,那么我們在 Solidity 里如何生成隨機數(shù)呢颓遏?

真正的答案是你不能徐矩,或者最起碼,你無法安全地做到這一點叁幢。

我們來看看為什么

keccak256 來制造隨機數(shù)
Solidity 中最好的隨機數(shù)生成器是 keccak256 哈希函數(shù).

我們可以這樣來生成一些隨機數(shù)

// 生成一個0到100的隨機數(shù):
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;

這個方法首先拿到 now 的時間戳滤灯、 msg.sender、 以及一個自增數(shù) nonce (一個僅會被使用一次的數(shù)曼玩,這樣我們就不會對相同的輸入值調用一次以上哈希函數(shù)了)鳞骤。

然后利用 keccak 把輸入的值轉變?yōu)橐粋€哈希值, 再將哈希值轉換為 uint, 然后利用 % 100 來取最后兩位, 就生成了一個0到100之間隨機數(shù)了。

這個方法很容易被不誠實的節(jié)點攻擊
在以太坊上, 當你在和一個合約上調用函數(shù)的時候, 你會把它廣播給一個節(jié)點或者在網絡上的 transaction 節(jié)點們黍判。 網絡上的節(jié)點將收集很多事務, 試著成為第一個解決計算密集型數(shù)學問題的人弟孟,作為“工作證明”,然后將“工作證明”(Proof of Work, PoW)和事務一起作為一個 block 發(fā)布在網絡上样悟。

一旦一個節(jié)點解決了一個PoW, 其他節(jié)點就會停止嘗試解決這個 PoW, 并驗證其他節(jié)點的事務列表是有效的拂募,然后接受這個節(jié)點轉而嘗試解決下一個節(jié)點庭猩。

這就讓我們的隨機數(shù)函數(shù)變得可利用了

我們假設我們有一個硬幣翻轉合約——正面你贏雙倍錢,反面你輸?shù)羲械腻X陈症。假如它使用上面的方法來決定是正面還是反面 (random >= 50 算正面, random < 50 算反面)蔼水。

如果我正運行一個節(jié)點,我可以 只對我自己的節(jié)點 發(fā)布一個事務录肯,且不分享它趴腋。 我可以運行硬幣翻轉方法來偷窺我的輸贏 — 如果我輸了,我就不把這個事務包含進我要解決的下一個區(qū)塊中去论咏。我可以一直運行這個方法优炬,直到我贏得了硬幣翻轉并解決了下一個區(qū)塊,然后獲利厅贪。

所以我們該如何在以太坊上安全地生成隨機數(shù)呢 蠢护?

因為區(qū)塊鏈的全部內容對所有參與者來說是透明的, 這就讓這個問題變得很難养涮,它的解決方法不在本課程討論范圍葵硕,你可以閱讀 這個 StackOverflow 上的討論 來獲得一些主意。 一個方法是利用 oracle 來訪問以太坊區(qū)塊鏈之外的隨機數(shù)函數(shù)贯吓。

當然懈凹, 因為網絡上成千上萬的以太坊節(jié)點都在競爭解決下一個區(qū)塊,我能成功解決下一個區(qū)塊的幾率非常之低悄谐。 這將花費我們巨大的計算資源來開發(fā)這個獲利方法 — 但是如果獎勵異常地高(比如我可以在硬幣翻轉函數(shù)中贏得 1個億)介评, 那就很值得去攻擊了。

所以盡管這個方法在以太坊上不安全爬舰,在實際中们陆,除非我們的隨機函數(shù)有一大筆錢在上面,你游戲的用戶一般是沒有足夠的資源去攻擊的洼专。

因為在這個教程中棒掠,我們只是在編寫一個簡單的游戲來做演示,也沒有真正的錢在里面屁商,所以我們決定接受這個不足之處烟很,使用這個簡單的隨機數(shù)生成函數(shù)。但是要謹記它是不安全的蜡镶。

實戰(zhàn)演練

我們來實現(xiàn)一個隨機數(shù)生成函數(shù)雾袱,好來計算戰(zhàn)斗的結果。雖然這個函數(shù)一點兒也不安全官还。

  • 1芹橡、給我們合約一個名為 randNonceuint,將其值設置為 0望伦。
  • 2林说、建立一個函數(shù)煎殷,命名為 randMod (random-modulus)。它將作為internal 函數(shù)腿箩,傳入一個名為 _modulus的 uint豪直,并 returns 一個 uint
  • 3珠移、這個函數(shù)首先將為 randNonce加一弓乙, (使用 randNonce++ 語句)。
  • 4钧惧、最后暇韧,它應該 (在一行代碼中) 計算 now, msg.sender, 以及 randNonce 的 keccak256 哈希值并轉換為 uint—— 最后 return % _modulus 的值。 (天! 聽起來太拗口了浓瞪。如果你有點理解不過來懈玻,看一下我們上面計算隨機數(shù)的例子,它們的邏輯非常相似)

zombiehelper.sol

pragma solidity ^0.4.19;

import "./zombiehelper.sol";

contract ZombieBattle is ZombieHelper {
  // 在這里開始
  uint randNonce = 0;

  function randMod(uint _modulus) internal returns (uint) {

    randNonce ++;
    return uint(keccak256(now, msg.sender, randNonce)) % _modulus;

  }
}

九追逮、游戲對戰(zhàn)

我們的合約已經有了一些隨機性的來源酪刀,可以用進我們的僵尸戰(zhàn)斗中去計算結果粹舵。

我們的僵尸戰(zhàn)斗看起來將是這個流程:

  • 你選擇一個自己的僵尸钮孵,然后選擇一個對手的僵尸去攻擊。
  • 如果你是攻擊方眼滤,你將有70%的幾率獲勝巴席,防守方將有30%的幾率獲勝。
  • 所有的僵尸(攻守雙方)都將有一個 winCount 和一個 lossCount诅需,這兩個值都將根據(jù)戰(zhàn)斗結果增長漾唉。
  • 若攻擊方獲勝,這個僵尸將升級并產生一個新僵尸堰塌。
  • 如果攻擊方失敗赵刑,除了失敗次數(shù)將加一外,什么都不會發(fā)生场刑。
  • 無論輸贏般此,當前僵尸的冷卻時間都將被激活。

這有一大堆的邏輯需要處理牵现,我們將把這些步驟分解到接下來的課程中去铐懊。

實戰(zhàn)演練

  • 1、給我們合約一個 uint 類型的變量瞎疼,命名為 attackVictoryProbability, 將其值設定為 70科乎。
  • 2、創(chuàng)建一個名為 attack的函數(shù)贼急。它將傳入兩個參數(shù): _zombieId (uint 類型) 以及 _targetId (也是 uint)茅茂。它將是一個 external 函數(shù)捏萍。

zombiehelper.sol

pragma solidity ^0.4.19;

import "./zombiehelper.sol";

contract ZombieBattle is ZombieHelper {
  uint randNonce = 0;
  // 在這里創(chuàng)建 attackVictoryProbability
  uint attackVictoryProbability = 70;

  function randMod(uint _modulus) internal returns(uint) {
    randNonce++;
    return uint(keccak256(now, msg.sender, randNonce)) % _modulus;
  }

  // 在這里創(chuàng)建新函數(shù)
  function attack(uint _zombieId, uint _targetId) external {

  }
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市空闲,隨后出現(xiàn)的幾起案子照弥,更是在濱河造成了極大的恐慌,老刑警劉巖进副,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件这揣,死亡現(xiàn)場離奇詭異,居然都是意外死亡影斑,警方通過查閱死者的電腦和手機给赞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來矫户,“玉大人片迅,你說我怎么就攤上這事〗粤桑” “怎么了柑蛇?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長驱闷。 經常有香客問我耻台,道長,這世上最難降的妖魔是什么空另? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任盆耽,我火速辦了婚禮,結果婚禮上扼菠,老公的妹妹穿的比我還像新娘摄杂。我一直安慰自己,他們只是感情好循榆,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布析恢。 她就那樣靜靜地躺著,像睡著了一般秧饮。 火紅的嫁衣襯著肌膚如雪映挂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天浦楣,我揣著相機與錄音袖肥,去河邊找鬼。 笑死振劳,一個胖子當著我的面吹牛椎组,可吹牛的內容都是我干的。 我是一名探鬼主播历恐,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼寸癌,長吁一口氣:“原來是場噩夢啊……” “哼专筷!你這毒婦竟也來了?” 一聲冷哼從身側響起蒸苇,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤磷蛹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后溪烤,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體味咳,經...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年檬嘀,在試婚紗的時候發(fā)現(xiàn)自己被綠了槽驶。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡鸳兽,死狀恐怖掂铐,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情揍异,我是刑警寧澤全陨,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站衷掷,受9級特大地震影響辱姨,放射性物質發(fā)生泄漏。R本人自食惡果不足惜棍鳖,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一炮叶、第九天 我趴在偏房一處隱蔽的房頂上張望碗旅。 院中可真熱鬧渡处,春花似錦、人聲如沸祟辟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽旧困。三九已至醇份,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間吼具,已是汗流浹背僚纷。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拗盒,地道東北人怖竭。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像陡蝇,于是被迫代替她去往敵國和親痊臭。 傳聞我的和親對象是個殘疾皇子哮肚,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354