以太坊Dapp開發全過程(solidity)
阿新 • • 發佈:2019-02-08
繼上篇用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);
}