區塊鏈上程式設計:DApp 開發實戰——來寫個競猜遊戲吧!

創宇前端發表於2018-10-10

本文旨在引導對 DApp 開發感興趣的開發者,構建一個基於以太坊去中心化應用,通過開發一款功能完備的競猜遊戲,邁出 DApp 開發的第一步,通過例項講解 Solidity 語言的常用語法,以及前端如何與智慧合約進行互動。

如果正在閱讀的你,從未接觸過 DApp 開發也不要緊,可以先閱讀【區塊鏈上程式設計:DApp開發簡介】進行前置知識補充。


隨著加密貓、FOMO3D 等遊戲的火爆,去中心化應用在遊戲領域遍地開花,下面就帶著大家一起開發一款簡單有趣的 DApp 遊戲,幫助大家熟悉 DApp 開發。本 DApp 實現的合約功能:使用者從 0-6 的數字中,任意組合三位數進行投注,合約計算出 3 位隨機數,根據隨機數的組合規則分別給予使用者不同倍數的獎勵,如隨機數為 AAA ,A 等於 6 則獎勵至少 20 倍投注金額,即獎池所有餘額;A 不等於 6 則獎勵 5 倍投注金額;隨機數為 AAB,則獎勵 2 倍投注金額;隨機數為 ABC 則不獎勵,同時使用者可檢視獎池餘額和個人投注記錄。

合約編寫

可以看出合約需要實現使用者投注、生成隨機數、發放獎勵、獎池餘額查詢的功能,接下來編寫我們的合約程式碼。

新建Lottery.sol合約檔案,宣告合約版本,^表示合約編譯版本為 0.4.0 至 0.5.0(不含 0.5.0)。

pragma solidity ^0.4.0;
複製程式碼

定義合約基本內容,同時宣告最低投注金額。

contract Lottery {
  uint public betMoney = 10 finney;
}
複製程式碼

生成隨機數,通過區塊難度block.difficulty和內建函式keccak256生成隨機數,在EVM中常用的資料儲存位置:memorystorage,函式的引數、返回值預設儲存在memory中,狀態變數預設儲存在storage中,我們可以通過宣告memorystorage改變預設儲存位置,兩者的儲存都需要消耗gas,但storage的開銷遠大於memory

contract Lottery {
  ...
  function generateRandomNumber() private view returns(uint[]) {
    uint[] memory dices = new uint[](3);
    for (uint i = 0; i < 3; i++) {
      dices[i] = uint(keccak256(abi.encodePacked(block.difficulty, now, i))) % 6 + 1;
    }
    return dices;
  }
  ...
}
複製程式碼

獲取合約餘額,address型別比較重要的成員屬性主要有balancetransfer,分別為獲取地址餘額、轉移eth至該地址,預設eth單位是wei

contract Lottery {
  ...
  function getBankBalance() public view returns(uint) {
    return address(this).balance;
  }
  ...
}
複製程式碼

使用者投注,投注方法需要使用payable關鍵字描述,用來表示可以接收eth;通過msg.sendermsg.value獲得交易傳送者地址和當前交易附帶的eth。通常使用require來校驗外部輸入引數,當判定條件為false時,則會將剩餘的gas退回,同時回滾交易;assert則用來處理合約內部的邏輯錯誤,當錯誤發生時會消耗掉所有gas,同時回滾交易。

contract Lottery {
  ...
  function bet() public payable {
    uint amount = msg.value;
    require(amount >= betMoney, 'bet money not less than 10 finney');
    require(address(this).balance >= amount * 20, 'contract money not enough to pay reward');

    uint[] memory dices = generateRandomNumber();
    require(dices.length == 3, 'dices illegal');

    address addr = msg.sender;
    bool isReward = false;
    uint reward = 0;

    if ((dices[0] == dices[1]) && (dices[1] == dices[2]) && (dices[0] == 6)) {
      isReward = true;
      reward = address(this).balance;
    } else if ((dices[0] == dices[1]) && (dices[1] == dices[2]) && (dices[0] != 6)) {
      isReward = true;
      reward = amount * 5;
    } else if ((dices[0] == dices[1]) || (dices[0] == dices[2]) || (dices[1] == dices[2])) {
      isReward = true;
      reward = amount * 2;
    }

    if (isReward) {
      addr.transfer(reward);
    }
  }
  ...
複製程式碼

定義事件,通過合約內部觸發事件,web3 監聽到事件回撥進行相應邏輯處理,從而進行頁面 UI 更新;同時前端也可以通過對應事件名稱獲取日誌資訊。

contract Lottery {
  ...
  event BetList(
    address addr,
    uint amount,
    bool isReward,
    uint reward,
    uint[] dices
  );

  function bet() public payable {
    ...
    emit BetList(addr, amount, isReward, reward, dices);
  }
  ...
複製程式碼

與合約進行互動

至此,我們已經寫完了合約程式碼,前端頁面實現就不在此贅述,主要介紹如何使用 web3 與合約互動,這裡使用到的 web3 版本是 1.0,web3 1.0 和 0.2x.x 的 API 呼叫方式差別較大,1.0 的 API 支援非同步呼叫。

安裝 Metamask 瀏覽器外掛後,會在瀏覽器頁面內注入一個 web3 例項。檢測頁面中是否存在 web3 例項,如果不存在則連線自己的例項。

import Web3 from 'web3';

if (typeof web3 !== 'undefined') {
  web3 = new Web3(web3.currentProvider);
} else {
  web3 = new Web3(new Web3.providers.HttpProvider(NODE_NRL));
}
複製程式碼

傳入合約 ABI,合約地址,例項化合約物件。

this.contract = new web3.eth.Contract(
  CONTRACT_ABI,
  CONTRACT_ADDR,
);
複製程式碼

呼叫合約中的投注方法,通過try catch可以捕獲到 Metamask 彈窗取消交易操作。

userBet = async () => {
  try {
    await this.contract.methods
      .bet(
        ...
      )
      .send({
        from: ACCOUNT,
        value: MONEY,
      });
  } catch (error) {
    ...
  }
}
複製程式碼

查詢記錄的日誌,可以通過指定事件名稱、區塊高度及過濾條件來進行日誌查詢,值得注意的是,在合約內不能查詢到日誌資訊。

queryEvent = async () => {
  const event = await this.contract.getPastEvents(
    EVENT_NAME,
    {
      filter: {},
      fromBlock: 0,
      toBlock: 'latest',
    }
  )
}
複製程式碼

功能擴充

比如修改使用者投注金額及充值這類敏感操作,就需要管理員的許可權來進行操作。同樣地,我們也可以擴充贊助商的功能,通過充值獎池的累計金額排名來展示贊助商的廣告,這裡就不做展開了。

定義修飾器,在建構函式裡設定管理員地址,將建立合約的賬戶設定為管理員。

contract Lottery {
  ...
  address public owner;

  modifier onlyOwner() {
    require(owner == msg.sender);
    _;
  }

  constructor() public {
    owner = msg.sender;
  }
  ...
}
複製程式碼

實現修改投注金額的功能,僅管理員賬戶可觸發。

contract Lottery {
  ...
  function setBetMoney(uint _betMoney) public onlyOwner {
    betMoney = _betMoney;
  }

  function deposit() public payable onlyOwner {}
  ...
}
複製程式碼

總結

當前隨機數的實現通過鏈上資訊生成,這種生成隨機數的方式容易受到不誠實的節點攻擊。下一篇文章將通過多個例項介紹相應的第三方工具庫的使用(Oricalize、SafeMath、OpenZepplin),生成安全的隨機數。

參考資料


前置文章:

區塊鏈上程式設計:DApp開發簡介

(掘金社群連結點此)


文 / ielapp

一個簡單的程式設計師

編 / 熒聲

本文已由作者授權釋出,版權屬於創宇前端。歡迎註明出處轉載本文。本文連結:knownsec-fed.com/2018-10-07-…

想要訂閱更多來自知道創宇開發一線的分享,請搜尋關注我們的微信公眾號:創宇前端(KnownsecFED)。歡迎留言討論,我們會盡可能回覆。

區塊鏈上程式設計:DApp 開發實戰——來寫個競猜遊戲吧!

感謝您的閱讀。

相關文章