1. 程式人生 > >以太坊Dapp開發全過程(solidity)

以太坊Dapp開發全過程(solidity)

繼上篇用php70行程式碼獲取所有以太坊區塊鏈應用程式碼,獲取到以太坊dapp的solidity程式碼,除了用mythril工具掃描出安全問題,還是得深入分析程式碼邏輯。然而solidity語法有些不明白的地方,故藉著loomnetwork的cryptozombies遊戲 學習下用solidity開發區塊鏈的全過程,在此總結分享一下

  • solidity副檔名為.sol,每條語句均使用分號結束
  • solidity開頭應該宣告版本,單個sol檔案中可以有多個contract(最後一個為主contract, 其他contract當作類使用),可以通過import引入其他sol檔案
  • 繼承語法:contract ZombieFactory is Ownable, xx 多重繼承
  • 函式返回值,注意returns 和 return兩個位置
  • mapping相當於字典,address為地址型別,msg.sender為當前執行合約人的address
  • require函式在條件不符合時會終止執行
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/math/SafeMath.sol';
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract ZombieFactory is Ownable {

  using SafeMath for uint256;

  event NewZombie(uint zombieId, string name, uint dna);//宣告事件

  uint dnaDigits = 16;
  uint dnaModulus = 10 ** dnaDigits;
  uint cooldownTime = 1 days;

  struct Zombie {
    string name;
    uint dna;
    uint32 level;
    uint32 readyTime;
    uint16 winCount;
    uint16 lossCount;
  }

  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, uint32(now + cooldownTime), 0, 0)) - 1;
    zombieToOwner[id] = msg.sender;
    ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].add(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 {
    require(ownerZombieCount[msg.sender] == 0);
    uint randDna = _generateRandomDna(_name);
    randDna = randDna - randDna % 100;
    _createZombie(_name, randDna);
  }

}
  • 以太坊上的dapp需要花費gas(以太幣)來執行(pow工作量證明機制,gas付給執行以太坊節點),一個 DApp 收取多少 gas 取決於功能邏輯的複雜程度。每個操作背後,都在計算完成這個操作所需要的計算資源,(比如,儲存資料就比做個加法運算貴得多), 一次操作所需要花費的 gas 等於這個操作背後的所有運算花銷的總和。如果你使用側鏈,倒是不一定需要付費,比如在 Loom Network 上構建的 CryptoZombies 就免費
  • now返回32位時間戳(自1970年1月1日以來經過的秒數),2038年會產生溢位。return (now >= (lastUpdated + 5 minutes)),判斷時間過去5分鐘
  • 儲存:函式之外的變數(狀態變數)均為storage變數,會永久儲存在區塊鏈上,操作他們需要花費gas;函式內部變數為memory臨時變數,也可以顯式宣告為storage
  • 整數使用uint256(uint), uint32, uint16,存在overflow和underflow問題,使用safemath庫解決。++, --操作會出現溢位問題。
  • uint、uint8放在struct中可以節省空間,當 uint 定義在一個 struct 中的時候,儘量使用最小的整數子型別以節約空間。 並且把同樣型別的變數放一起(即在 struct 中將把變數按照型別依次放置),這樣 Solidity 可以將儲存空間最小化
  • safemath庫分uint256, uint32等版本,可以定義在一個safemath.sol檔案中,合約中使用庫using SafeMath for uint256;
  • using SafeMath32 for uint32; using SafeMath16 for uint16;只需要複製library SafeMath,相應的更改名稱及引數型別
pragma solidity ^0.4.11;


/**
 * @title SafeMath
 * @dev Math operations with safety checks that throw on error
 */
library SafeMath {
  function mul(uint256 a, uint256 b) internal returns (uint256) {
    uint256 c = a * b;
    assert(a == 0 || c / a == b);
    return c;
  }

  function div(uint256 a, uint256 b) internal returns (uint256) {
    // assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold
    return c;
  }

  function sub(uint256 a, uint256 b) internal returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}
  • 函式修飾符有public,private,internal,external,view,pure,payable型別。public為任何其他合約可見的;private為僅本檔案合約可用;internal為本合約及子合約可用;external為只能外部合約使用;view為僅讀取合約資料,不消耗gas的程式碼;pure為僅產生資料不操作合約資料的程式碼,也不消耗gas;payable函式需要前端支付gas才能執行。函式預設為public,變數預設為internal
  • payable函式,向用戶收取以太幣
CryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001","ether") })
  • 呼叫其他合約,需要定義介面Interface,僅宣告函式。呼叫需要獲傳入合約地址
contract NumberInterface {
  function getNum(address _myAddress) public view returns (uint);
}      address NumberInterfaceAddress = 0xab38...;
  // ^ 這是FavoriteNumber合約在以太坊上的地址
  NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
  • 有些操作只能供合約owner呼叫,可以建個Ownable (OpenZeppelin庫) 合約供其他合約繼承,建構函式Ownable在合約初次部署時被記錄。增加onlyOwner函式modifier,提供便捷的require判斷,函式modifier可以傳引數,_;為交接執行許可權到函式本身
pragma solidity ^0.4.19;
/**
 * @title Ownable
 * @dev The Ownable contract has an owner address, and provides basic authorization control
 * functions, this simplifies the implementation of "user permissions".
 */
contract Ownable {
  address public owner;

  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

  /**
   * @dev The Ownable constructor sets the original `owner` of the contract to the sender
   * account.
   */
  function Ownable() public {
    owner = msg.sender;
  }


  /**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }


  /**
   * @dev Allows the current owner to transfer control of the contract to a newOwner.
   * @param newOwner The address to transfer ownership to.
   */
  function transferOwnership(address newOwner) public onlyOwner {
    require(newOwner != address(0));
    OwnershipTransferred(owner, newOwner);
    owner = newOwner;
  }

}
  • uint[] public intarray;宣告公開的動態陣列。solidity自動建立getter方法,前端可以通過intarray(0)獲取值;intarray.push(1),新增元素。記憶體陣列必須 用長度引數建立uint[] memory values = new uint[](3);
  • sha3雜湊函式keccake256(),用雜湊值進行判斷字串相等;用雜湊值生成偽隨機數,https://blog.positive.com/predicting-random-numbers-in-ethereum-smart-contracts-e5358c6b8620
  • msg.sender.transfer(msg.value - itemFee);向執行合約的人退回多餘的以太幣。this.balance代表當前合約儲存了多少以太幣,下面是合約提現函式
function withdraw() external onlyOwner {
    owner.transfer(this.balance);
  }
  • 以太坊的代幣比如ERC20 代幣,所有合約遵循相同規則,即它實現了所有其他代幣合約共享的一組標準函式,例如 transfer(address _to, uint256 _value) 和 balanceOf(address _owner)
  • 在智慧合約內部,通常有一個對映, mapping(address => uint256) balances,用於追蹤每個地址還有多少餘額。所以基本上一個代幣只是一個追蹤誰擁有多少該代幣的合約,和一些可以讓那些使用者將他們的代幣轉移到其他地址的函式。一個例子就是交易所。 當交易所新增一個新的 ERC20 代幣時,實際上它只需要新增與之對話的另一個智慧合約。 使用者可以讓那個合約將代幣傳送到交易所的錢包地址,然後交易所可以讓合約在使用者要求取款時將代幣傳送回給他們
  • 有另一個代幣標準更適合如 CryptoZombies 這樣的加密收藏品——它們被稱為ERC721 代幣。ERC721 代幣是不能互換的,因為每個代幣都被認為是唯一且不可分割的。 你只能以整個單位交易它們,並且每個單位都有唯一的 ID,每個單位都有特定屬性。下面是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 規範有兩種不同的方法來轉移代幣,直接transfer和先發出申請、然後接收人主動接收。transfer之後需要傳送Transfer事件到前端,Approval事件也需要
function transfer(address _to, uint256 _tokenId) public;

function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
  • 前端編寫,以太坊節點接收json-rpc類協議,前端使用web3.js與節點互動。web3.js可設定infura和metamask作為服務提供者(另一層封裝),開發者不需要自己搭建節點。
  • infura  var web3 = new Web3(new  Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
  • 寫入合約資料需要使用者私鑰,infura不如metamask方便。metamask是基於infura開發的瀏覽器外掛,可以管理使用者以太坊賬號。Metamask 把它的 web3 提供者注入到瀏覽器的全域性 JavaScript物件web3中。所以你的應用可以檢查 web3 是否存在。若存在就使用 web3.currentProvider 作為它的提供者
window.addEventListener('load', function() {

  // 檢查web3是否已經注入到(Mist/MetaMask)
  if (typeof web3 !== 'undefined') {
    // 使用 Mist/MetaMask 的提供者
    web3js = new Web3(web3.currentProvider);
  } else {
    // 處理使用者沒安裝的情況, 比如顯示一個訊息
    // 告訴他們要安裝 MetaMask 來使用我們的應用
  }

  // 現在你可以啟動你的應用並自由訪問 Web3.js:
  startApp()

})
  • 前端呼叫合約,var myContract = new web3js.eth.Contract(myABI, myContractAddress);引數為編譯產生的ABI和部署後的合約地址  (較為麻煩,未來會使用ENS和swarm:http://blog.sina.com.cn/s/blog_6cd4a7350102yc91.html)
  • 呼叫合約函式有call和send兩個函式,call呼叫不需要花費gas的函式
myContract.methods.myMethod(123).call()
myContract.methods.myMethod(123).send({ from: userAccount }).on("receipt", function(receipt) {}).on("error", function(error) {})
//public陣列zombies
cryptoZombies.methods.zombies(id).call()
  • 從metamask中獲取使用者賬號var userAccount = web3.eth.accounts[0]
var accountInterval = setInterval(function() {
  // 檢查賬戶是否切換
  if (web3.eth.accounts[0] !== userAccount) {
    userAccount = web3.eth.accounts[0];
    // 呼叫一些方法來更新介面
    updateInterface();
  }
}, 100);
  • 獲取合約資料, Web3.js 的 1.0 版使用的是 Promises 而不是回撥函式
function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

        var accountInterval = setInterval(function() {
          // Check if account has changed
          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];
            // Call a function to update the UI with the new account
            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);
      }

      function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }
  • event是合約用來發送事件,供前端web3.js來接受的,前端訂閱事件( Web3.js 最新版1.0的,此版本使用了 WebSockets 來訂閱事件,metamask暫不支援websocket,可使用infura provider)
為了篩選僅和當前使用者相關的事件,我們的 Solidity 合約將必須使用 indexed 關鍵字,就像我們在 ERC721 實現中的Transfer 事件中那樣:
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
日誌中儲存的不同的索引事件就叫不同的主題。事件定義,event transfer(address indexed _from, address indexed _to, uint value)有三個主題,第一個主題為預設主題,即事件簽名transfer(address,address,uint256),但如果是宣告為anonymous的事件,則沒有這個主題;另外兩個indexed的引數也會分別形成兩個主題,可以分別通過_from,_to主題來進行過濾。如果陣列,包括字串,位元組資料做為索引引數,實際主題是對應值的Keccak-256雜湊值
在這種情況下, 因為_from 和 _to 都是 indexed,這就意味著我們可以在前端事件監聽中過濾事件
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
  let data = event.returnValues;
  // 當前使用者更新了一個殭屍!更新介面來顯示
}).on('error', console.error);
  • 查詢過去的事件
我們甚至可以用 getPastEvents 查詢過去的事件,並用過濾器 fromBlock 和 toBlock 給 Solidity 一個事件日誌的時間範圍("block" 在這裡代表以太坊區塊編號):

cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: 'latest' })
.then(function(events) {
  // events 是可以用來遍歷的 `event` 物件 
  // 這段程式碼將返回給我們從開始以來建立的殭屍列表
});

因為你可以用這個方法來查詢從最開始起的事件日誌,這就有了一個非常有趣的用例: 用事件來作為一種更便宜的儲存。
這裡的短板是,事件不能從智慧合約本身讀取
  • ERC20規範
contract ERC20 {
    function totalSupply() constant returns (uint supply);
    function balanceOf( address who ) constant returns (uint value);
    function allowance( address owner, address spender ) constant returns (uint _allowance);//判斷還可以轉移多少幣

    function transfer( address to, uint value) returns (bool ok);
    function transferFrom( address from, address to, uint value) returns (bool ok);
    function approve( address spender, uint value ) returns (bool ok);

    event Transfer( address indexed from, address indexed to, uint value);
    event Approval( address indexed owner, address indexed spender, uint value);
}