在本系列關於使用以太坊構建DApps教程的第3部分中,咱們構建並將咱們的代幣部署到以太坊測試網絡Rinkeby。在這部分中,咱們將開始編寫Story DAO代碼。php
咱們將使用第1部分中列出的條件來作指導。java
讓咱們用這個骨架建立一個新的合約StoryDao.sol
:node
pragma solidity ^0.4.24; import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol"; import "../node_modules/openzeppelin-solidity/contracts/ownership/Ownable.sol"; contract StoryDao is Ownable { using SafeMath for uint256; mapping(address => bool) whitelist; uint256 public whitelistedNumber = 0; mapping(address => bool) blacklist; event Whitelisted(address addr, bool status); event Blacklisted(address addr, bool status); uint256 public daofee = 100; // hundredths of a percent, i.e. 100 is 1% uint256 public whitelistfee = 10000000000000000; // in Wei, this is 0.01 ether event SubmissionCommissionChanged(uint256 newFee); event WhitelistFeeChanged(uint256 newFee); uint256 public durationDays = 21; // duration of story's chapter in days uint256 public durationSubmissions = 1000; // duration of story's chapter in entries function changedaofee(uint256 _fee) onlyOwner external { require(_fee < daofee, "New fee must be lower than old fee."); daofee = _fee; emit SubmissionCommissionChanged(_fee); } function changewhitelistfee(uint256 _fee) onlyOwner external { require(_fee < whitelistfee, "New fee must be lower than old fee."); whitelistfee = _fee; emit WhitelistFeeChanged(_fee); } function lowerSubmissionFee(uint256 _fee) onlyOwner external { require(_fee < submissionZeroFee, "New fee must be lower than old fee."); submissionZeroFee = _fee; emit SubmissionFeeChanged(_fee); } function changeDurationDays(uint256 _days) onlyOwner external { require(_days >= 1); durationDays = _days; } function changeDurationSubmissions(uint256 _subs) onlyOwner external { require(_subs > 99); durationSubmissions = _subs; } }
咱們正在導入SafeMath
以便再次進行安全計算,但此次咱們還使用了Zeppelin的Ownable
合約,該合約容許某人「擁有」故事並執行某些僅限管理員的功能。簡單地說咱們的StoryDao is Ownable
就夠了;隨時檢查合約,看看它是如何工做的。python
咱們還使用此合約中的onlyOwner
修飾符。函數修飾符基本上是函數擴展和插件。onlyOwner
修飾符以下所示:android
modifier onlyOwner() { require(msg.sender == owner); _; }
當onlyOwner
被添加到一個函數中時,那個函數的體被粘貼到_;
所在的部分,而且它比其餘的一切內容都先執行。所以,經過使用此修飾符,該函數會自動檢查郵件發件人是否也是合約的全部者,而後照常繼續(若是是)。若是沒有,它會崩潰。程序員
經過在改變咱們的Story DAO的費用和其餘參數的函數上使用onlyOwner
修飾符,咱們確保只有管理員才能進行這些更改。web
讓咱們測試一下初始函數。mongodb
若是文件夾test
不存在,請建立它。而後在其中建立文件TestStoryDao.sol
和TestStoryDao.js
。由於在Truffle中沒有本地方法來測試異常,因此也可使用如下內容建立helpers/expectThrow.js
:編程
export default async promise => { try { await promise; } catch (error) { const invalidOpcode = error.message.search('invalid opcode') >= 0; const outOfGas = error.message.search('out of gas') >= 0; const revert = error.message.search('revert') >= 0; assert( invalidOpcode || outOfGas || revert, 'Expected throw, got \'' + error + '\' instead', ); return; } assert.fail('Expected throw not received'); };
注意:Solidity測試一般用於測試基於合約的低級函數,即智能合約的內部。JS測試一般用於測試合約是否能夠與外部進行正確的交互,這是咱們最終用戶將要作的事情。json
在TestStoryDao.sol
,輸入如下內容:
pragma solidity ^0.4.24; import "truffle/Assert.sol"; import "truffle/DeployedAddresses.sol"; import "../contracts/StoryDao.sol"; contract TestStoryDao { function testDeploymentIsFine() public { StoryDao sd = StoryDao(DeployedAddresses.StoryDao()); uint256 daofee = 100; // hundredths of a percent, i.e. 100 is 1% uint256 whitelistfee = 10000000000000000; // in Wei, this is 0.01 ether uint256 durationDays = 21; // duration of story's chapter in days uint256 durationSubmissions = 1000; // duration of story's chapter in entries Assert.equal(sd.daofee(), daofee, "Initial DAO fee should be 100"); Assert.equal(sd.whitelistfee(), whitelistfee, "Initial whitelisting fee should be 0.01 ether"); Assert.equal(sd.durationDays(), durationDays, "Initial day duration should be set to 3 weeks"); Assert.equal(sd.durationSubmissions(), durationSubmissions, "Initial submission duration should be set to 1000 entries"); } }
這將檢查StoryDao合約是否正確部署,並提供正確的費用和持續時間。第一行確保經過從已部署地址列表中讀取它來部署它,而且最後一節作了一些斷言——檢查聲明是真仍是假。在咱們的例子中,咱們將數字與已部署合約的初始值進行比較。每當它爲「true」時,Assert.equals
部分將發出一個「True」的事件,這是Truffle在測試時正在監聽的事件。
在TestStoryDao.js
,輸入如下內容:
import expectThrow from './helpers/expectThrow'; const StoryDao = artifacts.require("StoryDao"); contract('StoryDao Test', async (accounts) => { it("should make sure environment is OK by checking that the first 3 accounts have over 20 eth", async () =>{ assert.equal(web3.eth.getBalance(accounts[0]).toNumber() > 2e+19, true, "Account 0 has more than 20 eth"); assert.equal(web3.eth.getBalance(accounts[1]).toNumber() > 2e+19, true, "Account 1 has more than 20 eth"); assert.equal(web3.eth.getBalance(accounts[2]).toNumber() > 2e+19, true, "Account 2 has more than 20 eth"); }); it("should make the deployer the owner", async () => { let instance = await StoryDao.deployed(); assert.equal(await instance.owner(), accounts[0]); }); it("should let owner change fee and duration", async () => { let instance = await StoryDao.deployed(); let newDaoFee = 50; let newWhitelistFee = 1e+10; // 1 ether let newDayDuration = 42; let newSubsDuration = 1500; instance.changedaofee(newDaoFee, {from: accounts[0]}); instance.changewhitelistfee(newWhitelistFee, {from: accounts[0]}); instance.changedurationdays(newDayDuration, {from: accounts[0]}); instance.changedurationsubmissions(newSubsDuration, {from: accounts[0]}); assert.equal(await instance.daofee(), newDaoFee); assert.equal(await instance.whitelistfee(), newWhitelistFee); assert.equal(await instance.durationDays(), newDayDuration); assert.equal(await instance.durationSubmissions(), newSubsDuration); }); it("should forbid non-owners from changing fee and duration", async () => { let instance = await StoryDao.deployed(); let newDaoFee = 50; let newWhitelistFee = 1e+10; // 1 ether let newDayDuration = 42; let newSubsDuration = 1500; await expectThrow(instance.changedaofee(newDaoFee, {from: accounts[1]})); await expectThrow(instance.changewhitelistfee(newWhitelistFee, {from: accounts[1]})); await expectThrow(instance.changedurationdays(newDayDuration, {from: accounts[1]})); await expectThrow(instance.changedurationsubmissions(newSubsDuration, {from: accounts[1]})); }); it("should make sure the owner can only change fees and duration to valid values", async () =>{ let instance = await StoryDao.deployed(); let invalidDaoFee = 20000; let invalidDayDuration = 0; let invalidSubsDuration = 98; await expectThrow(instance.changedaofee(invalidDaoFee, {from: accounts[0]})); await expectThrow(instance.changedurationdays(invalidDayDuration, {from: accounts[0]})); await expectThrow(instance.changedurationsubmissions(invalidSubsDuration, {from: accounts[0]})); }) });
爲了使咱們的測試成功運行,咱們還須要告訴Truffle咱們想要部署StoryDao——由於它不會爲咱們作。所以,讓咱們在migrations
建立3_deploy_storydao.js
,其內容幾乎與咱們以前編寫的遷移相同:
var Migrations = artifacts.require("./Migrations.sol"); var StoryDao = artifacts.require("./StoryDao.sol"); module.exports = function(deployer, network, accounts) { if (network == "development") { deployer.deploy(StoryDao, {from: accounts[0]}); } else { deployer.deploy(StoryDao); } };
此時,咱們還應該在項目文件夾的根目錄中更新(或建立,若是它不存在)package.json
文件,其中包含咱們目前所需的依賴項,而且可能在不久的未來須要:
{ "name": "storydao", "devDependencies": { "babel-preset-es2015": "^6.18.0", "babel-preset-stage-2": "^6.24.1", "babel-preset-stage-3": "^6.17.0", "babel-polyfill": "^6.26.0", "babel-register": "^6.23.0", "dotenv": "^6.0.0", "truffle": "^4.1.12", "openzeppelin-solidity": "^1.10.0", "openzeppelin-solidity-metadata": "^1.2.0", "openzeppelin-zos": "", "truffle-wallet-provider": "^0.0.5", "ethereumjs-wallet": "^0.6.0", "web3": "^1.0.0-beta.34", "truffle-assertions": "^0.3.1" } }
和.babelrc
文件的內容:
{ "presets": ["es2015", "stage-2", "stage-3"] }
咱們還須要在咱們的Truffle配置中要求Babel,所以它知道它應該在編譯時使用它。
注意:Babel是NodeJS的一個附加組件,它容許咱們在當前一代NodeJS中使用下一代JavaScript,所以咱們能夠編寫諸如import
。若是這超出了你的理解範圍,只需忽略它,而後只需逐字粘貼便可。在以這種方式安裝後,你可能永遠沒必要再處理這個問題。
require('dotenv').config(); ================== ADD THESE TWO LINES ================ require('babel-register'); require('babel-polyfill'); ======================================================= const WalletProvider = require("truffle-wallet-provider"); const Wallet = require('ethereumjs-wallet'); // ...
如今,終於進行truffle test
。輸出應該相似於這個:
有關測試的更多信息,請參閱本教程,該教程專門用於測試智能合約。
在本課程的後續部分中,咱們將跳過測試,由於輸入它們會使教程太長,但請參考項目的最終源代碼來檢查它們。咱們剛剛完成的過程已經設置了測試環境,所以你能夠在進一步設置的狀況下編寫測試。
如今讓咱們構建一個白名單機制,讓用戶參與構建Story。將如下函數框架添加到StoryDao.sol
:
function whitelistAddress(address _add) public payable { // whitelist sender if enough money was sent } function() external payable { // if not whitelisted, whitelist if paid enough // if whitelisted, but X tokens at X price for amount }
未命名的函數function()
被稱爲回調函數,這是在沒有特定指令的狀況下將錢發送到此合約時被調用的函數(即,沒有專門調用其餘函數)。這可讓人們加入StoryDao,只需將以太發送到DAO並當即將其列入白名單,或者購買代幣,具體取決於它們是否已經列入白名單。
whitelistSender
功能用於白名單,能夠直接調用,可是若是發送方還沒有列入白名單,咱們將確保當收到一些以太時,後備功能會自動調用它。whitelistAddress
函數被聲明爲public
由於它也應該能夠從其餘合約中調用,而且回調函數是external
函數,由於money
將僅從外部地址轉到此地址。調用此合約的合約能夠直接輕鬆調用所需的功能。
讓咱們首先處理回調函數。
function() external payable { if (!whitelist[msg.sender]) { whitelistAddress(msg.sender); } else { // buyTokens(msg.sender, msg.value); } }
咱們檢查發件人是否已經在白名單中,並將調用委託給whitelistAddress
函數。請注意,咱們已經註釋掉了buyTokens
函數,由於咱們尚未它。
接下來,讓咱們處理白名單。
function whitelistAddress(address _add) public payable { require(!whitelist[_add], "Candidate must not be whitelisted."); require(!blacklist[_add], "Candidate must not be blacklisted."); require(msg.value >= whitelistfee, "Sender must send enough ether to cover the whitelisting fee."); whitelist[_add] = true; whitelistedNumber++; emit Whitelisted(_add, true); if (msg.value > whitelistfee) { // buyTokens(_add, msg.value.sub(whitelistfee)); } }
請注意,此函數接受地址做爲參數,而且不從消息中提取它(來自交易)。若是有人沒法承擔加入DAO的費用,這還有一個額外的好處,即人們能夠將其餘人列入白名單。
咱們經過一些健壯性檢查啓動該功能:發件人不得列入白名單或列入黑名單(禁止),而且必須已發送足夠的費用以支付費用。若是這些條件使人滿意,則將地址添加到白名單中,發出白名單事件,最後,若是發送的以太數量大於覆蓋白名單費用所需的以太數量,則剩餘部分用於買這些代幣。
注意:咱們使用sub
而不是-
來減,由於這是一個安全計算的SafeMath
函數。
用戶如今能夠將本身或其餘人列入白名單,只要他們向StoryDao合約發送0.01以太或更多。
咱們在本教程中構建了DAO的初始部分,但還有不少工做要作。請繼續關注:在下一部分中,咱們將處理爲Story添加內容的問題!
======================================================================
分享一些以太坊、EOS、比特幣等區塊鏈相關的交互式在線編程實戰教程:
- java以太坊開發教程,主要是針對java和android程序員進行區塊鏈以太坊開發的web3j詳解。
- python以太坊,主要是針對python工程師使用web3.py進行區塊鏈以太坊開發的詳解。
- php以太坊,主要是介紹使用php進行智能合約開發交互,進行帳號建立、交易、轉帳、代幣開發以及過濾器和交易等內容。
- 以太坊入門教程,主要介紹智能合約與dapp應用開發,適合入門。
- 以太坊開發進階教程,主要是介紹使用node.js、mongodb、區塊鏈、ipfs實現去中心化電商DApp實戰,適合進階。
- C#以太坊,主要講解如何使用C#開發基於.Net的以太坊應用,包括帳戶管理、狀態與交易、智能合約開發與交互、過濾器和交易等。
- EOS教程,本課程幫助你快速入門EOS區塊鏈去中心化應用的開發,內容涵蓋EOS工具鏈、帳戶與錢包、發行代幣、智能合約開發與部署、使用代碼與智能合約交互等核心知識點,最後綜合運用各知識點完成一個便籤DApp的開發。
- java比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在Java代碼中集成比特幣支持功能,例如建立地址、管理錢包、構造裸交易等,是Java工程師不可多得的比特幣開發學習課程。
- php比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在Php代碼中集成比特幣支持功能,例如建立地址、管理錢包、構造裸交易等,是Php工程師不可多得的比特幣開發學習課程。
- tendermint區塊鏈開發詳解,本課程適合但願使用tendermint進行區塊鏈開發的工程師,課程內容即包括tendermint應用開發模型中的核心概念,例如ABCI接口、默克爾樹、多版本狀態庫等,也包括代幣發行等豐富的實操代碼,是go語言工程師快速入門區塊鏈開發的最佳選擇。
匯智網原創翻譯,轉載請標明出處。這裏是原文以太坊構建DApps系列教程(四):Story DAO的白名單和測試