da-dapps
In part 6 of this tutorial series on building DApps with Ethereum, we took the DAO towards completion by adding voting, blacklisting/unblacklisting, and dividend distribution and withdrawal, while throwing in some additional helper functions for good measure. In this tutorial, we’ll build a web interface for interacting with our story, as we otherwise can’t count on any user engagement. So this is the final part of our story before we launch it into the wild.
在本教程系列的第6部分中 ,我们使用以太坊构建DApp,通过添加投票,将黑名单/非黑名单以及股利分配和提款来完成DAO,同时还引入了一些其他辅助函数来很好地解决问题。 在本教程中,我们将构建一个用于与故事进行交互的Web界面,否则我们将无法依靠任何用户参与。 因此,这是我们故事的最后一部分,然后我们将其付诸实践。
Since this isn’t a web application tutorial, we’ll keep things extremely simple. The code below is not production-ready, and is meant to serve only as a proof of concept on how to connect JavaScript to the blockchain. But first, let’s add a new migration.
由于这不是Web应用程序教程,因此我们将使事情变得非常简单。 以下代码尚未投入生产,仅用于作为如何将JavaScript连接到区块链的概念证明。 但是首先,让我们添加一个新的迁移。
自动传输 (Automating Transfers)
Right now as we deploy our token and DAO, they sit on the blockchain but don’t interact. To test what we’ve built, we need to manually transfer token ownership and balance to the DAO, which can be tedious during testing.
现在,当我们部署令牌和DAO时,它们位于区块链上,但不会交互。 要测试我们构建的内容,我们需要手动将令牌所有权和余额转移到DAO,这在测试期间可能很繁琐。
Let’s write a new migration which does this for us. Create the file 4_configure_relationship.js
and put the following content in there:
让我们写一个新的迁移为我们做这件事。 创建文件4_configure_relationship.js
并将以下内容放入其中:
var Migrations = artifacts.require("./Migrations.sol");
var StoryDao = artifacts.require("./StoryDao.sol");
var TNSToken = artifacts.require("./TNSToken.sol");
var storyInstance, tokenInstance;
module.exports = function (deployer, network, accounts) {
deployer.then(function () {
return TNSToken.deployed();
}).then(function (tIns) {
tokenInstance = tIns;
return StoryDao.deployed();
}).then(function (sIns) {
storyInstance = sIns;
return balance = tokenInstance.totalSupply();
}).then(function (bal) {
return tokenInstance.transfer(storyInstance.address, bal);
})
.then(function (something) {
return tokenInstance.transferOwnership(storyInstance.address);
});
}
Here’s what this code does. First, you’ll notice it’s promise-based. It’s full of then
calls. This is because we depend on a function returning some data before we call the next one. All contract calls are promise-based, meaning they don’t return data immediately because Truffle needs to ask the node for information, so a promise to return data at a future time is made. We force the code to wait for this data by using then
and providing all then
calls with callback functions which get called with this result when it’s finally given.
这是这段代码的作用。 首先,您会注意到它是基于承诺的。 then
到处都是电话。 这是因为我们依赖于一个函数在调用下一个数据之前返回一些数据。 所有合同调用均基于承诺,这意味着它们不会立即返回数据,因为Truffle需要向节点询问信息,因此做出了将来返回数据的承诺 。 我们通过使用then
并为所有then
调用提供回调函数来强制代码等待这些数据,这些回调函数在最终给出结果时会被调用。
So, in order:
因此,为了:
- first, ask the node for the address of the deployed token and return it 首先,向节点询问已部署令牌的地址并返回
- then, accepting this data, save it into a global variable and ask for the address of the deployed DAO and return it 然后,接受此数据,将其保存到全局变量中,并询问已部署DAO的地址并返回
- then, accepting this data, save it into a global variable and ask for the balance the owner of the token contract will have in their account, which is technically the total supply, and return this data 然后,接受此数据,将其保存到全局变量中,并要求令牌合约的所有者在其帐户中拥有余额(从技术上来说是总供给),然后返回此数据
then, once you get this balance, use it to call the
transfer
function of this token and send tokens to the DAO’s address and return the result然后,一旦获得此余额,就可以使用它来调用此令牌的
transfer
函数,并将令牌发送到DAO的地址并返回结果- then, ignore the returned result — we just wanted to know when it’s done — and finally transfer ownership of the token to the DAO’s address, returning the data but not discarding it. 然后,忽略返回的结果(我们只是想知道何时完成),最后将令牌的所有权转移到DAO的地址,返回数据但不丢弃它。
Running truffle migrate --reset
should now produce an output like this:
运行truffle migrate --reset
现在应该产生如下输出:
前端 (The Front End)
The front end is a regular, static HTML page with some JavaScript thrown in for communicating with the blockchain and some CSS to make things less ugly.
前端是一个常规的静态HTML页面,其中添加了一些JavaScript以与区块链进行通信,并提供一些CSS以使事情变得不那么难看。
Let’s create a file index.html
in the subfolder public
and give it the following content:
让我们在子文件夹public
创建文件index.html
,并为其提供以下内容:
<!DOCTYPE HTML>
<html lang="en">
<head>
<title>The Neverending Story</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="description" content="The Neverending Story is an community curated and moderated Ethereum dapp-story">
<link rel="stylesheet" href="assets/css/main.css"/>
</head>
<body>
<div class="grid-container">
<div class="header container">
<h1>The Neverending Story</h1>
<p>A story on the Ethereum blockchain, community curated and moderated through a Decentralized Autonomous Organization (DAO)</p>
</div>
<div class="content container">
<div class="intro">
<h3>Chapter 0</h3>
<p class="intro">It's a rainy night in central London.</p>
</div>
<hr>
<div class="content-submissions">
<div class="submission">
<div class="submission-body">This is an example submission. A proposal for its deletion has been submitted.</div>
<div class="submission-submitter">0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9</div>
<div class="submission-actions">
<div class="deletionproposed" data-votes="3024" data-deadline="1531607200"></div>
</div>
</div>
<div class="submission">
<div class="submission-body">This is a long submission. It has over 244 characters, just we can see what it looks like when rendered in the UI. We need to make sure it doesn't break anything and the layout also needs to be maintained, not clashing with actions/buttons etc.</div>
<div class="submission-submitter">0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9</div>
<div class="submission-actions">
<div class="delete"></div>
</div>
</div>
<div class="submission">
<div class="submission-body">This is an example submission. A proposal for its deletion has been submitted but is looking like it'll be rejected.</div>
<div class="submission-submitter">0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9</div>
<div class="submission-actions">
<div class="deletionproposed" data-votes="-790024" data-deadline="1531607200"></div>
</div>
</div>
</div>
</div>
<div class="events container">
<h3>Latest Events</h3>
<ul class="eventlist">
</ul>
</div>
<div class="information container">
<p>Logged in / out</p>
<div class="avatar">
<img src="http://placeholder.pics/svg/200/DEDEDE/555555/avatar" alt="avatar">
</div>
<dl>
<dt>Contributions</dt>
<dd>0</dd>
<dt>Deletions</dt>
<dd>0</dd>
<dt>Tokens</dt>
<dd>0</dd>
<dt>Proposals submitted</dt>
<dd>0</dd>
<dt>Proposals voted on</dt>
<dd>0</dd>
</dl>
</div>
</div>
<script src="assets/js/web3.min.js"></script>
<script src="assets/js/app.js"></script>
<script src="assets/js/main.js"></script>
</body>
</html>
Note: this is a really really basic skeleton, just to demo integration. Please don’t rely on this being the final product!
注意:这只是一个非常基本的框架,仅用于演示集成。 请不要依赖于最终产品!
It’s possible that you’re missing the dist
folder in the web3
folder. The software is still beta, so minor slip-ups are still possible there. To get around this and install web3 with the dist
folder, run npm install ethereum/web3.js --save
.
您可能会丢失web3
文件夹中的dist
文件夹。 该软件仍处于测试阶段,因此仍可能存在一些小问题。 要解决此问题并使用dist
文件夹npm install ethereum/web3.js --save
,请运行npm install ethereum/web3.js --save
。
For CSS, let’s put something rudimentary into public/assets/css/main.css
:
对于CSS,让我们将一些基本内容放入public/assets/css/main.css
:
@supports (grid-area: auto) {
.grid-container{
display: grid;
grid-template-columns: 6fr 5fr 4fr;
grid-template-rows: 10rem ;
grid-column-gap: 0.5rem;
grid-row-gap: 0.5rem;
justify-items: stretch;
align-items: stretch;
grid-template-areas:
"header header information"
"content events information";
height: 100vh;
}
.events {
grid-area: events;
}
.content {
grid-area: content;
}
.information {
grid-area: information;
}
.header {
grid-area: header;
text-align: center;
}
.container {
border: 1px solid black;
padding: 15px;
overflow-y: scroll;
}
p {
margin: 0;
}
}
body {
padding: 0;
margin: 0;
font-family: sans-serif;
}
Then as JS we’ll start with this in public/assets/js/app.js
:
然后,作为JS,我们将从public/assets/js/app.js
:
var Web3 = require('web3');
var web3 = new Web3(web3.currentProvider);
console.log(web3);
What’s going on here?
这里发生了什么?
Since we agreed that we’ll assume all our users will have MetaMask installed, and MetaMask injects its own instance of Web3 into the DOM of any visited web page, we basically have access to the “wallet provider” from MetaMask right in our website. Indeed, if we log in to MetaMask while the page is open, we’ll see this in the console:
由于我们同意假定所有用户都将安装MetaMask ,并且MetaMask将自己的Web3实例注入到任何访问的网页的DOM中,因此我们基本上可以从我们网站上的MetaMask访问“钱包提供程序”。 确实,如果我们在页面打开时登录到MetaMask,则会在控制台中看到以下内容:
Notice how the MetamaskInpageProvider is active. In fact, if we type web3.eth.accounts
into the console, all the accounts we have access to through MetaMask will be printed out:
请注意MetamaskInpageProvider如何处于活动状态。 实际上,如果我们在控制台中键入web3.eth.accounts
,那么我们通过MetaMask访问的所有帐户都将被打印出来:
This particular account is, however, one that’s added to my own personal Metamask by default and as such will have a balance of 0 eth. It’s not part of our running Ganache or PoA chain:
但是,该特定帐户是默认情况下添加到我自己的个人Metamask中的帐户,因此余额为0 eth。 它不是我们运行的Ganache或PoA链的一部分:
Notice how asking for the balance of our active (MetaMasked) account yields 0, while asking for balance of one of our private blockchain accounts yields 100 ether (in my case it’s Ganache, so all accounts are initialized with 100 ether).
请注意,询问我们的活跃(MetaMasked)账户余额如何产生0,而询问我们的一个私有区块链账户之一的余额如何产生100以太(在我的情况下是Ganache,因此所有账户都以100以太初始化)。
关于语法 (About the syntax)
You’ll notice that the syntax for these calls looks a little odd:
您会注意到,这些调用的语法看起来有些奇怪:
web3.eth.getBalance("0x35d4dCDdB728CeBF80F748be65bf84C776B0Fbaf", function(err, res){console.log(JSON.stringify(res));});
In order to read blockchain data, most MetaMask users will not have a node running locally but will instead request it from Infura or another remote node. Because of this, we can practically count on lag. For this reason, synchronous methods are generally not supported. Instead, everything is done through promises or with callbacks — just like with the deployment step at the beginning of this post. Does this mean you need to be intimately familiar with promises to develop JS for Ethereum? No. It means the following. When doing JS calls in the DOM …
为了读取区块链数据,大多数MetaMask用户将没有本地运行的节点,而是从Infura或另一个远程节点请求它。 因此,我们实际上可以指望滞后。 因此, 通常不支持同步方法 。 相反,所有事情都是通过Promise或通过回调完成的,就像本文开头的部署步骤一样。 这是否意味着您需要非常熟悉为以太坊开发JS的承诺? 不。这意味着以下内容。 在DOM中执行JS调用时...
- always provide a callback function as the last argument to a function you’re calling 始终提供回调函数作为您正在调用的函数的最后一个参数
assume its return values will be twofold: first
error
, thenresult
.假设其返回值将是双重的:第一个
error
,然后是result
。
So, basically, just think of it in terms of a delayed response. When the node responds back with data, the function you defined as the callback function will be called by JavaScript. Yes, this does mean you can’t expect your code to execute line by line as it’s written!
因此,基本上,只需考虑延迟响应即可。 当节点用数据响应时,JavaScript将调用您定义为回调函数的函数。 是的,这确实意味着您不能期望代码在编写时逐行执行!
For more information about promises, callbacks and all that async jazz, see this post.
有关Promise,回调和所有异步Jazz的更多信息,请参阅本文 。
帐户信息 (Account Information)
If we open the website skeleton as presented above, we get something like this:
如果我们按上述方式打开网站框架,则会得到以下信息:
Let’s populate the right-most column about account information with real data.
让我们用真实数据填充最右边的有关帐户信息的列。
届会 (Session)
When the user is not signed into their MetaMask extension, the account list will be empty. When MetaMask isn’t even installed, the provider will be empty (undefined). When they are signed into MetaMask, the provider will be available and offer account info and interaction with the connected Ethereum node (live or Ganache or something else).
当用户未登录其MetaMask扩展名时,帐户列表将为空。 甚至没有安装MetaMask时,提供程序将为空(未定义)。 当他们登录到MetaMask后,该提供程序将可用,并提供帐户信息以及与连接的以太坊节点(实时或Ganache或其他)的交互。
Tip: for testing, you can log out of MetaMask by clicking on your avatar icon in the top right and then selecting Log Out. If the UI doesn’t look like it does in the screenshot below, you might need to activate the Beta UI by opening the menu and clicking on “Try Beta”.
提示:为了进行测试,您可以通过单击右上角的头像图标,然后选择注销来注销MetaMask。 如果用户界面看起来不像下面的屏幕截图所示,则可能需要通过打开菜单并单击“尝试Beta”来激活Beta用户界面。
First, let’s replace all the content of the right status column with a message for the user if they’re logged out:
首先,让我们在注销后用一条消息替换右侧状态列的所有内容:
<div class="information container">
<div class="logged out">
<p>You seem to be logged out of MetaMask or MetaMask isn't installed. Please log into MetaMask - to learn more,
see
<a href="https://bitfalls.com/2018/02/16/metamask-send-receive-ether/">this tutorial</a>.</p>
</div>
<div class="logged in" style="display: none">
<p>You are logged in!</p>
</div>
</div>
The JS to handle this looks like this (in public/assets/js/main.js
):
处理此问题的JS如下所示(在public/assets/js/main.js
):
var loggedIn;
(function () {
loggedIn = setLoggedIn(web3.currentProvider !== undefined && web3.eth.accounts.length > 0);
})();
function setLoggedIn(isLoggedIn) {
let loggedInEl = document.querySelector('div.logged.in');
let loggedOutEl = document.querySelector('div.logged.out');
if (isLoggedIn) {
loggedInEl.style.display = "block";
loggedOutEl.style.display = "none";
} else {
loggedInEl.style.display = "none";
loggedOutEl.style.display = "block";
}
return isLoggedIn;
}
The first part — (function () {
— wraps the bit of logic to be executed once the website loads. So anything inside that will get executed immediately when the page is ready. A single function setLoggedIn
is called and a condition is passed to it. The condition is that:
第一部分- (function () {
-包装了网站加载后要执行的逻辑部分。因此,页面准备就绪后,其中的所有内容都将立即执行。调用单个函数setLoggedIn
并将条件传递给它。条件是:
The currentProvider of the
web3
object is set (i.e. there’s a web3 client present in the website).设置了
web3
对象的currentProvider(即,网站中存在一个web3客户端)。- There’s a non-zero number of accounts available, i.e. an account is available for use via this web3 provider. In other words, we’re logged in to at least one account. 可用的帐户数量非零,即可以通过此web3提供程序使用的帐户。 换句话说,我们已登录至少一个帐户。
If these conditions together evaluate to true
, the setLoggedIn
function makes the “Logged out” message invisible, and the “Logged In” message visible.
如果这些条件的setLoggedIn
为true
,则setLoggedIn
函数将使“注销”消息不可见,而使“登录”消息可见。
All this has the added advantage of being able to use any other web3 provider as well. If a MetaMask alternative shows up eventually, it’ll be instantly compatible with this code because we’re not explicitly expecting MetaMask anywhere.
所有这些还具有能够使用任何其他web3提供程序的附加优点。 如果最终出现了MetaMask替代方案,则它将立即与此代码兼容,因为我们没有明确期望在任何地方使用MetaMask。
帐户头像 (Account avatar)
Because each private key to an Ethereum wallet is unique, it can be used to generate a unique image. This is where colorful avatars like the ones you see in MetaMask’s upper right corner or when using MyEtherWallet come from, though Mist, MyEtherWallet and MetaMask all use different approaches. Let’s generate one for our logged-in user and display it.
由于以太坊钱包的每个私钥都是唯一的,因此可以用来生成唯一的图像。 尽管Mist,MyEtherWallet和MetaMask都使用了不同的方法,但是在这里像您在MetaMask右上角或使用MyEtherWallet时看到的那样,是五颜六色的化身的来源。 让我们为登录用户生成一个并显示它。
The icons in Mist are generated with the Blockies library — but a customized one, because the original has a broken random number generator and can produce identical images for different keys. So to install this one, download this file into one in your assets/js
folder. Then, in index.html
we include it before main.js
:
Mist中的图标是使用Blockies库生成的,但是是自定义的,因为原始库的随机数生成器损坏了,并且可以为不同的键生成相同的图像。 因此,要安装此文件 ,请将此文件下载到assets/js
文件夹中的一个文件中。 然后,在index.html
,将它包括在main.js
之前:
<script src="assets/js/app.js"></script>
<script src="assets/js/blockies.min.js"></script>
<script src="assets/js/main.js"></script>
</body>
We should also upgrade the logged.in
container:
我们还应该升级logged.in
容器:
<div class="logged in" style="display: none">
<p>You are logged in!</p>
<div class="avatar">
</div>
</div>
In main.js
, we kickstart the function.
在main.js
,我们启动了该函数。
if (isLoggedIn) {
loggedInEl.style.display = "block";
loggedOutEl.style.display = "none";
var icon = blockies.create({ // All options are optional
seed: web3.eth.accounts[0], // seed used to generate icon data, default: random
size: 20, // width/height of the icon in blocks, default: 8
scale: 8, // width/height of each block in pixels, default: 4
});
document.querySelector("div.avatar").appendChild(icon);
So we upgrade the logged-in section of the JS code to generate the icon and paste it into the avatar section. We should align that a little with CSS before rendering:
因此,我们升级了JS代码的登录部分,以生成图标并将其粘贴到头像部分。 在渲染之前,我们应该将其与CSS对齐:
div.avatar {
width: 100%;
text-align: center;
margin: 10px 0;
}
Now if we refresh the page when logged in to MetaMask, we should see our generated avatar icon.
现在,如果在登录MetaMask时刷新页面,我们应该会看到生成的头像图标。
帐户余额 (Account balances)
Now let’s output some of the account balance information.
现在,让我们输出一些帐户余额信息。
We have a bunch of read-only functions at our disposal that we developed exclusively for this purpose. So let’s query the blockchain and ask it for some info. To do that, we need to call a smart contract function via the following steps.
我们拥有专门为此目的而开发的一堆只读功能。 因此,让我们查询区块链并询问一些信息。 为此,我们需要通过以下步骤调用智能合约功能 。
1. ABI (1. ABI)
Get the ABI of the contracts whose functions we’re calling. The ABI contains function signatures, so our JS code knows how to call them. Learn more about ABI here.
获取我们正在调用其功能的合同的ABI。 ABI包含函数签名,因此我们的JS代码知道如何调用它们。 在此处了解有关ABI的更多信息。
You can get the ABI of the TNS token and the StoryDAO by opening the build/TNSToken.json
and build/StoryDao.json
files in your project folder after compilation and selecting only the abi
part — so the part between the square brackets [
and ]
:
通过编译后打开项目文件夹中的build/TNSToken.json
和build/StoryDao.json
文件并仅选择abi
部分,即可获取TNS令牌的ABI和StoryDAO,并仅选择abi
部分-因此方括号[
和]
之间的部分:
We’ll put this ABI at the top of our JavaScript code into main.js
like so:
如下所示,我们将这个ABI放在我们JavaScript代码的顶部到main.js
:
Note that the above screenshot shows the abbreviated insertion, collapsed by my code editor (Microsoft Visual Code). If you look at line numbers, you’ll notice that the ABI of the token is 400 lines of code, and the ABI of the DAO is another 1000, so pasting that into this article would make no sense.
请注意,上面的屏幕截图显示了由我的代码编辑器(Microsoft Visual Code)折叠的缩写插入。 如果查看行号,您会注意到令牌的ABI是400行代码,而DAO的ABI是另外1000行,因此将其粘贴到本文中是没有意义的。
2.实例化令牌 (2. Instantiate token)
if (loggedIn) {
var token = TNSToken.at('0x3134bcded93e810e1025ee814e87eff252cff422');
var story = StoryDao.at('0x729400828808bc907f68d9ffdeb317c23d2034d5');
token.balanceOf(web3.eth.accounts[0], function(error, result) {console.log(JSON.stringify(result))});
story.getSubmissionCount(function(error, result) {console.log(JSON.stringify(result))});
//...
We call upon each contract with the address given to us by Truffle and create an instance for each — token
and story
respectively. Then, we simply call the functions (asynchronously as before). The console gives us two zeroes because the account in MetaMask has 0 tokens, and because there are 0 submissions in the story for now.
我们要求每个合同都带有Truffle给我们的地址,并为每个合同分别创建一个实例- token
和story
。 然后,我们简单地调用函数(与之前异步)。 控制台给我们两个零,因为MetaMask中的帐户有0个令牌,并且因为故事中目前有0个提交。
3.读取和输出数据 (3. Read and output data)
Finally, we can populate the user’s profile data with the info we have available.
最后,我们可以使用可用的信息填充用户的个人资料数据。
Let’s update our JavaScript:
让我们更新我们JavaScript:
var loggedIn;
(function () {
loggedIn = setLoggedIn(web3.currentProvider !== undefined && web3.eth.accounts.length > 0);
if (loggedIn) {
var token = TNSToken.at('0x3134bcded93e810e1025ee814e87eff252cff422');
var story = StoryDao.at('0x729400828808bc907f68d9ffdeb317c23d2034d5');
token.balanceOf(web3.eth.accounts[0], function(error, result) {console.log(JSON.stringify(result))});
story.getSubmissionCount(function(error, result) {console.log(JSON.stringify(result))});
readUserStats().then(User => renderUserInfo(User));
}
})();
async function readUserStats(address) {
if (address === undefined) {
address = web3.eth.accounts[0];
}
var User = {
numberOfSubmissions: await getSubmissionsCountForUser(address),
numberOfDeletions: await getDeletionsCountForUser(address),
isWhitelisted: await isWhitelisted(address),
isBlacklisted: await isBlacklisted(address),
numberOfProposals: await getProposalCountForUser(address),
numberOfVotes: await getVotesCountForUser(address)
}
return User;
}
function renderUserInfo(User) {
console.log(User);
document.querySelector('#user_submissions').innerHTML = User.numberOfSubmissions;
document.querySelector('#user_deletions').innerHTML = User.numberOfDeletions;
document.querySelector('#user_proposals').innerHTML = User.numberOfProposals;
document.querySelector('#user_votes').innerHTML = User.numberOfVotes;
document.querySelector('dd.user_blacklisted').style.display = User.isBlacklisted ? 'inline-block' : 'none';
document.querySelector('dt.user_blacklisted').style.display = User.isBlacklisted ? 'inline-block' : 'none';
document.querySelector('dt.user_whitelisted').style.display = User.isWhitelisted ? 'inline-block' : 'none';
document.querySelector('dd.user_whitelisted').style.display = User.isWhitelisted ? 'inline-block' : 'none';
}
async function getSubmissionsCountForUser(address) {
if (address === undefined) {
address = web3.eth.accounts[0];
}
return new Promise(function (resolve, reject) {
resolve(0);
});
}
async function getDeletionsCountForUser(address) {
if (address === undefined) {
address = web3.eth.accounts[0];
}
return new Promise(function (resolve, reject) {
resolve(0);
});
}
async function getProposalCountForUser(address) {
if (address === undefined) {
address = web3.eth.accounts[0];
}
return new Promise(function (resolve, reject) {
resolve(0);
});
}
async function getVotesCountForUser(address) {
if (address === undefined) {
address = web3.eth.accounts[0];
}
return new Promise(function (resolve, reject) {
resolve(0);
});
}
async function isWhitelisted(address) {
if (address === undefined) {
address = web3.eth.accounts[0];
}
return new Promise(function (resolve, reject) {
resolve(false);
});
}
async function isBlacklisted(address) {
if (address === undefined) {
address = web3.eth.accounts[0];
}
return new Promise(function (resolve, reject) {
resolve(false);
});
}
And let’s change the profile info section:
让我们更改个人资料信息部分:
<div class="logged in" style="display: none">
<p>You are logged in!</p>
<div class="avatar">
</div>
<dl>
<dt>Submissions</dt>
<dd id="user_submissions"></dd>
<dt>Proposals</dt>
<dd id="user_proposals"></dd>
<dt>Votes</dt>
<dd id="user_votes"></dd>
<dt>Deletions</dt>
<dd id="user_deletions"></dd>
<dt class="user_whitelisted">Whitelisted</dt>
<dd class="user_whitelisted">Yes</dd>
<dt class="user_blacklisted">Blacklisted</dt>
<dd class="user_blacklisted">Yes</dd>
</dl>
</div>
You’ll notice we used promises when fetching the data, even though our functions are currently just mock functions: they return flat data immediately. This is because each of those functions will need a different amount of time to fetch the data we asked it to fetch, so we’ll await their completion before populating the User object and then passing it on into the render function which updates the info on the screen.
您会注意到,即使当前我们的函数只是模拟函数,我们在获取数据时也使用了promises:它们立即返回平面数据。 这是因为每个这些函数将需要不同的时间来获取我们要求获取的数据,因此在填充User对象之前,我们将等待它们完成,然后将其传递给render函数,以更新有关信息。屏幕。
If you’re unfamiliar with JS promises and would like to learn more, see this post.
如果您不熟悉JS promise,并且想了解更多信息,请参阅这篇文章 。
For now, all our functions are mocks; we’ll need to do some writes before there’s something to read. But first we’ll need to be ready to notice those writes happening!
现在,我们所有的功能都是模拟的。 我们需要先进行一些写操作,然后再进行一些阅读。 但是首先,我们需要准备好注意那些写入的发生!
听事件 (Listening to events)
In order to be able to follow events emitted by the contract, we need to listen for them — as otherwise we’ve put all those emit
statements into the code for nothing. The middle section of the mock UI we built is meant to hold those events.
为了能够跟踪合同发出的事件,我们需要侦听它们-否则,我们会将所有那些emit
语句全部放入代码中。 我们构建的模拟UI的中间部分用于保存这些事件。
Here’s how we can listen to events emitted by the blockchain:
这是我们如何侦听区块链发出的事件的方法:
// Events
var WhitelistedEvent = story.Whitelisted(function(error, result) {
if (!error) {
console.log(result);
}
});
Here we call the Whitelisted
function on the story
instance of our StoryDao contract, and pass a callback into it. This callback is automatically called whenever this given event is fired. So when a user gets whitelisted, the code will automatically log to the console the output of that event.
在这里,我们在StoryDao合约的story
实例上调用Whitelisted
函数,并将回调传递给该函数。 每当触发此给定事件时,都会自动调用此回调。 因此,当用户被列入白名单时,代码将自动将该事件的输出记录到控制台。
However, this will only get the last event of the last block mined by a network. So if there are several Whitelisted events fired from block 1 to 10, it will only show us those in block 10, if any. A better way is to use this approach:
但是,这只会获取网络挖掘的最后一个块的最后一个事件。 因此,如果从块1到10触发了多个列入白名单的事件,它将仅向我们显示块10中的事件(如果有)。 更好的方法是使用这种方法:
story.Whitelisted({}, { fromBlock: 0, toBlock: 'latest' }).get((error, eventResult) => {
if (error) {
console.log('Error in myEvent event handler: ' + error);
} else {
// eventResult contains list of events!
console.log('Event: ' + JSON.stringify(eventResult[0].args));
}
});
Note: put the above into a separate section at the bottom of your JS file, one dedicated to events.
注意:将以上内容放在JS文件底部的单独部分中,一个专门用于事件。
Here, we use the get
function which lets us define the block range from which to fetch events. We use 0 to latest, meaning we can fetch all events of this type, ever. But this has the added problem of clashing with the watching method above. The watching method outputs the last block’s event, and the get
method outputs all of them. We need a way to make the JS ignore double events. Don’t write those you already fetched from history. We’ll do that further down, but for now, let’s deal with whitelisting.
在这里,我们使用get
函数,该函数使我们可以定义从中获取事件的块范围。 我们使用0到最新值,这意味着我们可以获取所有此类事件。 但这又增加了与上述观看方法冲突的问题。 watching方法输出最后一个块的事件,而get
方法输出所有这些事件。 我们需要一种使JS忽略double事件的方法。 不要写那些已经从历史中获取的东西。 我们将继续进行下去,但现在让我们处理白名单。
帐户白名单 (Account whitelisting)
Finally, let’s get to some write operations.
最后,让我们开始一些写操作。
The first and simplest one is getting whitelisted. Remember, to get whitelisted, an account needs to send at least 0.01 ether to the DAO’s address. You’ll get this address on deployment. If your Ganache/PoA chain restarted in between parts of this course, that’s okay, simply re-run the migrations with truffle migrate --reset
and you’ll get the new addresses for both the token and the DAO. In my case, the address of the DAO is 0x729400828808bc907f68d9ffdeb317c23d2034d5
and my token is at 0x3134bcded93e810e1025ee814e87eff252cff422
.
第一个也是最简单的一个已列入白名单。 请记住,要进入白名单,帐户需要向DAO的地址发送至少0.01的以太币。 您将在部署时获得此地址。 如果您的Ganache / PoA链在本课程的两部分之间重新启动,那没关系,只需使用truffle migrate --reset
migration truffle migrate --reset
重新运行迁移,即可获得令牌和DAO的新地址。 在我的情况下,DAO的地址为0x729400828808bc907f68d9ffdeb317c23d2034d5
,我的令牌位于0x3134bcded93e810e1025ee814e87eff252cff422
。
With everything above set up, let’s try sending an amount of ether to the DAO address. Let’s try it with 0.05 ether just for fun, so we can see if the DAO gives us the extra calculated tokens, too, for overpaying.
完成上述所有设置后,让我们尝试向DAO地址发送一定数量的以太。 让我们用0.05乙醚尝试一下,只是为了好玩,所以我们可以看看DAO是否也为我们提供了额外的计算代币,以供多付。
Note: don’t forget to customize the gas amount — just slap another zero on top of the 21000 limit — using the icon marked red. Why is this necessary? Because the function that gets triggered by a simple ether send (the fallback function) executes additional logic which goes beyond 21000, which is enough for simple sends. So we need to up the limit. Don’t worry: anything over this limit is instantly refunded. For a primer on how gas works, see here.
注意:别忘了使用标有红色的图标自定义加油量-只需在21000限制的上方再加一个零即可。 为什么这是必要的? 因为由简单的以太发送触发的功能(后备功能)执行了超过21000的附加逻辑,这对于简单的发送就足够了。 因此,我们需要提高极限。 不用担心:超过此限制的任何款项都会立即退还。 有关气体工作原理的入门资料,请参见此处 。
After the transaction confirms (you’ll see this in MetaMask as “confirmed”), we can check the token amount in our MetaMask account. We’ll need to add our custom token to MetaMask first so it can track them. As per the animation below, the process is as follows: select the MetaMask menu, scroll down to Add Tokens, select Custom Token, paste in the address of the token given to you by Truffle on migration, click Next, see if the balance is fine, and then select Add Tokens.
交易确认后(您将在MetaMask中看到此为“已确认”),我们可以在MetaMask帐户中检查代币金额。 我们需要先将自定义标记添加到MetaMask,以便它可以跟踪它们。 按照下面的动画,过程如下:选择MetaMask菜单,向下滚动到Add Tokens ,选择Custom Token ,粘贴Truffle在迁移时给您的令牌地址,单击Next ,查看余额是否为,然后选择添加令牌 。
For 0.05 eth we should have 400k tokens, and we do.
对于0.05 eth,我们应该有40万个令牌,而且确实如此。
What about the event, though? Were we notified of this whitelisting? Let’s look in the console.
那事件呢? 我们是否收到此白名单的通知? 让我们看一下控制台。
Indeed, the full dataset is there — the address which emitted the event, the block number and hash in which this was mined, and so on. Among all this is the args
object, which tells us the event data: addr is the address being whitelisted, and status is whether it was added to the whitelist or removed from it. Success!
确实,完整的数据集就在那里—发出事件的地址,块的编号和在其中进行挖掘的哈希等等。 其中有一个args
对象,它告诉我们事件数据:addr是被列入白名单的地址,状态是将其添加到白名单还是从白名单中删除。 成功!
If we refresh the page now, the event is again in the console. But how? We didn’t whitelist anyone new. Why did the event fire? The thing with events in EVM is that they are not one-off things like in JavaScript. Sure, they contain arbitrary data and serve as output only, but their output is forever registered in the blockchain because the transaction that caused them is also forever registered in the blockchain. So events will remain after having been emitted, which saves us from having to store them somewhere and recall them on page refresh!
如果现在刷新页面,则该事件再次出现在控制台中。 但是如何? 我们没有将任何新成员列入白名单。 为什么发生该事件? EVM中的事件是,它们不是JavaScript中的一次性事件。 当然,它们包含任意数据并仅用作输出,但是由于导致它们的交易也永远在区块链中注册,因此它们的输出将永久注册在区块链中。 因此,事件在发出后仍将保留,这使我们不必将它们存储在某个地方并在刷新页面时将其召回!
Now let’s add this to the events screen in the UI! Edit the Events section of the JavaScript file like so:
现在,将其添加到UI的事件屏幕中! 像这样编辑JavaScript文件的“事件”部分:
// Events
var highestBlock = 0;
var WhitelistedEvent = story.Whitelisted({}, { fromBlock: 0, toBlock: "latest" });
WhitelistedEvent.get((error, eventResult) => {
if (error) {
console.log('Error in Whitelisted event handler: ' + error);
} else {
console.log(eventResult);
let len = eventResult.length;
for (let i = 0; i < len; i++) {
console.log(eventResult[i]);
highestBlock = highestBlock < eventResult[i].blockNumber ? eventResult[i].blockNumber : highestBlock;
printEvent("Whitelisted", eventResult[i]);
}
}
});
WhitelistedEvent.watch(function(error, result) {
if (!error && result.blockNumber > highestBlock) {
printEvent("Whitelisted", result);
}
});
function printEvent(type, object) {
switch (type) {
case "Whitelisted":
let el;
if (object.args.status === true) {
el = "<li>Whitelisted address "+ object.args.addr +"</li>";
} else {
el = "<li>Removed address "+ object.args.addr +" from whitelist!</li>";
}
document.querySelector("ul.eventlist").innerHTML += el;
break;
default:
break;
}
}
Wow, events got complicated fast, huh? Not to worry, we’ll clarify.
哇,事件很快就复杂了,是吗? 不用担心,我们会澄清。
The highestBlock
variable will remember the latest block fetched from history. We create an instance of our event and attach two listeners to it. One is get
, which gets all events from history and remembers the latest block. The other is watch
, which watches for events “live” and triggers when a new one appears in the most recent block. The watcher only triggers if the block that just came in is bigger than the block we have remembered as highest, making sure that only the new events get appended to the list of events.
highestBlock
变量将记住从历史记录中获取的最新块。 我们创建事件的实例,并为其附加两个侦听器。 一个是get
,它从历史记录中获取所有事件并记住最新的块。 另一个是watch
,它watch
“实时”事件,并在最近的块中出现新事件时触发。 监视者仅在刚进入的块大于我们记得最高的块时才触发,并确保只有新事件才追加到事件列表中。
We also added a printEvent
function to make things easier; we can re-use it for other event types too!
我们还添加了printEvent
函数以使事情变得更容易。 我们也可以将其用于其他事件类型!
If we test this now, indeed, we get it nicely printed out.
如果现在进行测试,确实可以很好地打印出来。
Try doing this yourself now for all the other events our story can emit! See if you can figure out how to handle them all at once rather than having to write this logic for each of them. (Hint: define their names in an array, then loop through those names and dynamically register the events!)
现在就为我们的故事可能发出的所有其他事件尝试自己做! 看看您是否可以弄清楚如何一次处理所有这些,而不必为每个它们编写此逻辑。 (提示:在数组中定义它们的名称,然后遍历这些名称并动态注册事件!)
手动检查 (Manual checking)
You can also manually check the whitelist and all other public parameters of the StoryDAO by opening it in MyEtherWallet and calling its whitelist
function.
您还可以通过在MyEtherWallet中打开StoryDAO并调用其whitelist
功能来手动检查StoryDAO的白名单和所有其他公共参数。
You’ll notice that if we check the account from which we just sent the whitelisting amount we’ll get a true
back, indicating that this account really exists in the whitelist
mapping.
您会注意到,如果我们检查刚刚从中发送白名单金额的帐户,则会得到true
退款,表明该帐户确实存在于whitelist
映射中。
Use this same function menu to experiment with other functions before adding them to the web UI.
在将其他功能添加到Web UI之前,请使用相同的功能菜单尝试其他功能。
提交条目 (Submitting an entry)
At long last, let’s do a proper write-function call from our UI. This time, we’ll submit an entry into our story. First we need to clear the sample entries we put there at the beginning. Edit the HTML to this:
最后,让我们从UI进行适当的write-function调用。 这次,我们将提交一个故事条目。 首先,我们需要清除开头放置的示例条目。 编辑HTML至此:
<div class="content container">
<div class="intro">
<h3>Chapter 0</h3>
<p class="intro">It's a rainy night in central London.</p>
</div>
<hr>
<div class="submission_input">
<textarea name="submission-body" id="submission-body-input" rows="5"></textarea>
<button id="submission-body-btn">Submit</button>
</div>
...
And some basic CSS:
还有一些基本CSS:
.submission_input textarea {
width: 100%;
}
We added a very simple textarea through which users can submit new entries.
我们添加了一个非常简单的文本区域,用户可以通过该文本区域提交新条目。
Let’s do the JS part now.
现在开始做JS部分。
First, let’s get ready to accept this event by adding a new one and modifying our printEvent
function. We can also refactor the whole event section a bit to make it more reusable.
首先,让我们准备通过添加一个新事件并修改我们的printEvent
函数来接受此事件。 我们还可以对整个事件部分进行重构,以使其更具可重用性。
// Events
var highestBlock = 0;
var WhitelistedEvent = story.Whitelisted({}, { fromBlock: 0, toBlock: "latest" });
var SubmissionCreatedEvent = story.SubmissionCreated({}, { fromBlock: 0, toBlock: "latest" });
var events = [WhitelistedEvent, SubmissionCreatedEvent];
for (let i = 0; i < events.length; i++) {
events[i].get(historyCallback);
events[i].watch(watchCallback);
}
function watchCallback(error, result) {
if (!error && result.blockNumber > highestBlock) {
printEvent(result.event, result);
}
}
function historyCallback(error, eventResult) {
if (error) {
console.log('Error in event handler: ' + error);
} else {
console.log(eventResult);
let len = eventResult.length;
for (let i = 0; i < len; i++) {
console.log(eventResult[i]);
highestBlock = highestBlock < eventResult[i].blockNumber ? eventResult[i].blockNumber : highestBlock;
printEvent(eventResult[i].event, eventResult[i]);
}
}
}
function printEvent(type, object) {
let el;
switch (type) {
case "Whitelisted":
if (object.args.status === true) {
el = "<li>Whitelisted address "+ object.args.addr +"</li>";
} else {
el = "<li>Removed address "+ object.args.addr +" from whitelist!</li>";
}
document.querySelector("ul.eventlist").innerHTML += el;
break;
case "SubmissionCreated":
el = "<li>User " + object.args.submitter + " created a"+ ((object.args.image) ? "n image" : " text") +" entry: #" + object.args.index + " of content " + object.args.content+"</li>";
document.querySelector("ul.eventlist").innerHTML += el;
break;
default:
break;
}
}
Now all we need to do to add a brand new event is instantiate it, and then define a case
for it.
现在,我们添加一个全新事件所需要做的就是实例化它,然后为其定义一个case
。
Next, let’s make it possible to make a submission.
接下来,让我们进行提交成为可能。
document.getElementById("submission-body-btn").addEventListener("click", function(e) {
if (!loggedIn) {
return false;
}
var text = document.getElementById("submission-body-input").value;
text = web3.toHex(text);
story.createSubmission(text, false, {value: 0, gas: 400000}, function(error, result) {
refreshSubmissions();
});
});
function refreshSubmissions() {
story.getAllSubmissionHashes(function(error, result){
console.log(result);
});
}
Here we add an event listener to our submission form which, once submitted, first rejects everything if the user isn’t logged in, then grabs the content and converts it to hex format (which is what we need to store values as bytes
).
在这里,我们在提交表单中添加了一个事件侦听器,一旦提交,一旦用户未登录,它会首先拒绝所有内容,然后获取内容并将其转换为十六进制格式(这就是我们需要将值存储为bytes
)。
Lastly, it creates a transaction by calling the createSubmission
function and providing two params: the text of the entry, and the false
flag (meaning, not an image). The third argument is the transaction settings: value means how much ether to send, and gas means how much of a gas limit you want to default to. This can be changed in the client (MetaMask) manually, but it’s a good starting point to make sure we don’t run into a limit. The final argument is the callback function which we’re already used to by now, and this callback will call a refresh function which loads all the submissions of the story. Currently, this refresh function only loads story hashes and puts them into the console so we can check that everything works.
最后,它通过调用createSubmission
函数并提供两个参数来创建事务:输入的文本和false
标志(意味着,不是图像)。 第三个参数是交易设置:value表示要发送多少以太,gas表示要默认设置为多少gas限制。 可以在客户端(MetaMask)中手动更改此设置,但这是确保我们不会遇到限制的一个很好的起点。 最后一个参数是我们现在已经习惯的回调函数,此回调将调用刷新函数,该函数将加载故事的所有提交内容。 当前,此刷新功能仅加载故事哈希并将其放入控制台,因此我们可以检查一切是否正常。
Note: ether amount is 0 because the first entry is free. Further entries will need ether added to them. We’ll leave that dynamic calculation up to you for homework. Tip: there’s a calculateSubmissionFee
function in our DAO for this very purpose.
注意:以太币数量为0,因为第一个条目是免费的。 其他条目将需要添加乙醚。 我们会将动态计算留给您来做作业。 提示:为此,我们在DAO中有一个calculateSubmissionFee
函数。
At this point, we need to change something at the top of our JS where the function auto-executes on page load:
此时,我们需要在JS顶部更改一些功能,该功能在页面加载时自动执行:
if (loggedIn) {
token.balanceOf(web3.eth.accounts[0], function(error, result) {console.log(JSON.stringify(result))});
story.getSubmissionCount(function(error, result) {console.log(JSON.stringify(result))});
web3.eth.defaultAccount = web3.eth.accounts[0]; // CHANGE
readUserStats().then(User => renderUserInfo(User));
refreshSubmissions(); // CHANGE
} else {
document.getElementById("submission-body-btn").disabled = "disabled";
}
The changes are marked with // CHANGE
: the first one lets us set the default account from which to execute transactions. This will probably be made default in a future version of Web3. The second one refreshes the submissions on page load, so we get a fully loaded story when the site opens.
更改标记为// CHANGE
:第一个// CHANGE
使我们设置执行交易的默认帐户。 Web3的未来版本中可能会将其设置为默认值。 第二个页面刷新页面上提交的内容,因此当网站打开时,我们得到了一个完整的故事。
If you try to submit an entry now, MetaMask should open as soon as you click Submit and ask you to confirm submission.
如果您现在尝试提交条目,则MetaMask应该在您单击“ 提交”并要求您确认提交后立即打开。
You should also see the event printed out in the events section.
您还应该在事件部分看到打印的事件。
The console should echo out the hash of this new entry.
控制台应回显此新条目的哈希值。
Note: MetaMask currently has a problem with private network and nonces. It’s described here and will be fixed soon, but in case you get the nonce
error in your JavaScript console when submitting entries, the stopgap solution for now is to re-install MetaMask (disabling and enabling will not work). REMEMBER TO BACK UP YOUR SEED PHRASE FIRST: you’ll need it to re-import your MetaMask accounts!
注意:MetaMask当前在专用网络和随机数方面存在问题。 它已在此处进行了描述,将很快得到修复,但是如果您在提交条目时在JavaScript控制台中遇到了nonce
错误,则目前的权宜之计是重新安装MetaMask(禁用和启用将不起作用)。 请记住首先备份种子短语:您将需要它来重新导入您的MetaMask帐户!
Finally, let’s fetch these entries and display them. Let’s start with a bit of CSS:
最后,让我们获取这些条目并显示它们。 让我们从一些CSS开始:
.content-submissions .submission-submitter {
font-size: small;
}
Now let’s update the refreshSubmissions
function:
现在,让我们更新refreshSubmissions
函数:
function refreshSubmissions() {
story.getAllSubmissionHashes(function (error, result) {
var entries = [];
for (var i = 0; i < result.length; i++) {
story.getSubmission(result[i], (err, res) => {
if (res[2] === web3.eth.accounts[0]) {
res[2] = 'you';
}
let el = "";
el += '<div class="submission">';
el += '<div class="submission-body">' + web3.toAscii(res[0]) + '</div>';
el += '<div class="submission-submitter">by: ' + res[2] + '</div>';
el += '</div>';
el += '</div>';
document.querySelector('.content-submissions').innerHTML += el;
});
}
});
}
We roll through all the submissions, get their hashes, fetch each one, and output it on the screen. If the submitter is the same as the logged-in user, “you” is printed instead of the address.
我们浏览所有提交的内容,获取其哈希值,获取每个哈希值,然后将其输出到屏幕上。 如果提交者与登录用户相同,则会打印“ you”而不是地址。
Let’s add another entry to test.
让我们添加另一个条目进行测试。
结论 (Conclusion)
In this part, we developed the beginnings of a basic front end for our DApp.
在这一部分中,我们为DApp开发了一个基本的前端。
Since developing the full front-end application could just as well be a course of its own, we’ll leave further development up to you as homework. Just call the functions as demonstrated, tie them into a regular JavaScript flow (either via a framework like VueJS or plain old jQuery or raw JS like we did above) and bind it all together. It’s literally like talking to a standard server API. If you do get stuck, check out the project repo for the code!
由于开发完整的前端应用程序本身也可能是一个过程,因此我们会将进一步的开发工作留给您作为家庭作业。 只需调用演示的函数,将它们绑定到常规JavaScript流中即可(通过像我们上面那样的框架通过VueJS或普通的旧jQuery或原始JS进行绑定)。 从字面上看,就像与标准服务器API交谈一样。 如果确实卡住了,请查看项目存储库中的代码!
Other upgrades you can do:
您可以执行的其他升级:
- detect when the web3 provider changes or when the number of available accounts changes, indicating log-in or log-out events and auto-reload the page 检测web3提供程序何时更改或可用帐户数何时更改,以指示登录或注销事件并自动重新加载页面
- prevent the rendering of the submission form unless the user is logged in 除非用户登录,否则阻止提交提交表单
- prevent the rendering of the vote and delete buttons unless the user has at least 1 token, etc. 除非用户至少有1个令牌等,否则禁止呈现投票和删除按钮。
- let people submit and render Markdown! 让人们提交和渲染Markdown!
- order events by time (block number), not by type! 按时间(块编号)而不是类型订购事件!
- make events prettier and more readable: instead of showing hex content, translate it to ASCII and truncate to 30 or so characters 使事件更美观,更易读:将其转换为ASCII并截断为30个左右的字符,而不是显示十六进制内容
- use a proper JS framework like VueJS to get some reusability out of your project and to have better structured code. 使用像VueJS这样的适当JS框架来使项目具有一定的可重用性,并具有更好的结构化代码。
In the next and final part, we’ll focus on deploying our project to the live internet. Stay tuned!
在下一个也是最后一部分,我们将重点放在将项目部署到实时Internet上。 敬请关注!
翻译自: https://www.sitepoint.com/building-ethereum-dapps-web3-ui-dao-contract/
da-dapps