以太坊构建DApps系列教程(七):为DAO合约构建Web3 UI

在本系列关于使用以太坊构建DApps教程的第6部分中,我们通过添加投票,黑名单,股息分配和撤销来完成DAO合约,同时投入一些额外的辅助函数以实现良好的标准。在本教程中,我们将构建一个用于与我们的故事Story交互的Web界面,否则我们无法统计用户如何参与。所以这是我们故事Story发布之前的最后一部分。

由于这不是一个Web应用程序教程,我们将保持非常简单。下面的代码不是生产就绪的,只是作为如何将JavaScript连接到区块链的概念证明。但首先,让我们添加一个新的迁移。

自动转移

现在,当我们部署代币和DAO时,它们位于区块链上但不进行交互。为了测试我们构建的内容,我们需要手动将代币所有权和余额转移到DAO,这在测试期间可能很乏味。

让我们写一个新的迁移,为我们做这件事。创建文件4_configure_relationship.js并将以下内容放在其中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
});
}

这是这段代码的作用。首先,你会注意到它是基于promise的。它充满了各种调用。这是因为我们在调用下一个数据之前依赖于返回一些数据的函数。所有合约调用都是基于promise的,这意味着它们不会立即返回数据,因为Truffle需要向节点请求信息,因此promise在将来返回数据。我们强制代码等待这些数据,使用then关键词并提供所有then调用函数,这些函数在最终给出时将使用此结果调用。

所以,按顺序:

  • 首先,向节点询问已部署代币的地址并将其返回。
  • 然后,接受此数据,将其保存到全局变量中,并询问已部署的DAO的地址并将其返回。
  • 然后,接受这些数据,将其保存到全局变量中,并询问代币合约的所有者将在其帐户中具有的余额,这在技术上是总供应量,并返回此数据。
  • 然后,一旦你得到这个余额,用它来调用这个代币的transfer函数,并将令牌发送到DAO的地址并返回结果。
  • 然后,忽略返回的结果——我们只想知道它何时完成——最后将代币的所有权转移到DAO的地址,返回数据但不丢弃它。

运行truffle migrate --reset现在应该产生这样的输出:

前端

前端是一个常规的静态HTML页面,其中包含一些JavaScript用于与区块链和一些CSS进行通信以使页面变得不那么难看。

让我们在子文件夹public创建一个文件index.html,并为其提供以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<!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>

注意:这是一个非常基本的框架,仅用于演示集成。请不要把这当成最终产品!

你可能缺少web3文件夹中的dist文件夹。该软件仍处于测试阶段,因此仍有可能出现轻微漏洞。要解决此问题并使用dist文件夹,请运行npm install ethereum/web3.js --save

对于CSS,让我们在public/assets/css/main.css一些基本内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@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;
}

然后作为JS,我们将在public/assets/js/app.js

1
2
3
4
var Web3 = require('web3');

var web3 = new Web3(web3.currentProvider);
console.log(web3);

这里发生了什么?

既然我们假设所有用户都安装了MetaMask,并且MetaMask将自己的Web3实例注入到任何访问过的网页的DOM中,我们基本上可以访问我们网站上MetaMask的wallet provider。实际上,如果我们在页面打开时登录MetaMask,我们将在控制台中看到:

注意MetamaskInpageProvider是如何激活的。实际上,如果我们在控制台中键入web3.eth.accounts,我们将通过MetaMask访问的所有帐户都将打印出来:

但是,这个特殊帐户默认添加到我自己的个人Metamask中,因此余额为0eth。它不是我们运行的Ganache或PoA链的一部分:

请注意,如果要求我们的MetaMask活动帐户的余额产生0,同时要求我们的一个私有区块链帐户的余额产生100以太(在我的情况下它是Ganache,所以所有帐户都用100以太初始化)。

关于语法

你会注意到这些调用的语法看起来有点奇怪:

1
web3.eth.getBalance("0x35d4dCDdB728CeBF80F748be65bf84C776B0Fbaf", function(err, res){console.log(JSON.stringify(res));});

为了读取区块链数据,大多数MetaMask用户不会在本地运行节点,而是从Infura或其他远程节点请求它。因此,我们实际上可以依靠回调。因此,通常不支持同步方法。相反,一切都是通过promise或回调来完成的——就像本文开头的部署步骤一样。这是否意味着你需要非常熟悉为以太坊开发JS的promise?不,这意味着以下内容。在DOM中进行JS调用时……

  • 总是提供一个回调函数作为你正在调用的函数的最后一个参数。
  • 假设它的返回值是双重的:第一个error,然后是result

所以,基本上,只需考虑延迟响应就可以了。当节点响应数据时,你定义为回调函数的函数将由JavaScript调用。是的,这意味着你不能指望你的代码在编写时逐行执行!

有关promises,回调和所有async jazz的更多信息,请参阅此文章

帐户信息

如果我们打开上面提到的网站骨架,我们得到这样的东西:

让我们用真实数据填充关于帐户信息的最右侧列。

session

当用户未登录其MetaMask扩展名时,帐户列表将为空。如果甚至没有安装MetaMask,则提供程序将为空(未定义)。当他们登录MetaMask时,接口将可用并提供帐户信息以及与连接的以太坊节点(live或Ganache或其他)的交互。

提示:要进行测试,你可以通过单击右上角的头像图标然后选择注销来注销MetaMask。如果用户界面看起来不像下面的屏幕截图,你可能需要通过打开菜单并单击“试用Beta”来激活Beta用户界面。

首先,如果用户已注销,请将该状态列的所有内容替换为用户的消息:

1
2
3
4
5
6
7
8
9
10
<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>

处理它的JS看起来像这样(在public/assets/js/main.js):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}

第一部分——(function () { -包含一旦网站加载就要执行的逻辑。因此,当页面准备就绪时,内部的任何内容都会立即执行。调用单个函数setLoggedIn并将条件传递给它条件是:

  • 设置web3对象的currentProvider(即网站中存在web3客户端)。
  • 可用的帐户数量非零,即可通过此Web3提供商使用帐户。 换句话说,我们已登录至少一个帐户。

如果这些条件一起评估为true,则setLoggedIn函数使“Logged out”消息不可见,并且“Logged In”消息可见。

所有这些都具有能够使用任何其他web3提供商的额外优势。如果最终出现MetaMask替代方案,它将立即与此代码兼容,因为我们并未明确期望任何地方的MetaMask。

帐户头像

因为以太坊钱包的每个私钥都是唯一的,所以它可用于生成独特的图像。这是你在MetaMask的右上角或使用MyEtherWallet时看到的彩色化身,尽管Mist,MyEtherWallet和MetaMask都使用不同的方法。让我们为登录用户生成一个并显示它。

Mist中的图标是使用Blockies库生成的——是自定义的,因为原始文件具有损坏的随机数生成器,并且可以为不同的键生成相同的图像。因此,要安装此文件,请将此文件下载到assets/js文件夹中。然后,在index.html我们在main.js之前包含它:

1
2
3
4
5
 <script src="assets/js/app.js"></script>
<script src="assets/js/blockies.min.js"></script>
<script src="assets/js/main.js"></script>

</body>

我们还应该升级logged.in容器:

1
2
3
4
5
6
<div class="logged in" style="display: none">
<p>You are logged in!</p>
<div class="avatar">

</div>
</div>

在main.js,我们启动该功能。

1
2
3
4
5
6
7
8
9
10
11
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);

因此,我们升级JS代码的登录部分以生成图标并将其粘贴到头像部分。我们应该在渲染之前将它与CSS稍微对齐:

1
div.avatar { width: 100%; text-align: center; margin: 10px 0; }

现在,如果我们在登录MetaMask时刷新页面,我们应该会看到生成的头像图标。

帐户余额

现在让我们输出一些帐户余额信息。

我们拥有一系列只读功能,我们专门为此目的而开发。所以让我们查询区块链并询问一些信息。为此,我们需要通过以下步骤调用智能合约功能

1.ABI

获取我们正在调用的函数的合约的ABI。ABI包含函数签名,因此我们的JS代码知道如何调用它们。在此处了解有关ABI的更多信息。

你可以通过在编译后打开项目文件夹中的build/TNSToken.jsonbuild/StoryDao.json文件并仅选择abi部分来获取TNS代币和StoryDAO的ABI([]方括号之间的部分):

我们将这个ABI放在我们的JavaScript代码的顶部,进入main.js如下所示:

请注意,上面的屏幕截图显示了我的代码编辑器(Microsoft Visual Code)折叠的缩写插入。如果你查看行号,你会注意到令牌的ABI是400行代码,而DAO的ABI是另外1000行,所以将它粘贴到本文中是没有意义的。

2.实例化代币

1
2
3
4
5
6
7
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))});
//...

我们使用Truffle给我们的地址调用每个合约,并分别为每个tokenstory创建一个实例。然后,我们简单地调用函数(与以前一样异步)。控制台给我们两个零,因为MetaMask中的帐户有0个代币,因为现在故事story中有0个提交。

3.读取和输出数据

最后,我们可以使用我们提供的信息填充用户的个人资料数据。

让我们更新我们的JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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);
});
}

让我们更改个人资料信息部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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>

你会注意到我们在获取数据时使用了promises,即使我们的函数当前只是模拟函数:它们会立即返回平面数据。这是因为每个函数都需要不同的时间来获取我们要求它获取的数据,因此我们将在填充User对象之前等待它们完成,然后将其传递给render函数,该函数更新了屏幕。

如果您对JS承诺不熟悉并希望了解更多信息,请参阅此帖子

现在,我们所有的功能都是嘲笑; 我们需要先做一些写操作才能阅读。 但首先我们需要准备好注意那些写作的发生!

监听事件

为了能够跟踪合约发出的事件,我们需要监听它们——否则我们将所有这些emit语句都放入代码中。我们构建的模拟UI的中间部分用于保存这些事件。

以下是我们如何监听区块链发出的事件:

1
2
3
4
5
6
7
// Events

var WhitelistedEvent = story.Whitelisted(function(error, result) {
if (!error) {
console.log(result);
}
})

这里我们在StoryDao合约的story实例上调用Whitelisted函数,并将回调传递给它。每当触发此给定事件时,将自动调用此回调。因此,当用户被列入白名单时,代码将自动将该事件的输出记录到控制台。

但是,这只会获取网络挖掘的最后一个块的最后一个事件。因此,如果从第1块到第10块触发了几个白名单事件,它只会向我们展示第10块中的那些事件,如果有的话。更好的方法是使用这种方法:

1
2
3
4
5
6
7
8
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));
}
});

注意:将上面的内容放在JS文件底部的一个单独的部分,一个专门用于事件。

在这里,我们使用get函数,它允许我们定义从中获取事件的块范围。我们使用0到最新,这意味着我们可以获取此类型的所有事件。但是这增加了与上述监听方法发生冲突的可能。监听方法输出最后一个块的事件,get方法输出所有这些事件。我们需要一种方法来使JS忽略双重事件。不要写那些你已经从历史中获取的东西。我们会进一步做到这一点,但就目前而言,让我们来处理白名单。

帐户白名单

最后,让我们进行一些写操作。

第一个也是最简单的一个是白名单。请记住,要获得白名单,帐户需要向DAO的地址发送至少0.01以太。你将在部署时获得此地址。如果你的Ganache/PoA链在本课程的各个部分之间重新启动,那没关系,只需使用truffle migrate --reset重新运行,你就可以获得代币和DAO的新地址。在我的例子中,DAO的地址是0x729400828808bc907f68d9ffdeb317c23d2034d5,我的代币是0x3134bcded93e810e1025ee814e87eff252cff422

设置完所有内容后,让我们尝试向DAO地址发送一定数量的以太。让我们尝试0.05以太只是为了好玩,所以我们可以看看DAO是否为我们提供额外的计算代币,以支付超额费用。

注意:不要忘记自定义gas量——只需在21000限制之上再拍一个零——使用标记为红色的图标。为什么这有必要?因为由简单的ether发送(回调函数)触发的函数执行超过21000的额外逻辑,这对于简单发送就足够了。所以我们需要达到极限。不要担心:超出此限制的任何内容都会立即退款。有关gas如何工作的入门读物,请参见[https://www.sitepoint.com/ethereum-transaction-costs]。

在交易确认后(你将在MetaMask中将其视为“已确认”),我们可以在MetaMask帐户中检查代币金额。我们首先需要将自定义代币添加到MetaMask中,以便跟踪它们。根据下面的动画,过程如下:选择MetaMask菜单,向下滚动到Add Tokens,选择Custom Token,粘贴Truffle在迁移时给你的代币地址,点击Next,查看余额是否为ok,然后选择添加Add Tokens

对于0.05 eth,我们应该有400k令牌,我们这样做。

但是这个事件怎么样?我们收到了这个白名单的通知吗?我们来看看控制台吧。

实际上,完整的数据集就在那​​里——发出事件的地址,块数和挖掘它的hash,等等。其中包括args对象,它告诉我们事件数据:addr是被列入白名单的地址,状态是它是添加到白名单还是从中删除。成功!

如果我们现在刷新页面,则事件再次出现在控制台中。但是怎么样?我们没有将任何新人列入白名单。为什么事件会发生警告?EVM中的事件是它们不像JavaScript那样是一次性的事情。当然,它们包含任意数据并仅用作输出,但它们的输出永远在区块链中注册,因为导致它们的交易也永久地在区块链中注册。因此事件将在发出之后保留,这使我们不必将它们存储在某处并在页面刷新时调用它们!

现在让我们将其添加到UI中的事件屏幕!编辑JavaScript文件的Events部分,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 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;
}
}

哇,变得很快,是吧?不用担心,我们会澄清。

highestBlock变量将记住从历史记录中获取的最新块。我们创建了一个事件的实例,并为它附加了两个监听器。一个是get,它从历史记录中获取所有事件并记住最新的块。另一个是watchwatch事件“实时”并在最近一个块中出现新事件时触发。只有当刚刚进入的块大于我们记忆中最高的块时,观察者才会触发,确保只有新事件被附加到事件列表中。

我们还添加了一个printEvent函数执行打印事件的操作; 我们也可以将它重复用于其他类型的事件!

如果我们现在测试它,确实,我们可以很好地打印出来。

现在尝试自己做这个,我们的故事Story可以发出的所有其他事件!看看你是否可以弄清楚如何一次处理它们,而不必为每个都写出这个逻辑。(提示:在数组中定义它们的名称,然后遍历这些名称并动态注册事件!)

手动检查

你还可以通过在MyEtherWallet中打开并调用其whitelist函数来手动检查StoryBAO的白名单和所有其他公共参数。

你会注意到,如果我们检查刚刚发送白名单金额的帐户,我们将获得true回复,表明此帐户确实存在于whitelist映射中。

在将其添加到Web UI之前,使用此相同的功能菜单来试验其他功能。

提交参赛作品

最后,让我们从UI进行正确的写函数调用。这一次,我们将在故事Story中提交一个条目。首先,我们需要清除我们在开始时放在那里的示例条目。编辑HTML到这个:

1
2
3
4
5
6
7
8
9
10
11
<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>
...

还有一些基本的CSS:

1
2
3
.submission_input textarea {
width: 100%;
}

我们添加了一个非常简单的textarea,用户可以通过它提交新条目。

我们现在来做JS部分吧。

首先,让我们准备通过添加一个新事件并修改我们的printEvent函数来接受这个事件。我们还可以对整个事件部分进行一些重构,以使其更具可重用性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 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;
}
}

现在我们需要做的就是添加一个全新的事件来实例化它,然后为它定义一个case。

接下来,让我们提交。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
});
}

在这里,我们向提交表单添加一个事件监听器,一旦提交,首先拒绝所有用户未登录的内容,然后抓取内容并将其转换为十六进制格式(这是我们需要将值存储为bytes )。

最后,它通过调用createSubmission函数并提供两个参数来创建交易:条目的文本和false标记(意思即不是图像)。第三个参数是交易设置:值表示要发送多少以太,而gas表示你想要默认的gas限制量。这可以在客户端(MetaMask)中手动更改,但这是一个很好的起点,以确保我们不会遇到限制。最后一个参数是我们现在已经习惯的回调函数,这个回调函数将调用一个刷新函数来加载故事Story的所有提交。目前,此刷新功能仅加载故事story哈希并将它们放入控制台,以便我们检查一切是否正常。

注意:以太量为0,因为第一个条目是免费的。进一步的条目将需要添加以太币。我们将动态计算留给你当作业。提示:为此目的,我们的DAO中有一个calculateSubmissionFee函数。

此时,我们需要在JS的顶部更改一些在页面加载时自动执行的函数:

1
2
3
4
5
6
7
8
9
10
11
12
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";
}

更改标记为//CHANGE:第一个允许我们设置执行交易的默认帐户。这可能会在未来的Web3版本中默认使用。第二个刷新页面加载时提交的内容,因此我们在网站打开时获得一个完整的故事story。

如果你现在尝试提交条目,MetaMask应在你单击“提交”后立即打开,并要求你确认提交。

你还应该在事件部分中看到事件打印出来。

控制台应该回显这个新条目的哈希值。

注意:MetaMask目前在私有网络和nonce方面存在问题。它在这里描述并将很快修复,但如果nonce在提交条目时在JavaScript控制台中收到错误,那么目前的权宜之计解决方案是重新安装MetaMask(禁用和启用将不起作用)。请记住首先备份你的种子SEED:你需要它来重新导入你的MetaMask帐户!

最后,让我们获取这些条目并显示它们。让我们从一些CSS开始:

1
.content-submissions .submission-submitter { font-size: small; }

现在让我们更新一下这个refreshSubmissions功能:

1
2


我们浏览所有提交内容,获取它们的哈希值,获取每个哈希值,然后在屏幕上输出。如果提交者与登录用户相同,则打印“你”而不是地址。

让我们添加另一个条目进行测试。

结论

在这一部分中,我们为DApp开发了基本前端的开端。

由于开发完整的前端应用程序也可以成为它自己的一个过程,我们将作为家庭作业留给你进一步的发展。只需调用所演示的函数,将它们绑定到常规JavaScript流程中(通过像VueJS这样的框架或普通的旧jQuery或像我们上面所做的原生JS)并将它们绑定在一起。它实际上就像与标准服务器API交谈。如果你遇到困难,请查看代码的项目仓库!

可以执行的其他升级:

  • 检测web3提供程序何时更改或可用帐户数何时更改,指示登录或注销事件并自动重新加载页面。
  • 除非用户已登录,否则将阻止呈现提交表单。
  • 防止呈现投票和删除按钮,除非用户至少有1个代币等。
  • 让人们提交并呈现Markdown!
  • 按时间(块号)订购事件,而不是按类型订购!
  • 使事件更漂亮,更可读:不是显示十六进制内容,而是将其翻译为ASCII并截断为30个左右的字符。
  • 使用像VueJS这样的适当的JS框架来从项目中获得一些可重用性并获得更好的结构化代码。

在下一部分和最后一部分中,我们将专注于将我们的项目部署到实时互联网。敬请关注!

======================================================================

分享一些以太坊、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系列教程(七):为DAO合约构建Web3 UI