以太坊dapp

tech2022-09-01  108

以太坊dapp

In part 3 of this tutorial series on building DApps with Ethereum, we built and deployed our token to the Ethereum testnet Rinkeby. In this part, we’ll start writing the Story DAO code.

在本教程系列的第3部分中 ,我们使用以太坊构建DApp,将令牌构建并部署到以太坊测试网Rinkeby。 在这一部分中,我们将开始编写Story DAO代码。

We’ll use the conditions laid out in the intro post to guide us.

我们将使用简介中列出的条件来指导我们。

合约大纲 (Contract Outline)

Let’s create a new contract, StoryDao.sol, with this skeleton:

让我们用这个框架创建一个新的合同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; } }

We’re importing SafeMath to have safe calculations again, but this time we’re also using Zeppelin’s Ownable contract, which lets someone “own” the story and execute certain admin-only functions. Simply saying that our StoryDao is Ownable is enough; feel free to inspect the contract to see how it works.

我们正在导入SafeMath以再次进行安全计算,但是这次我们还使用Zeppelin的Ownable合同,该合同允许某人“拥有”故事并执行某些仅用于管理员的功能。 仅仅说我们的StoryDao is Ownable就足够了; 随时检查合同以了解其工作原理。

We also use the onlyOwner modifier from this contract. Function modifiers are basically extensions, plugins for functions. The onlyOwner modifier looks like this:

我们还使用此合同中的onlyOwner修饰符。 函数修饰符基本上是功能的扩展,插件。 onlyOwner修饰符如下所示:

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

When onlyOwner is added to a function, then that function’s body is pasted into the part where the _; part is, and everything before it executes first. So by using this modifier, the function automatically checks if the message sender is also the owner of the contract and then continues as usual if so. If not, it crashes.

将onlyOwner添加到函数后,该函数的主体将粘贴到_;所在的部分_; 部分是,并且它首先执行之前的所有操作。 因此,通过使用此修饰符,该功能会自动检查消息发件人是否也是合同的所有者,然后继续照常继续。 如果没有,它就会崩溃。

By using the onlyOwner modifier on the functions that change the fees and other parameters of our story DAO, we make sure that only the admin can do these changes.

通过在用于更改故事DAO的费用和其他参数的函数上使用onlyOwner修饰符,我们确保只有管理员才能进行这些更改。

测试中 (Testing)

Let’s test the initial functions.

让我们测试一下初始功能。

Create the folder test if it doesn’t exist. Then inside it, create the files TestStoryDao.sol and TestStoryDao.js. Because there’s no native way to test for exceptions in Truffle, also create helpers/expectThrow.js with the content:

如果不存在,请创建文件夹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'); };

Note: Solidity tests are generally used to test low-level, contract-based functions, the internals of a smart contract. JS tests are generally used to test if the contract can be properly interacted with from the outside, which is something our end users will be doing.

注意:可靠性测试通常用于测试基于合同的底层功能,智能合约的内部。 JS测试通常用于测试合同是否可以从外部正确交互,这是我们最终用户将要做的事情。

In TestStoryDao.sol, put the following content:

在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"); } }

This checks that the StoryDao contract gets deployed properly with the right numbers for fees and duration. The first line makes sure it’s deployed by reading it from the list of deployed addresses, and last section does some assertions — checking that a claim is true or false. In our case, we’re comparing numbers to initial values of the deployed contract. Whenever it’s “true”, the Assert.equals part will emit an event that says “True”, which is what Truffle is listening for when testing.

这将检查StoryDao合同是否正确部署了正确的费用和期限编号。 第一行通过从已部署地址的列表中读取来确保已部署它,最后一部分进行一些断言 —检查声明是对还是错。 在我们的案例中,我们正在将数字与已部署合同的初始值进行比较。 每当它为“ true”时, Assert.equals部分都会发出一个“ True”事件,这是Truffle在测试时正在侦听的事件。

In TestStoryDao.js, put the following content:

在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]})); }) });

In order for our tests to successfully run, we also need to tell Truffle that we want the StoryDao deployed — because it’s not going to do it for us. So let’s create 3_deploy_storydao.js in migrations with content almost identical to the previous migration we wrote:

为了使测试成功运行,我们还需要告诉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); } };

At this point, we should also update (or create, if it’s not present) a package.json file in the root of our project folder with the dependencies we needed so far and may need in the near future:

在这一点上,我们还应该在项目文件夹的根目录中更新(或创建,如果不存在的话)一个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" } }

And a .babelrc file with the content:

还有一个.babelrc文件,其内容为:

{ "presets": ["es2015", "stage-2", "stage-3"] }

And we also need to require Babel in our Truffle configuration so it knows it should use it when compiling tests.

我们还需要在我们的Truffle配置中要求Babel,以便它知道在编译测试时应该使用它。

Note: Babel is an add-on for NodeJS which lets us use next-generation JavaScript in current-generation NodeJS, so we can write things like import etc. If this is beyond your understanding, simply ignore it and just paste this verbatim. You’ll probably never have to deal with this again after installing it this way.

注意: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'); // ...

Now, finally run truffle test. The output should be similar to this one:

现在, 终于运行truffle test 。 输出应与此类似:

For more information about testing, see this tutorial, which we prepared specifically to cover testing of smart contracts.

有关测试的更多信息,请参阅本教程 ,我们专门准备了该教程以涵盖智能合约的测试。

In subsequent parts of this course, we’ll be skipping the tests, as typing them out would make the tutorials too long, but please refer to the final source code of the project to inspect them all. The process we just went through has set up the environment for testing, so you can just write the tests with zero further setup.

在本课程的后续部分中,我们将跳过测试,因为将它们键入将使教程过长,但是请参考项目的最终源代码以检查所有这些。 我们刚刚经历的过程已经设置了测试环境,因此您可以在零设置的情况下编写测试。

白名单 (Whitelist)

Let’s build the whitelisting mechanism now, which lets users participate in building the story. Add the following function skeletons to StoryDao.sol:

现在,让我们建立白名单机制,让用户参与构建故事。 将以下函数框架添加到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 }

The unnamed function function() is called a fallback function, and that’s the function that gets called when money is sent to this contract without a specific instruction (i.e. without calling another function specifically). This lets people join the StoryDao by just sending Ether to the DAO and either getting whitelisted instantly, or buying tokens, depending on whether or not they are already whitelisted.

未命名的函数function()称为后备函数 ,该函数是在没有特定指令(即,没有专门调用另一个函数)的情况下向该合约发送资金时调用的函数。 这使人们可以通过将Ether发送到DAO并立即被列入白名单,或购买令牌(取决于他们是否已被列入白名单)来加入StoryDao。

The whitelistSender function is there for whitelisting and can be called directly, but we’ll make sure the fallback function automatically calls it when it receives some ether if the sender has not yet been whitelisted. The whitelistAddress function is declared public because it should be callable from other contracts as well, and the fallback function is external because money will be going to this address only from external addresses. Contracts calling this contract can easily call required functions directly.

whitelistSender函数可用于白名单,并且可以直接调用,但如果发件人尚未被列入白名单,我们将确保fallback函数在收到一些以太时自动调用它。 将whitelistAddress函数声明为public是因为它也应该可以从其他合同中调用,而fallback函数是external因为金钱只会从外部地址转到该地址。 调用此合同的合同可以轻松地直接调用所需的功能。

Let’s deal with the fallback function first.

让我们先处理回退功能。

function() external payable { if (!whitelist[msg.sender]) { whitelistAddress(msg.sender); } else { // buyTokens(msg.sender, msg.value); } }

We check if the sender isn’t already on the whitelist, and delegate the call to the whitelistAddress function. Notice that we commented out our buyTokens function because we don’t yet have it.

我们检查发件人是否还不在白名单中,然后将调用委派给whitelistAddress函数。 请注意,由于我们尚不具备buyTokens函数,因此已将其注释掉。

Next, let’s handle the whitelisting.

接下来,让我们处理白名单。

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)); } }

Notice that this function accepts the address as a parameter and doesn’t extract it from the message (from the transaction). This has the added advantage of people being able to whitelist other people, if someone can’t afford to join the DAO for example.

请注意,此函数接受地址作为参数,并且不会从消息(从事务)中提取地址。 例如,如果某人无力加入DAO,这具有人们能够将其他人列入白名单的附加优势。

We start the function with some sanity checks: the sender must not be whitelisted already or blacklisted (banned) and must have sent in enough to cover the fee. If these conditions are satisfactory, the address is added to the whitelist, the Whitelisted event is emitted, and, finally, if the amount of ether sent in is greater than the amount of ether needed to cover the whitelisting fee, the remainder is used to buy the tokens.

我们通过一些健全性检查来启动该功能:发件人一定不能已经被列入白名单或被列入黑名单(禁止),并且一定已经发送了足够的费用来支付费用。 如果满足这些条件,则将地址添加到白名单中,发出列入白名单的事件,最后,如果发送的以太币数量大于支付白名单费用所需的以太币数量,则其余部分用于购买代币。

Note: we’re using sub instead of - to subtract, because that’s a SafeMath function for safe calculations.

注意:我们使用sub而不是-进行减法,因为这是用于安全计算的SafeMath函数。

Users can now get themselves or others whitelisted as long as they send 0.01 ether or more to the StoryDao contract.

现在,只要他们向StoryDao合约发送0.01或更多的以太币,用户就可以将自己或其他人列入白名单。

结论 (Conclusion)

We built the initial part of our DAO in this tutorial, but a lot more work remains. Stay tuned: in the next part we’ll deal with adding content to the story!

我们在本教程中构建了DAO的初始部分,但还有很多工作要做。 请继续关注:在下一部分中,我们将为故事添加内容!

翻译自: https://www.sitepoint.com/building-ethereum-dapps-whitelisting-testing-story-dao/

以太坊dapp

最新回复(0)