以太坊构建DApps系列教程(四):Story DAO的白名单和测试

87次阅读

共计 9923 个字符,预计需要花费 25 分钟才能阅读完成。

在本系列关于使用以太坊构建 DApps 教程的第 3 部分中,我们构建并将我们的代币部署到以太坊测试网络 Rinkeby。在这部分中,我们将开始编写 Story DAO 代码。
我们将使用第 1 部分中列出的条件来做指导。
合约大纲
让我们用这个骨架创建一个新的合约 StoryDao.sol:
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 就够了;随时检查合约,看看它是如何工作的。
我们还使用此合约中的 onlyOwner 修饰符。函数修饰符基本上是函数扩展和插件。onlyOwner 修饰符如下所示:
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
当 onlyOwner 被添加到一个函数中时,那个函数的体被粘贴到_; 所在的部分,并且它比其他的一切内容都先执行。因此,通过使用此修饰符,该函数会自动检查邮件发件人是否也是合约的所有者,然后照常继续(如果是)。如果没有,它会崩溃。
通过在改变我们的 Story DAO 的费用和其他参数的函数上使用 onlyOwner 修饰符,我们确保只有管理员才能进行这些更改。
测试
让我们测试一下初始函数。
如果文件夹 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 测试通常用于测试合约是否可以与外部进行正确的交互,这是我们最终用户将要做的事情。
在 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 的白名单和测试

正文完
 0