文: Xwang
本文原创,转载请注明作者及出处
上一篇文章我们介绍了一些DAPP基本原理与基础开发知识,以及从实战的角度从零开始实现了一个基于区块链简单投票系统,这篇文章会接着 上一篇继续开发,并且引入数字通证(Token)的概念。
一.引入数字代币进行第三次迭代
1.1
在以太坊中,一个重要概念就是通证(token),也就是常说的加密数字币,或者代币,比如以下这些。

通证就是在以太坊上构建的一种数字资产,可以用它来表示现实世界里的东西,比如黄金,或者是自己的数字资产(就像货币一样)。实际上就是智能合约,并没有什么太多的神奇之处,举个栗子:
-
黄金通证:银行可以有1千克的黄金储备,然后发行1千个通证。买100个黄金通证就等于买100克的黄金。
-
公司股票:公司股票可以用以太坊上的代币来表示。通过支付以太币,人们可以购买公司股票。
-
游戏币:在一个多玩家游戏中,游戏者可以用以太币购买游戏币,并在游戏中进行消费。
-
忠诚度积分:商店可以给购物者发行通证作为忠诚度积分,它可以在将来作为现金回收,或是在第三方市场售卖。
在合约中如何实现通证,实际上并没有限制。但是,以太坊有一个叫做 ERC20的通证标准,该标准还在不断进化中。 ERC20通证的优点是很容易与其他的符合 ERC20标准的通证进行交换,同时,也更容易将你的通证集成到其他DApp中。 总的来说,后续将讨论以下内容:
-
学习并掌握新的数据类型,比如结构体(struct),以便在区块链上组织和存储数据
-
理解通证概念并实现投票应用的通证
-
学习使用以太币进行支付,以太币是以太坊区块链平台的数字加密货币。
提到投票,通常会想起普通的选举,例如,通过投票来选出一个国家的首相或总统。在这种情况下,每个公民都会有一票,可以投给他们支持的候选人。
还有另外一种 加权投票(weighted voting),它常常用于公开上市交易的公司。 在这些公司,股东的投票权取决于其持有的股票数量。比如,如果你拥有 10,000 股公司股票,你就有 10,000 个投票权(而不是普通选举中的一票)。
例如,假设有一个叫做Block的上市公司。公司有 3 个空闲职位,分别是总裁、副总裁和部长,以及一组候选人。该公司希望通过股东投票的方式来决定哪个候选人得到哪个职位。获得最高票数的候选人将会成为总裁,然后是副总裁,最后是部长。
针对这个应用场景,我们可以构建一个DApp来发行公司股票,该应用允许任何人购买股票从而成为股东。 股东基于其拥有的股票数为候选人投票。例如,如果你持有10,000 股,你可以一个候选人投 5,000 股, 另一个候选人 3,000 股,第三个候选人 2,000 股。
以下是我们将要在本章实现应用的图示,任何人都可以调用合约的 buy()方法来购买公司发行的股票通证,然后就可以调用合约的 voteForCandidate()方法为特定的候选人投票:

1.2
综上,我们可以按以下思路来实现加权投票应用:
首先初始化一个新的truffle项目,然后修改关键代码文件:
-
投票合约:Voting.sol
-
合约迁移脚本:2deploycontracts.js
-
前端代码:index.html、app.js和app.css
在部署合约时初始化参与竞争的候选人名单。我们前面已经知道了如何实现这一点,在迁移脚本 2_deploy_contracs.js中完成这个任务。
由于投票人需要先持有公司股票。所以,我们还需要在部署合约时初始化公司发行的股票总量。 这些股票就是构成公司的数字资产。在以太坊的世界中,这些数字资产被称为通证 (Token)。 因此,从现在开始,我们将会把这些股票称为股票通证。
当然,股票可以看做是一种通证,但是并非所有的以太坊通证都是股票。股票仅仅是我们前一节中提到的通证使用场景的一种。
我们还需要向投票合约中增加一个新的方法,以便任何人都可以购买这些通证。容易理解,投票人给候选人投票时将使用(消耗)这些股票通证。
接下来还需要添加一个方法来查询投票人的信息,以及他们分别给谁投了票、总共持有多少股票通证、 还有多少可用的通证余额等等。
为了跟踪所有这些数据,我们需要使用几个mapping类型的字段,同时还需要引入新的数据类型 struct(结构体)来组织投票人信息。
和原来一样,我们使用truffle的 webpack项目模版来初始化一个新项目, 并从contracts目录下移除无用的合约文件:
~$ mkdir -p ~/repo/tkapp
~$ cd ~/repo/tkapp
~/repo/tkapp$ truffle unbox webpack
~/repo/tkapp$ rm contracts/ConvertLib.sol contracts/MetaCoin.sol
1.3
新的合约设计如下

之前的投票合约仅仅包含两个状态:数组 candidateList保存候选人名单,字典 votesReceived跟踪每个候选人获得的投票。
在加权投票合约中,我们需要额外跟踪一些数据:
-
投票人信息:solidity的
结构体( struct)类型可以将相关数据组织在一起(类似于JAVA BEAN)。用结构体来存储投票人信息非常好。我们将使用一个struct来存储投票人的账户、已经购买的股票通证数量以及给每个候选人投票时所用的股票数量。例如:
struct voter {
address voterAddress; //投票人账户地址
uint tokensBought; //投票人持有的股票通证总量
uint[] tokensUsedPerCandidate; //为每个候选人消耗的股票通证数量
}
-
投票人信息字典:使用一个mapping字典来保存所有的投票人信息,键为投票人账户地址,值为投票人信息。 这样给定一个投票人的账户地址,就可以很方面地提取他的相关信息。我们使用voterInfo来表示该字典。 例如:
mapping (address => voter) public voterInfo。
-
股票通证的相关信息:使用
totalTokens来保存通证发行总量,balanceTokens保存通证余额,tokenPrice保存通证的价格。
在部署合约时,除了指定候选人名单,我们还需要声明股票通证发行总量和股票单价。 因此在合约的构造函数中,需要补充声明这些参数。例如:
contract Voting{
function Voting(uint tokens, uint pricePerToken, bytes32[] candidateNames) public {}
}
当股东调用 voteForCandidate()方法投票给特定候选人时,还需要声明其支持力度 —— 用多少股票来支持 这个候选人。因此,我们需要为该方法添加额外的参数以便传入股票通证数量。例如:
contract Voting{
function voteForCandidate(bytes32 candidate, uint votesInTokens) public {}
}
任何人都可以调用 buy()方法来购买公司发行的股票通证,从而成为公司的股东并获得投票权。 你应该已经注意到了该方法的 payable修饰符。在Sodility合约中,只有声明为 payable的方法, 才可以接收支付的货币( msg.value值)。
-
contract
Voting{
-
function buy() payable
public returns
(uint)
{
-
//使用msg.value来读取用户的支付金额,这要求方法必须具有payable声明
-
}
-
}
所有合约代码可以在git目录中查看。
1.4
合约中的 buy()方法用于提供购买股票的接口。注意关键字 payable,有了它买股票的人才可以付钱给你,接收钱如此简单。
-
function
buy() payable public
returns (
uint)
{
-
uint tokensToBuy
= msg
.value
/ tokenPrice
;
//根据购买金额和通证单价,计算出购买量
-
require(tokensToBuy <=
balanceTokens);
//继续执行合约需要确认合约的通证余额不小于购买量
-
voterInfo
[msg.sender
].voterAddress
= msg
.sender
;
//保存购买人地址
-
voterInfo
[msg.sender
].tokensBought
+= tokensToBuy
;
//更新购买人持股数量
-
balanceTokens
-= tokensToBuy
;
//将售出的通证数量从合约的余额中剔除
-
return tokensToBuy
;
//返回本次购买的通证数量
-
}
当用户(或程序)调用合约的 buy()方法时,需要在请求消息里利用 value属性设置用于购买股票通证的以太币金额。例如:
-
contract
.buy({
-
value
:web3
.toWei
('1'
,'ether'
),
//购买者支付的以太币金额
-
from:
web3.
eth.
accounts[
1]
//购买者账户地址
-
})
在合约的 payable方法实现代码中使用 msg.value来读取用户支付的以太币数额。 基于用户支付额和股票通证单价,就可以计算出购买数量,并将这些通证赋予购买人, 购买人的账户地址可以通过 msg.sender获取。
当然,也可以从truffle控制台调用 buy()方法来购买股票通证:
-
truffle
(development
)>
Voting.
deployed().
then(
function(
contract)
{
contract.
buy({
value:
web3.
toWei(
'1',
'ether'
),
from:
web3.
eth.
accounts[
1]})})
如前所述,加权投票方法不仅要指定候选人名称,还要指定使用多少股票通证来支持该候选人。 我们分别用 candidate和 votesInTokens来表示这两个参数:
-
function
voteForCandidate(
bytes32 candidate,
uint
votesInTokens)
public
{}
在投票人调用 voteForCandidate()方法投票时,我们不仅需要为指定的候选人增加其投票数,还需要跟踪投票人的相关信息,比如投票人是谁(即其账户地址),以及给每个候选人投了多少票。因此在该方法的开始部分,检查如果是该投票人第一次参与投票的话,首先初始化该投票人的 voterInfo结构:
-
if
(
voterInfo[
msg.
sender].
tokensUsedPerCandidate.
length ==
0
)
{
-
for(
uint i
=
0;
i <
candidateList.
length;
i++)
{
-
voterInfo
[msg
.sender
].tokensUsedPerCandidate
.push
(0
);
//该投票人为每个候选人投入的通证数量初始化为0
-
}
-
}
接下来我们计算该投票人当前的有效持股数量 —— 从该投票人的持股数量中扣除其为所有投票人已经消耗的股票通证数量:
uint availableTokens = voterInfo[msg.sender].tokensBought - totalTokensUsed(voterInfo[msg.sender].tokensUsedPerCandidate)
显然,在合约继续执行之前,需要满足条件 —— 投票人的有效持股数量不小于本次投票使用的股票通证数量:
require (availableTokens >= votesInTokens)
如果投票人依然持有足够数量的股票通证,我们就更新候选人获得的票数,同时更新投票人的通证使用记录:
votesReceived[candidate] += votesInTokens;
voterInfo[msg.sender].tokensUsedPerCandidate[index] += votesInTokens;
当一个用户调用 buy()方法发送以太来购买了合约发行的股票通证后,合约收到的资金去了哪里?
所有收到的资金(以太)都在这个投票合约里。每个合约都有它自己的地址,因此也是一个账户。 在以太坊里,这种账户被称为 合约账户(ContractAccount),而之前的人员账户,则被称为 外控账户(ExternalControlledAccount)。 因此,合约的地址里存着这些销售收益。
我们新增加的 transferTo()方法,可以将合约里的资金转移到指定账户:
-
function
transferTo(
address account)
public
{
-
account
.transfer
(this
.balance
);
-
}
需要注意的是, transferTo()方法的当前实现,并没有限制调用者,因此任何人都可以调用该方法从而转走投票合约账户里的资金!在生产系统中,你必须添加一些限制条件来避免上面的资金漏洞,例如,检查目标账户是否在一个白名单里。
合约里面剩下的方法都是辅助性的 getter方法,仅仅用来返回合约变量的值。
注意 tokensSold()等方法声明中的 constant修饰符,这表明该方法是只读的,即方法的执行 并不会改变区块链的状态,因此执行这些交易不会耗费任何gas。
1.5
与之前类似,我们修改迁移脚本 2_deploy_contracts.js来自动化投票合约的部署。 不过由于新的加权投票合约的构造函数声明了额外的参数,因此需要在迁移脚本中传入两个额外的参数 :
var Voting = artifacts.require("./Voting.sol");
module.exports = function(deployer) {
deployer.deploy(Voting, 10000, web3.toWei('0.01', 'ether'), ['Rama', 'Nick', 'Jose']);
};
在上面的代码中,我们部署的合约发行了10000个股票通证,单价为0.01以太。由于所有的价格需要以 Wei为单位计价,所以我们需要用 toWei()方法将Ether转换为Wei。
以太币面值
Wei 是 Ether 的最小面值。1 Ether 等于 1000000000000000000 Wei —— 18个0。
你可以把它当成是美分与美元,就像 Nickel(5 美分),Dime(10 美分),Quarter(25 美分),Ether 也有不同面值。其他面值如下:
kwei/babbage
mwei/lovelace
gwei/shannon
szabo
finney
ether
kether/grand/einstein
mether
gether
tether
当然也可以在 truffle 控制台,执行 web3.toWei(1,'ether') 来看一下ether(或其他面值)与 wei 之间 的转换关系。例如:
truffle(development)> web3.toWei(1,'ether')
现在可以编译合约并将其部署到区块链了:
~/repo/tkapp$ truffle compile
Compiling Migrations.sol...
Compiling Voting.sol...
Writing artifacts to ./build/contracts
~/repo/tkapp$ truffle migrate
Running migration: 1_initial_migration.js
Deploying Migrations...Migrations: 0x3cee101c94f8a06d549334372181bc5a7b3a8bee
Saving successful migration to network...
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying Voting...Voting: 0xd24a32f0ee12f5e9d233a2ebab5a53d4d4986203
Saving successful migration to network...
Saving artifacts...
1.6
成功地将合约部署到了 ganache后,执行 truffle console进入控制台,让我们和合约互动一下:
-
一个候选人(比如 Nick)有多少投票?
truffle(development)> Voting.deployed().then(function(instance) {instance.totalVotesFor.call('Nick').then(function(i) {console.log(i)})})
-
一共初始化发行了多少通证?
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.totalTokens().then(function(v) {console.log(v)}))})
-
已经售出了多少通证?
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.tokensSold().then(function(v) {console.log(v)}))})
-
购买 100个通证
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.buy({value: web3.toWei('1', 'ether')}).then(function(v) {console.log(v)}))})
-
购买以后账户余额是多少?
truffle(development)> web3.eth.getBalance(web3.eth.accounts[0])
-
已经售出了多少通证?
Voting.deployed().then(function(instance) {console.log(instance.tokensSold().then(function(v) {console.log(v)}))})
-
给 Jose 投 25 个 通证,给 Rama 和 Nick 各投 10 个 通证。
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.voteForCandidate('Jose', 25).then(function(v) {console.log(v)}))})
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.voteForCandidate('Rama', 10).then(function(v) {console.log(v)}))})
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.voteForCandidate('Nick', 10).then(function(v) {console.log(v)}))})
-
查询你所投账户的投票人信息(除非用了其他账户,否则你的账户默认是 web3.eth.accounts[0])
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.voterDetails('0x004ee719ff5b8220b14acb2eac69ab9a8221044b').then(function(v) {console.log(v)}))})
-
现在候选人Rama有多少投票?
truffle(development)> Voting.deployed().then(function(instance) {instance.totalVotesFor.call('Rama').then(function(i) {console.log(i)})})
1.7
现在,已经了解新的投票合约可以如约工作。现在开始构建前端逻辑,以便用户能够通过网页浏览器与合约交互。

以下可以自行参照git上的代码
-
HTML
如果仔细审查代码的话,你会发现网页中已经没有硬编码的值了。候选人的名字将通过向部署好的合约查询来进行填充。 网页也会显示公司发行的股票通证总量,以及已售出和剩余的通证量。
-
Javascript
通过移除候选者姓名等等的硬编码,我们已经大幅改进了 HTML 文件。我们会使用javascript/web3js来填充 HTML页面里的所有值,并实现查询投票人信息的额外功能。
如果对 JavaScript 不太熟悉,这些代码可能略显复杂。那么最好先理解populateCandidates() 函数的实现。
实现帮助
创建一个 Voting 合约的实例
在页面加载时,初始化并创建 web3 对象。
创建一个在页面加载时调用的函数,它需要:
使用 Voting 合约对象,向区块链查询来获取所有的候选者姓名并填充表格。
再次查询区块链得到每个候选人所获得的所有投票并填充表格的列。
填充 token 信息,比如所有初始化的 token,剩余 token,已售出的 token 以及 token 成本。
实现 buyTokens 函数,它在上一节的 html 里面调用。你已经在控制台交互一节中购买了 token。buyTokens 代码与那一节一样不可或缺。
类似地,实现 lookupVoterInfo 函数来打印一个投票人的细节。
和之前一样,执行以下命令进行构建:
~/repo/tkapp$ webpack
然后进入build目录,启动轻量web服务器:
~/repo/tkapp/build$ python -m SimpleHTTPServer
如果一切顺利,你可以看到网页,可以输入一个账户地址(投票人的地址),观察他们的投票行为和股票通证数量的变化。 并且可以购买更多的股票通证,为任意候选者投票并查看投票人信息。
二.总结
到这里基本的教程介绍已经结束,虽说是入门级的DAPP开发,对于之前没有接触过的人来说,也是需要花点时间理顺和让代码跑起来的。投票系统这个案例可能会在很多入门教学中都会使用,所以如果需要跟人展示和交流的话,可能换一种场景会比较看上去高大上一点。最后,如同开发手机APP一样,技术总是工具,真正产生价值的,更多是融入技术的思想。
参考文章与扩展阅读
本文项目代码 https://github.com/same4869/dappDemo
《以太坊开发入门,如何搭建一个区块链DApp投票系统》 https://yq.aliyun.com/articles/561058
Solidity文档 http://solidity.readthedocs.io/en/latest/index.html#2/_blank
Ethereum Contract ABI https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#2/_blank
TRUFFLE官网 http://truffleframework.com/#3/_blank

推荐阅读
从 SQL Server 到 MySQL (一):异构数据库迁移

