精华内容
下载资源
问答
  • 为了便于构建基于web的DApp以太坊还提供了一个非常方便的JavaScript库 web3.js ,它封装了以太坊节点的API 协议,从而让开发者可以轻松地连接到区块链节点而不必编写繁琐的 RPC 协议包。所以,我们可以在常用的JS...

    1、初识以太坊

    1、课程概述

    本课程面向初学者,内容涵盖以太坊开发相关的基本概念,并将手把手地教大家如何构建一个 基于以太坊的完整去中心化应用 —— 区块链投票系统。

    ethereum logo

    通过本课程的学习,你将掌握:

    • 以太坊区块链的基本知识
    • 开发和部署以太坊合约所需的软件环境
    • 使用高级语言(solidity)编写以太坊合约
    • 使用NodeJS编译、部署合约并与之交互
    • 使用Truffle框架开发分布式应用
    • 使用控制台或网页与合约进行交互

    前序知识要求

    为了顺利完成本课程,最好对以下技术已经有一些基本了解:

    • 一种面向对象的开发语言,例如:Python,Ruby,Java...
    • 前端开发语言:HTML/CSS/JavaScript
    • Linxu命令行的使用
    • 数据库的基本概念

    课程的所有代码均已在Ubuntu(Trusty、Xenial)和 macOS 上测试过。

    2、课程项目简介

    在本课程中,我们将会构建一个去中心化的(Decentralized)投票应用。利用这个投票应用, 用户可以在不可信(trustless)的分布环境中对特定候选人投票,每次投票都会被记录在区块 链上:

    voting dapp

    所谓去中心化应用(DApp:Dcentralized Application),就是一个不存在中心服务器 的应用。在网络中成百上千的电脑上,都可以运行该应用的副本,这使得它几乎不可能 出现宕机的情况。

    基于区块链的投票是完全去中心化的,因此无须任何中心化机构的存在。

    思考一下,以下应用是不是去中心化应用:QQ、电驴、迅雷、加密币交易所?

    3、开发迭代

    本课程将涵盖应用开发的整个过程,我们将通过三次迭代来渐进地引入区块链应用 开发所涉及的相关概念、语言和工具:

    voting dapp iteration

    • Vanilla:在第一个迭代周期,我们不借助任何开发框架,而仅仅使用NodeJS来进行应用开发, 这有助于我们更好地理解区块链应用的核心理念。
    • Truffle:在第二个迭代周期,我们将使用最流行的去中心化应用开发框架Truffle进行开发。 使用开发框架有助于我们提高开发效率。
    • Token:在第三个迭代周期,我们将为投票应用引入代币(Token) —— 现在大家都改口 称之为通证了 —— 都是ICO惹的祸。代币是公链上不可或缺的激励机制,也是区块链 应用区别于传统的中心化应用的另一个显著特征。

    为什么选择投票应用作为课程项目?

    之所以选择投票作为我们的第一个区块链应用,是因为集体决策 —— 尤其是投票机制 —— 是以太坊的 一个核心的价值主张。

    另一个原因在于,投票是很多复杂的去中心化应用的基础构件,所以我们选择了投票应用作为学习区块链 应用开发的第一个项目。

    4、初识区块链

    如果你熟悉关系型数据库,就应该知道一张数据表里可以包含很多行数据记录。例如,下面的数据表中 包含了6条交易记录:

    relation database

    本质上,区块链首先就是一个分布式(Distributed)数据库,这个数据库维护了一个不断增长的记录列表。 现在,让我们对数据进行批量(batch)存储,比如每批 100 行,并将各存储批次连接起来,是不是就像一条链?

    chain

    在区块链里,多个数据记录组成的批次就被称为块(block),块里的每一行数据记录就被称为交易(transaction):

    basic blockchain

    最开始的那个块,通常被称为创世块(genesis block),它不指向任何其他块。

    不可篡改性

    区块链的一个显著特点是,数据一旦写入链中,就不可篡改重写。

    在传统的关系型数据库中,你可以很容易地更新一条数据记录。但是,在区块链中,一旦数据写入就无法 再更新了 —— 因此,区块链是一直增长的。

    那么,区块链是如何实现数据的不可篡改特性?

    这首先得益于哈希(Hash)函数 —— 如果你还没接触过哈希函数,不妨将它视为一个数字指纹的计算函数: 输入任意长度的内容,输出定长的码流(指纹)。哈希函数的一个重要特性就是,输入的任何一点微小变化,都会 导致输出的改变。因此可以将哈希值作为内容的指纹来使用。 你可以点击这里 进一步了解哈希函数。

    由于区块链里的每个块都存储有前一个块内容的哈希值,因此如果有任何块的内容被篡改,被篡改的块之后 所有块的哈希值也会随之改变,这样我们就很容易检测出区块链的各块是否被篡改了。

    去中心化的挑战

    一旦完全去中心化,在网络上就会存在大量的区块链副本(即:全节点),很多事情都会变得比之前中心化 应用环境复杂的多,例如:

    • 如何保证所有副本都已同步到最新状态?
    • 如何保证所有交易都被广播到所有运行和维护区块链副本的节点计算机上?
    • 如何防止恶意参与者篡改区块链
    • ......

    在接下来的课程中,通过与经典的C/S架构的对比,我们将逐步理解去中心化应用的核心思路, 并掌握如何构建以太坊上的去中心化应用。

    阅读课程内容并思考,区块链采用了哪些机制来保证链上数据不被篡改?

    5、C/S架构 —— 以服务器为中心

    理解去中心化应用架构的最好方法,就是将它与熟悉的Client/Server架构进行对比。如果你是一个web开发者, 应该对下图很了解,这是一个典型的Client/Server架构:

    webapp architecture

    一个典型web应用的服务端通常由 Java,Ruby,Python 等等语言实现。前端代码由 HTML/CSS/JavaScript 实现。 然后将整个应用托管在云端,比如 AWS、Google Cloud Platform、Heroku....,或者放在你租用的一个VPS 主机上。

    用户通过客户端(Client)与 web 应用(Server)进行交互。典型的客户端包括浏览器、命令行工具(curlwget等)、 或者是API访问代码。注意在这种架构中,总是存在一个(或一组)中心化的 web 服务器,所有的客户端都需要 与这一(组)服务器进行交互。当一个客户端向服务器发出请求时,服务器处理该请求,与数据库/缓存进行交互, 读/写/更新数据库,然后向客户端返回响应。

    这是我们熟悉的中心化架构。在下一节,我们将会看到基于区块链的去中心化架构的一些显著区别。

    思考一下,是不是所有的C/S架构的应用都是中心化应用?

    6、去中心化架构 —— 彼此平等的节点

    下图给出了基于以太坊的去中心化应用架构:

    ethereum architecture

    你应该已经注意到,每个客户端(浏览器)都是与各自的节点应用实例进行交互,而不是向 一个中心化的服务器请求服务。

    在一个理想的去中心化环境中,每个想要跟DApp交互的人,都需要在他们的计算机或手机上面运行 一个的完整区块链节点 —— 简言之,每个人都运行一个全节点。这意味着,在能够真正使用一个 去中心化应用之前,用户不得不下载整个区块链。

    不过我们并非生活在一个乌托邦里,期待每个用户都先运行一个全节点,然后再使用你的应用是不现实的。 但是去中心化背后的核心思想,就是不依赖于中心化的服务器。所以,区块链社区已经出现了 一些解决方案,例如提供公共区块链节点的Infura, 以及浏览器插件Metamask等。通过这些方案, 你就不需要花费大量的硬盘、内存和时间去下载并运行完整的区块链节点,同时也可以利用去中心化 的优点。我们将会以后的课程中对这些解决方案分别进行评测。

    思考一下,为什么在每个区块链节点上都需要保存全部的数据?

    7、以太坊 —— 世界计算机

    以太坊是一种区块链的实现。在以太坊网络中,众多的节点彼此连接,构成了以太坊网络:

    ethereum

    以太坊节点软件提供两个核心功能:数据存储、合约代码执行。

    在每个以太坊全节点中,都保存有完整的区块链数据。以太坊不仅将交易数据保存在链上,编译后 的合约代码同样也保存在链上。

    以太坊全节点中,同时还提供了一个虚拟机来执行合约代码。

    交易数据

    以太坊中每笔交易都存储在区块链上。当你部署合约时,一次部署就是一笔交易。当你为候选者投票时,一次投票 又是另一笔交易。所有的这些交易都是公开的,每个人都可以看到并进行验证。这个数据永远也无法篡改。

    为了确保网络中的所有节点都有着同一份数据拷贝,并且没有向数据库中写入任何无效数据,以太坊 目前使用工作量证明 (POW:Proof Of Work)算法来保证网络安全,即通过矿工挖矿(Mining)来达成共识(Consensus)—— 将数据同步到所有节点。

    工作量证明不是达成共识的唯一算法,挖矿也不是区块链的唯一选择。现在,我们只需要了解,共识是指各节点 的数据实现了一致,POW只是众多用于建立共识的算法中的一种,这种算法需要通过矿工的挖矿来实现非可信环境下的 可信交易。共识是目的,POW是手段。

    合约代码

    以太坊不仅仅在链上存储交易数据,它还可以在链上存储合约代码。

    在数据库层面,区块链的作用就是存储交易数据。那么给候选者投票、或者检索投票结果的逻辑放在哪儿呢? 在以太坊的世界里,你可以使用Solidity语言来编写业务逻辑/应用代码(也就是合约:Contract), 然后将合约代码编译为以太坊字节码,并将字节码部署到区块链上:

    contract development and deploy

    编写合约代码也可以使用其他的语言,不过 Solidity是到目前为止最流行的选择。

    以太坊虚拟机

    以太坊区块链不仅存储数据和代码,每个节点中还包含一个虚拟机(EVM:Ethereum Virtual Machine)来执行 合约代码 —— 听起来就像计算机操作系统。

    事实上,这一点是以太坊区别于比特币(Bitcoin)的最核心的一点:虚拟机的存在使区块链迈入了2.0 时代,也让区块链第一次成为应用开发者友好的平台。

    JS开发库

    为了便于构建基于web的DApp,以太坊还提供了一个非常方便的JavaScript库web3.js,它封装了以太坊节点的API 协议,从而让开发者可以轻松地连接到区块链节点而不必编写繁琐的RPC协议包。所以,我们可以在常用的JS框架 (比如 reactjs、angularjs 等)中直接引入该库来构建去中心化应用:

    web3.js

    阅读教程,回答以下问题:

    • 共识是什么意思?它和工作量证明是什么关系?
    • 合约是保存在链上吗?

    2、使用NodeJS开发DApp

    1、开发流程概述

            现在,在初步了解以太坊的基本概念之后,我们将开始构建投票DApp。通过实际应用开发,有助于我们加深 对以太坊的认识,并初步了解以太坊所提供的功能。

    下图展示了应用的整体结构:

                                        voting

            从图中可以看到,网页通过(HTTP上的)远程过程调用(PRC:Remote Procedure Call)与区块链节点进行通信。 web3.js  已经封装了以太坊规定的全部 RPC 调用,因此利用它就可以与区块链进行交互,而不必手写那些RPC请求包。 使用 web3.js 的另一个好处是,你可以使用自己喜欢的前端框架来构建出色的web 应用。

    由于获得一个同步的全节点相当耗时,并占用大量磁盘空间。为了在我们对区块链的兴趣消失之前掌握 如何开发一个去中心化应用,本课程将使用 ganache 软件来模拟区块链节点,以便快速开发并测试应用, 从而可以将注意力集中在去中心化的思想理解与DApp应用逻辑开发方面。

    接下来,我们将编写一个投票合约,然后编译合约并将其部署到区块链节点 —— ganache上。

    最后,我们将分别通过命令行和网页这两种方式,与区块链进行交互。

    2、节点仿真器

            课程环境已经预置了 ganache 软件。在终端中执行下面命令来启动它:

    ~$ ganache-cli

          ganache 将输出如下信息:

    Ganache CLI v6.0.3 (ganache-core: 2.0.2)
    
    Available Accounts
    ==================
    (0) 0x5c252a0c0475f9711b56ab160a1999729eccce97
    (1) 0x353d310bed379b2d1df3b727645e200997016ba3
    (2) 0xa3ddc09b5e49d654a43e161cae3f865261cabd23
    (3) 0xa8a188c6d97ec8cf905cc1dd1cd318e887249ec5
    (4) 0xc0aa5f8b79db71335dacc7cd116f357d7ecd2798
    (5) 0xda695959ff85f0581ca924e549567390a0034058
    (6) 0xd4ee63452555a87048dcfe2a039208d113323790
    (7) 0xc60c8a7b752d38e35e0359e25a2e0f6692b10d14
    (8) 0xba7ec95286334e8634e89760fab8d2ec1226bf42
    (9) 0x208e02303fe29be3698732e92ca32b88d80a2d36
    
    Private Keys
    ==================
    (0) a6de9563d3db157ed9926a993559dc177be74a23fd88ff5776ff0505d21fed2b
    (1) 17f71d31360fbafbc90cad906723430e9694daed3c24e1e9e186b4e3ccf4d603
    (2) ad2b90ce116945c11eaf081f60976d5d1d52f721e659887fcebce5c81ee6ce99
    (3) 68e2288df55cbc3a13a2953508c8e0457e1e71cd8ae62f0c78c3a5c929f35430
    (4) 9753b05bd606e2ffc65a190420524f2efc8b16edb8489e734a607f589f0b67a8
    (5) 6e8e8c468cf75fd4de0406a1a32819036b9fa64163e8be5bb6f7914ac71251cc
    (6) c287c82e2040d271b9a4e071190715d40c0b861eb248d5a671874f3ca6d978a9
    (7) cec41ef9ccf6cb3007c759bf3fce8ca485239af1092065aa52b703fd04803c9d
    (8) c890580206f0bbea67542246d09ab4bef7eeaa22c3448dcb7253ac2414a5362a
    (9) eb8841a5ae34ff3f4248586e73fcb274a7f5dd2dc07b352d2c4b71132b3c73f0
    
    HD Wallet
    ==================
    Mnemonic:   cancel better shock lady capable main crunch alcohol derive alarm duck umbrella
    Base HD Path: m/44'/60'/0'/0/{account_index}
    
    Listening on localhost:8545

            为了便于开发与测试,ganache 默认会自动创建 10 个账户,每个账户有 100 个以太币(ETH:Ether)。 可以把账户视为银行账户,以太币就是以太坊生态系统中的货币。

            例如,在上面的输出中,可以看到第一个账户是 

          0x5c252a0c0475f9711b56ab160a1999729eccce97,启动时,该账户中预置有100ETH的余额(Balance)。

            接下来我们将使用这个账户创建交易、发送/接收以太币。

            上面输出的最后一句话,描述了节点仿真器的监听地址和端口为localhost:8545,在使用web3.js 时,需要传入这个地址来告诉web3js库应当连接到哪一个节点。

    当你在自己的计算机上练习时,也可以安装 GUI 版本的 ganache,下载地址: ganache-gui

        思考与练习:

    1. 在第一个终端窗口中输入以下命令查看ganache-cli的可选参数:ganache-cli --help
    2. 使用默认参数启动ganache-cli,查看屏幕输出,找出账户列表。

    3、投票合约设计

            了解如何运行节点仿真器之后,可以开始设计我们的第一个合约了。

    我们使用Solidity语言来编写合约。如果你熟悉面向对象的开发和JavaScript,那么学习Solidity 应该非常简单。可以将合约类比于OOP的类:合约中的属性用来声明合约的状态,而合约中的方法则提 供修改状态的访问接口。下图给出了投票合约的主要接口:

            voting contract

    基本上,投票合约Voting包含以下内容:

    • 构造函数,用来初始化候选人名单。
    • 投票方法Vote(),每次执行就将指定的候选人得票数加 1
    • 得票查询方法totalVotesFor(),执行后将返回指定候选人的得票数

    有两点需要特别指出:

    • 合约状态是持久化到区块链上的,因此对合约状态的修改需要消耗以太币。
    • 只有在合约部署到区块链的时候,才会调用构造函数,并且只调用一次。
    • 与 web 世界里每次部署代码都会覆盖旧代码不同,在区块链上部署的合约是不可改变的,也就是说,如果你更新 合约并再次部署,旧的合约仍然会在区块链上存在,并且合约的状态数据也依然存在。新的部署将会创建合约的一 个新的实例。

    Solidity语言的详细介绍可以在这里 找到。

            思考与练习,阅读教程,回答以下问题:

    • 合约的构造函数和面向对象开发中的类的构造函数有什么不同之处?
    • 一个实例化合约的属性和一个类的实例化对象的属性有什么不同之处?

    4、合约代码开发

            投票合约的代码保存在~/repo/chapter1/Voting.sol文件中,可以使用实验环境中的编辑器 打开查看其内容。

            Voting.sol 代码如下:

    pragma solidity ^0.4.18;
    
    contract Voting {
    
      mapping (bytes32 => uint8) public votesReceived;
      bytes32[] public candidateList;
    
      function Voting(bytes32[] candidateNames) public {
        candidateList = candidateNames;
      }
    
      function totalVotesFor(bytes32 candidate) view public returns (uint8) {
        require(validCandidate(candidate));
        return votesReceived[candidate];
      }
    
      function voteForCandidate(bytes32 candidate) public {
        require(validCandidate(candidate));
        votesReceived[candidate]  += 1;
      }
    
      function validCandidate(bytes32 candidate) view public returns (bool) {
        for(uint i = 0; i < candidateList.length; i++) {
          if (candidateList[i] == candidate) {
            return true;
          }
        }
        return false;
       }
    }

    编译器要求

    pragma solidity ^0.4.18;
    

    声明合约代码的编译器版本要求。^0.4.18表示要求合约编译器版本不低于0.4.18

    合约声明

    contract Voting{}
    

    contract关键字用来声明一个合约。

    字典类型:mapping

    mapping (bytes32 => uint8) public votesReceived;
    

    mapping可以类比于一个关联数组或者是字典,是一个键值对。例如,votesReceived状态的 键是候选者的名字,类型为bytes32 —— 32个字节定长字符串。votesReceived状态中每个键对应的值 是一个单字节无符号整数(uint8),用来存储该候选人的得票数:

                    mapping sample

    bytes32[] public candidateList;
    

    在JS中,使用votesReceived.keys 就可以获取所有的候选人姓名。但是在Solidity中 没有这样的方法,所以我们需要单独管理全部候选人的名称 —— candidateList数组。

    function voteForCandidate(bytes32 candidate) public {
      require(validCandidate(candidate));
      votesReceived[candidate]  += 1;
    }
    

    voteForCandidate()方法中,请注意 votesReceived[key] 有默认值 0,所以我们没有进行初始化, 而是直接加1。

    在合约方法体内的require()语句类似于断言,只有条件为真时,合约才继续执行。validateCandidate() 方法只有在给定的候选人名称在部署合约时传入的候选人名单中时才返回真值,从而避免乱投票的行为:

    function validCandidate(bytes32 candidate) view public returns (bool) {
      for(uint i = 0; i < candidateList.length; i++) {
        if (candidateList[i] == candidate) {
          return true;
        }
      }
      return false;
    }
    

    方法声明符与修饰符

    Solidity中,可以为函数应用可视性声明符(visibility specifier),例如 publicprivate。 public意味着可以从合约外调用函数。如果一个方法仅限合约内部调用,可以把它声明为私有(private)。 点击这里 可以查看所有的可视性说明符。

    Solidity中,还可以为函数声明修饰符(modifier),例如view用来告诉编译器,这个函数是只读的,也就是说, 该函数的执行不会改变区块链的状态)。所有的修饰符都可以在这里 看到。

            思考与练习:

            当前合约允许一个人多次投票。请修改合约来保证一个人只能投一次票。

    5、合约代码编译

            我们使用solc库来编译合约代码。如果你还记得的话,之前我们提到过 web3js库, 它能够让你通过 RPC 与区块链进行交互。我们将在node控制台里用这个库编译和部署合约, 并与区块链进行交互。

            首先,请确保ganache已经在第一个终端窗口中运行:~$ ganache-cli

            然后,在另一个终端中进入repo/chapter1目录,启动node 控制台,然后初始化 web3 对象,并向本地区块 链节点(http://localhost:8545)查询获取所有的账户:

    ~$ cd ~/repo/chapter1
    ~/repo/chapter1$ node
    > Web3 = require('web3')
    > web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
    > web3.eth.accounts
    ['0x5c252a0c0475f9711b56ab160a1999729eccce97'
    '0x353d310bed379b2d1df3b727645e200997016ba3'
    '0xa3ddc09b5e49d654a43e161cae3f865261cabd23'
    '0xa8a188c6d97ec8cf905cc1dd1cd318e887249ec5'
    '0xc0aa5f8b79db71335dacc7cd116f357d7ecd2798'
    '0xda695959ff85f0581ca924e549567390a0034058'
    '0xd4ee63452555a87048dcfe2a039208d113323790'
    '0xc60c8a7b752d38e35e0359e25a2e0f6692b10d14'
    '0xba7ec95286334e8634e89760fab8d2ec1226bf42'
    '0x208e02303fe29be3698732e92ca32b88d80a2d36']
    

            要编译合约,首先需要载入 Voting.sol文件的内容,然后使用编译器(solc)的compile()方法 对合约代码进行编译:

    > code = fs.readFileSync('Voting.sol').toString()
    > solc = require('solc')
    > compiledCode = solc.compile(code)
    

            成功编译合约后可以查看一下编译结果。直接在控制台输入:

    > compiledCode
    

    相当长一大段输出霸屏...

    便以结果是一个JSON对象,其中包含两个重要的字段:

    • compiledCode.contracts[':Voting'].bytecode: 投票合约编译后的字节码,也是要部署到区块链上的代码。
    • compiledCode.contracts[':Voting'].interface: 投票合约的接口,被称为应用二进制接口(ABI:Application Binary Interface), 它声明了合约中包含的接口方法。无论何时需要跟一个合约进行交互,都需要该合约的abi定义。你可以在 这里查看ABI的详细信息。

    在接下来的几节课,我们将会使用truffle框架来管理合约的编译过程以及与区块链的交互过程。但是, 在使用框架之前,深入了解其工作原理还是大有裨益的,因为框架会将这些脏活封装起来,在出现 故障时并不容易排查错误。

            思考与练习:

            阅读教程,启动ganache-cli,编译投票合约并查看其ABI定义和字节码。

    6、投票合约部署

            让我们继续课程,现在将投票合约部署到区块链上。

            为此,需要先传入合约的abi定义来创建合约对象VotingContract,然后利用该对象完成合约在链上的部署和初始化。

            在node控制台执行以下命令:

    > abiDefinition = JSON.parse(compiledCode.contracts[':Voting'].interface)
    > VotingContract = web3.eth.contract(abiDefinition)
    > byteCode = compiledCode.contracts[':Voting'].bytecode
    > deployedContract = VotingContract.new(['Rama','Nick','Jose'],{data: byteCode, from: web3.eth.accounts[0], gas: 4700000})
    > deployedContract.address
    '0x0396d2b97871144f75ba9a9c8ae12bf6c019f610' <- 你的部署地址可能和这个不一样
    > contractInstance = VotingContract.at(deployedContract.address)
    

            调用VotingContract对象的new()方法来将投票合约部署到区块链。new()方法参数列表应当与合约的 构造函数要求相一致。对于投票合约而言,new()方法的第一个参数是候选人名单。

    new()方法的最后一个参数用来声明部署选项。现在让我们来看一下这个参数的内容:

    {
      data: byteCode,             //合约字节码
      from: web3.eth.accounts[0], //部署者账户,将从这个账户扣除执行部署交易的开销
      gas: 4700000                //愿意为本次部署最多支付多少油费,单位:Wei
    }
    
    • data: 这是合约编译后,需要部署到区块链上的合约字节码。
    • from: 区块链必须跟踪是谁部署了一个合约。在本例中,我们简单地利用web3.eth.accounts返回的 第一个账户,作为部署这个合约的账户。在提交交易之前,你必须拥有并解锁这个账户。不过为了方便 起见,ganache默认会自动解锁这10个账户。
    • gas: 与区块链进行交互需要消耗资金。这笔钱用来付给矿工,因为他们帮你把代码部署到在区块链里。你 必须声明愿意花费多少资金让你的代码包含在区块链中,也就是设定 “gas” 的值。“from”字段声明的账户的 余额将会被用来购买 gas。gas 的价格则由区块链网络设定。

    我们已经成功部署了投票合约,并且获得了一个合约实例(变量contractInstance),现在可以用这个实例 与合约进行交互了。

    在区块链上有上千个合约。那么,如何识别你的合约已经上链了呢?

    答案是:使用deployedContract.address。 当你需要跟合约进行交互时,就需要这个部署地址和我们之前 谈到的abi定义。 因此,请记住这个地址

            思考与练习:

            参考教程,将投票合约部署到区块链上。

    7、控制台交互

    让我们继续课程。

    调用合约的totalVotesFor() 方法来查看某个候选人的得票数。例如,下面的代码 查看候选人Rama的得票数:

    > contractInstance.totalVotesFor.call('Rama')
    { [String: '0'] s: 1, e: 0, c: [ 0 ] }
    

    { [String: '0'] s: 1, e: 0, c: [ 0 ] } 是数字 0 的科学计数法表示. 你可以在 这里 了解科学计数法的详细信息。

    调用合约的voteForCandidate()方法投票给某个候选人。下面的代码给Rama投了三次票:

    > contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
    '0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53'
    > contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
    '0x02c054d238038d68b65d55770fabfca592a5cf6590229ab91bbe7cd72da46de9'
    > contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
    '0x3da069a09577514f2baaa11bc3015a16edf26aad28dffbcd126bde2e71f2b76f'
    

    现在我们再次查看Rama的得票数:

    > contractInstance.totalVotesFor.call('Rama').toLocaleString()
    '3'
    

    投票 = 交易

    每执行一次投票,就会产生一次交易,因此voteForCandidate()方法将返回一个交易id,作为 交易的凭据。比如:0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53。 交易id是交易发生的凭据,交易是不可篡改的,因此任何时候可以使用交易id引用或查看交易内容 都会得到同样的结果。

    对于区块链而言,交易不可篡改是其核心特性。在接下来的章节,我们将会利用这一特性来构建应用。

            思考与练习:

            参考教程,分别投票给Jose和Nick。

    8、网页交互

    至此,大部分的工作都已完成,接下来让我们创建一个简单的html页面,以便用户可以使用浏览器 而不是复杂的命令行来与投票合约交互:

                html voting

    页面的主要功能如下:

    • 列出所有的候选人及其得票数
    • 用户在页面中可以输入候选人的名称,然后点击投票按钮,网页中的JS代码将调用投票合约的 voteForCandidate()方法 —— 和我们nodejs控制台里的流程一样。

    你可以在实验环境编辑器中打开~/repo/chapter1/index.html来查看页面源代码。 为了聚焦核心业务逻辑,我们在网页中硬编码了候选人姓名。如果你喜欢的话,可以调整代码来动态生成候选人。

    index.html 代码如下:

    <!DOCTYPE html>
    <html>
    <head>
     <title>Hello World DApp</title>
     <link href='/lib/gfonts.css' rel='stylesheet' type='text/css'>
     <link href='/lib/bootstrap.min.css' rel='stylesheet' type='text/css'>
    </head>
    <body class="container">
     <h1>A Simple Hello World Voting Application</h1>
     <div class="table-responsive">
      <table class="table table-bordered">
       <thead>
        <tr>
         <th>Candidate</th>
         <th>Votes</th>
        </tr>
       </thead>
       <tbody>
        <tr>
         <td>Rama</td>
         <td id="candidate-1"></td>
        </tr>
        <tr>
         <td>Nick</td>
         <td id="candidate-2"></td>
        </tr>
        <tr>
         <td>Jose</td>
         <td id="candidate-3"></td>
        </tr>
       </tbody>
      </table>
     </div>
     <input type="text" id="candidate" />
     <a href="#" onclick="voteForCandidate()" class="btn btn-primary">Vote</a>
    </body>
    <script src="/lib/web3.js"></script>
    <script src="/lib/jquery-3.1.1.slim.min.js"></script>
    <script src="./index.js"></script>
    </html>

    页面文件中的JS代码都封装在了一个单独的JS文件中,可以在试验环境编辑器中打开~/repo/chapter1/index.js来查看其内容。

    index.js 代码如下:

    web3 = new Web3(new Web3.providers.HttpProvider("http://8545.0bcc71cc8eb1ffc1fcd374acb10ccf06.x.hubwiz.com/"));
    abi = JSON.parse('[{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"totalVotesFor","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"validCandidate","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"votesReceived","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"x","type":"bytes32"}],"name":"bytes32ToString","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"candidateList","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"voteForCandidate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"contractOwner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"inputs":[{"name":"candidateNames","type":"bytes32[]"}],"payable":false,"type":"constructor"}]')
    VotingContract = web3.eth.contract(abi);
    contractInstance = VotingContract.at('0x9a0036b01f999f8c046ea5fa7b5dddabe24ed8de');
    candidates = {"Rama": "candidate-1", "Nick": "candidate-2", "Jose": "candidate-3"}
    
    function voteForCandidate(candidate) {
     candidateName = $("#candidate").val();
     try {
      contractInstance.voteForCandidate(candidateName, {from: web3.eth.accounts[0]}, function() {
       let div_id = candidates[candidateName];
       $("#"+div_id).html(contractInstance.totalVotesFor.call(candidateName).toString());
      });
     } catch (err) {
     }
    }
    
    $(document).ready(function() {
     candidateNames = Object.keys(candidates);
     for (var i = 0; i < candidateNames.length; i++) {
      let name = candidateNames[i];
      let val = contractInstance.totalVotesFor.call(name).toString()
      $("#"+candidates[name]).html(val);
     }
    });

    为了将页面运行起来,需要根据你的私有试验环境对JS代码进行一下调整:

    节点的RPC API地址

    web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
    

    HttpProvier()对象的构造函数参数是web3js库需要链接的以太坊节点RPC API的URL,要调整为 你的私有试验环境中ganache的访问端结点,格式为:

    http://8545.<你的私有实验环境URL>/
    

    查看试验环境中的嵌入浏览器地址栏来获取你的私有实验环境URL:

    private lab url

    投票合约地址

    当一个合约部署到区块链上时,将获得一个地址,例如0x329f5c190380ebcf640a90d06eb1db2d68503a53。 由于每次部署都会获得一个不同的地址,因此你需要指定它:

    contractInstance = VotingContract.at('0x329f5c190380ebcf640a90d06eb1db2d68503a53')
    

    如果你在部署合约的时候没有记录这个地址,就重新部署吧。

    运行web服务

    在第二个终端中输入以下命令来启动一个简单的Web服务器,以便我们可以在试验环境中的嵌入浏览器中访问页面:

    ~$ cd ~/repo/chapter1
    ~/repo/chapter1$ python -m SimpleHTTPServer
    

    Python的SimpleHTTPServer模块将启动在8000端口的监听。

    现在,在试验环境的嵌入浏览器中点击刷新按钮。如果一切顺利的话,你应该可以看到投票应用的页面了。 当你在文本框中输入候选人姓名,例如Rama,然后点击按钮后,应该会看到候选人Rama的得票数加 1 。

            思考与练习:

            参考教程,通过网页访问投票合约。

    9、课程小结     

    如果你可以看到页面,当点击投票按钮后可以看到投票数增加,那你就已经成功创建了第一个去中心化应用,恭喜!

    总结一下,下面是我们到目前为止已经完成的事情:

    • 使用nodejs, npm 和 ganache作为开发环境。
    • 开发简单的投票合约,编译并部署到区块链节点上。
    • 使用nodejs 控制台与合约交互。
    • 编写网页与合约交互。
    • 所有的投票都保存到区块链上,并且不可修改。
    • 任何人都可以独立验证每个候选人获得了多少投票。

    在接下来的课程,我们将学习如何使用Truffle框架构建去中心化应用,也会更深入地学习 合约开发语言Solidity

    3、使用Truffle开发DApp

    1、内容概述

    在之前的课程中,我们已经基于区块链(ganache仿真器)实现了一个投票合约,并且成功地 通过 nodejs 控制台和网页实现了与合约的交互。

    voting dapp

    在接下来的章节,我们将会实现以下内容:

    • 使用Truffle框架开发投票应用,它可以方便地编译、部署合约。
    • 修改已有的投票应用代码,以便适配开发框架。
    • 利用Truffle控制台、网页与投票合约进行交互。
    • 对投票合约进行扩展,加入通证(token)及购买功能。
    • 对前端代码进行扩展,通过网页前端购买股票通证,并利用股票通证为候选人投票。

    2、初始化项目

    Truffle是一个DApp开发框架,它简化了去中心化应用的构建和管理。你可以在 这里了解框架的 更多内容和完整特性。

    在实验环境中已经预置了Truffle,如果你需要在自己的机器上安装,可以使用 npm 全局安装:

    ~$ npm install -g truffle
    

    Truffle提供了众多的项目模版,可以快速搭建一个去中心化应用的骨架代码。下面的代码 使用webpack项目模版来创建应用tfapp

    ~$ mkdir -p ~/repo/tfapp
    ~$ cd ~/repo/tfapp
    ~/repo/tfapp$ truffle unbox webpack
    

    初始化一个Truffle项目时,它会创建运行一个完整的DApp所需的文件和目录。 可以使用ls命令来查看生成的项目结构:

    ~/repo/tfapp$ ls
    README.md       contracts       node_modules      test          webpack.config.js   truffle.js
    app          migrations       package.json
    ~/repo/tfapp$ ls app/
    index.html javascripts stylesheets
    ~/repo/tfapp$ ls contracts/
    ConvertLib.sol MetaCoin.sol Migrations.sol
    ~/repo/tfapp$ ls migrations/
    1_initial_migration.js 2_deploy_contracts.js
    

    由于不需要所生成的示例应用中的合约,因此可以放心地删除contracts目录中的 除Migrations.sol之外的其他合约文件:

    ~/repo/tfapp$ rm contracts/ConvertLib.sol contracts/MetaCoin.sol
    

    Migrations.sol合约用来管理应用合约的部署,因此请勿删除。

            思考与练习:

            参考教程,在repo目录下创建新项目tfapp

    3、升级投票应用代码

            

    在前面课程实现的投票应用中,我们分别编写了三个文件:

    • Voting.sol:合约文件
    • index.html:页面文件
    • index.js:JS脚本文件

    现在对这几个文件分别进行处理,以便应用到Truffle生成的应用中。

    Voting.sol

    合约文件不需要修改,直接拷贝到 contracts 目录即可:

    ~/repo/tfapp$ cp ../chapter1/Voting.sol contracts/
    ~/repo/tfapp$ ls contracts/
    Migrations.sol Voting.sol
    

    index.html

    先将页面文件拷贝到app目录,覆盖Truffle生成的index.html

    ~/repo/tfapp$ cp ../chapter1/index.html app/
    

    由于Truffle的webpack模版在打包JS脚本时,默认使用app.js作为打包入口, 因此,我们将页面文件中对index.js的引用改为对app.js的引用:

    <script src="app.js"></script>
    

    app.js

    在Truffle下,我们需要重写与区块链交互的JS脚本。由于使用webpack打包,因此 可以使用ES2015语法。 你可以在实验环境的编辑器中打开~/repo/chapter2/app/javascripts/app.js 查看其内容。

    当使用Truffle来编译和部署合约时,框架会将合约的应用接口定义(abi:Application Binary interface) 以及部署地址保存到build/contracts目录中同名的json文件中 —— 我们不需要自己记部署地址了! 例如,Voting.sol的部署信息对应与build/contracts/Voting.json文件。利用这个文件就可以创建 投票合约对象:

    import voting_artifacts from '../../build/contracts/Voting.json'
    var Voting = contract(voting_artifacts)
    

    合约对象的deployed()方法返回一个Promise,其解析值为该合约对象的部署实例代理(真正的实例 在链上!),利用这个代理可以执行合约的方法:

    Voting.deployed()
      .then(instance => instance.voteForCandidate('Rama')) 
      .then(() => instance.totalVotesFor.call('Rama'))
      .then(votes => console.log('Rama got votes: ', votes))
    

    思考与练习:

    参考教程,升级投票应用代码。

    4、迁移脚本

    迁移(migration)目录的内容非常重要。Truffle使用该目录下的迁移脚本来管理应用合约的部署。 如果你还记得的话,我们在之前的课程中,是通过在 node 控制台中调用合约对象的new()方法来 将投票合约 部署到区块链上。有了Truffle,以后再也不需要这么做了。

    第一个迁移脚本1_initial_migration.js的作用是向区块链部署Migrations合约, 这个合约的作用是存储并跟踪已经部署的最新合约。每次运行迁移任务时,Truffle就会向区块链查询获取 已部署好的合约,然后部署新的合约。在部署完成后,这个脚本会更新Migrations合约中的last_completed_migration 字段指向最新部署的合约。

    可以简单地把Migrations合约当成是一个数据库表,字段last_completed_migration 总是保持最新状态。 更多细节可见 Truffle官方文档

    修改迁移脚本

    将迁移脚本2_deploy_contracts.js的内容修改为以下内容,以便部署我们的投票合约Voting

    var Voting = artifacts.require("./Voting.sol");
    module.exports = function(deployer) {
     deployer.deploy(Voting, ['Rama', 'Nick', 'Jose'], {gas: 290000});
    };
    

    从上面的代码可以看出,Truffle框架将向迁移脚本传入一个部署器对象(deployer),调用其deploy() 方法即可实现指定合约的部署。

    deploy()方法的第一个参数为要部署合约的编译对象,调用artifacts.require()即可直接将合约代码转换为 合约编译对象,例如:artifacts.require('./Voting.sol') 。容易理解,Truffle的artifacts 对象自动调用solidity编译器来编译合约代码文件并返回编译结果对象。

    deploy()方法的最后一个参数是合约实例化选项对象,可以用来指定部署代码所需的油费 —— 别忘了部署合约 也是交易,因此需要烧点油(gas)。gas 数量会随着你的合约大小而变化 —— 确切的说,部署一个合约所需的油费 取决于编译生成的合约字节码,不同的字节码指令对应不同的开销,累加起来就可以估算出部署费用。

    对于投票合约而言, 290000个油就足够了 —— 这个价格是我们为部署这个合约愿意承担的最大费用(gasLimit), 最终的开支可能用不了这么多。当然,如果你的合约很复杂,有可能你愿意买单的这个上限还不够,那么 节点就会返回一个提示,告诉你部署失败,油资不足 —— Out of gas:-)

    deploy()方法的第一个参数和最后一个参数之间,需要按合约构造函数的参数要求依次传入。例如,对于 投票合约,我们只需传入一个候选人名单(数组)。

    更新 truffle 配置文件

    Truffle在执行任务时,将读取当前目录下的配置文件truffle.js。通常我们在该配置文件中 声明要连接的以太坊节点地址,例如localhost:8545

    require('babel-register')
    module.exports = {
     networks: {
      dev: {
       host: 'localhost',
       port: 8545,
       network_id: '*',
       gas: 470000
      }
     }
    }
    

    你应该会注意到gas 选项。这是一个会应用到所有迁移任务的全局变量。当我们调用deploy()方法部署一个 合约时,如果没有声明愿意承担的油费,那么Truffle就会采用这个值作为该合约的部署油资。

    另一个值得指出的是,Truffle支持将合约部署到多个区块链网络,例如开发网络、私有网络、测试网或公网。 在上面的配置文件中,我们仅定义了一个用于开发的网络dev —— 你知道它指向的是ganache模拟器,Truffle 在执行命令时将自动连接到这个网络。

    思考与练习:

    参考教程,编写投票合约的迁移脚本。

    5、合约的编译与部署

    在Truffle中执行compile命令来编译contracts下的所有合约:

    ~/repo/tfapp$ truffle compile
    Compiling Migrations.sol...
    Compiling Voting.sol...
    Writing artifacts to ./build/contracts
    

    在Truffle中执行migrate命令将编译后的合约部署到链上:

    ~/repo/tfapp$ 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...
    

    如果由于油费不足而导致部署失败,可以尝试增加migrations/2_deploy_contracts.js 里面的 gas 值。比如:deployer.deploy(Voting, ['Rama', 'Nick', 'Jose'], {gas: 500000})

    如果希望自选一个账户来部署合约,而不是使用默认的accounts[0],可以在迁移脚本中使用from 选项指定,例如:deployer.deploy(Voting, ['Rama', 'Nick', 'Jose'], {gas: 500000,from:'0x8cff691c888afe73ffa3965db39be96ba3b34e49'}) 也可以在 truffle.js 中指定默认的用来与区块链交互的账户地址:

    module.exports = {
     networks: {
      dev: {
       host: 'localhost',
       port: 8545,
       network_id: '*',
       gas: 470000,
       from: '0x8cff691c888afe73ffa3965db39be96ba3b34e49'
      }
     }
    }
    

    思考与练习:

    参考教程,编译并部署投票合约。

    6、控制台和网页交互

    部署顺利的话,现在就可以通过控制台和网页与合约进行交互了。

    使用Truffle控制台

    在第二个终端中输入truffle console命令进入控制台:

    ~/repo/tfapp$ truffle console
    truffle(development)> Voting.deployed().then(function(contractInstance) {contractInstance.voteForCandidate('Rama').then(function(v) {console.log(v)})})
    
    { blockHash: '0x7229f668db0ac335cdd0c4c86e0394a35dd471a1095b8fafb52ebd7671433156',
    blockNumber: 469628,
    contractAddress: null,
    ....
    ....
    truffle(default)> Voting.deployed().then(function(contractInstance) {contractInstance.totalVotesFor.call('Rama').then(function(v) {console.log(v)})})
    { [String: '1'] s: 1, e: 0, c: [ 1] }
    

    注意,truffle 的所有调用都会返回promise,这就是为什么每个响应都被包裹在 then() 函数里的原因。

    通过网页交互

    进入build目录,先建立网页资源文件的符号连接,然后启动web服务器:

    ~/repo/tfapp/build$ ln -s ~/repo/common/lib lib
    ~/repo/tfapp/build$ ln -s ~/repo/common/fonts fonts
    ~/repo/tfapp/build$ python -m SimpleHTTPServer
    

    现在,在实验环境的嵌入浏览器中点击刷新按钮。BING!

    思考与练习:

    参考教程,分别使用控制台和网页,实现与合约的交互。

    7、总结

    你已经成功地利用Truffle构建了去中心化的投票应用。恭喜!

    下面是进一步学习以太坊的相关资源链接:

    在下面的课程中,我们将学习通证的使用,并让投票应用支持通证的购买和消费。

    4、使用数字代币/通证

    1、概述

    在以太坊中,你会遇到的一个重要概念就是通证(token),也就是常说的加密数字币,或者代币:

    coins

    通证就是在以太坊上构建的数字资产,可以用它来表示现实世界里的东西,比如黄金,或者是自己 的数字资产(就像货币一样)。通证实际上就是智能合约,并没有什么神奇之处。

    • 黄金通证:银行可以有 1 千克的黄金储备,然后发行 1千个通证。买 100 个 黄金通证 就等于买 100 克的黄金。
    • 公司股票:公司股票可以用以太坊上的代币来表示。通过支付以太,人们可以购买公司股票。
    • 游戏币:在一个多玩家游戏中,游戏者可以用以太购买游戏币,并在游戏中进行消费。
    • Golem通证:这是一个基于以太坊的真实项目,个人可以通过租售空闲的 CPU 来赚取通证。
    • 忠诚度积分:商店可以给购物者发行通证作为忠诚度积分,它可以在将来作为现金回收,或是在第三方市场售卖。

    在合约中如何实现通证,实际上并没有限制。但是,以太坊有一个叫做ERC20的通证标准,该标准还 在不断进化中。ERC20通证的优点是很容易与其他的符合ERC20标准的通证进行交换,同时,也更容易 将你的通证集成到其他DApp中。

    在下一节中,我们为投票应用增加通证和支付功能。总的来说,后续课程将涵盖以下内容:

    • 学习并掌握新的数据类型,比如结构体(struct),以便在区块链上组织和存储数据
    • 理解通证概念并实现投票应用的通证
    • 学习使用以太币进行支付,以太币是以太坊区块链平台的数字加密货币。

    2、加权投票应用

    一提到投票,你通常会想起普通的选举,例如,通过投票来选出一个国家的首相或总统。在这种情况下, 每个公民都会有一票,可以投给他们支持的候选人。

    还有另外一种加权投票(weighted voting),它常常用于公开上市交易的公司。 在这些公司,股东的投票权取决于其持有的股票数量。比如,如果你拥有 10,000 股公司股票,你就有 10,000 个投票权(而不是普通选举中的一票)。关于加权投票的更多内容可以查看这里

    例如,假设有一个叫做Block的上市公司。公司有 3 个空闲职位,分别是总裁、副总裁和部长,以及 一组候选人。该公司希望通过股东投票的方式来决定哪个候选人得到哪个职位。获得最高票数的候选人 将会成为总裁,然后是副总裁,最后是部长。

    针对这个应用场景,我们可以构建一个DApp来发行公司股票,该应用允许任何人购买股票从而成为股东。 股东基于其拥有的股票数为候选人投票。例如,如果你持有10,000 股,你可以一个候选人投 5,000 股, 另一个候选人 3,000 股,第三个候选人 2,000 股。

    这里是我们将要在本课程实现应用的图示,任何人都可以调用合约的buy()方法来购买公司发行的 股票通证,然后就可以调用合约的voteForCandidate()方法为特定的候选人投票:

    votingn transaction

    在下一节,我们将会勾勒出实现框架,并随后实现构建完整应用的所有组件。

    3、实现思路

    经过简单地思考,我们将按以下思路来实现加权投票应用:

    首先初始化一个新的truffle项目,然后修改关键代码文件:

    • 投票合约:Voting.sol
    • 合约迁移脚本:2_deploy_contracts.js
    • 前端代码:index.htmlapp.jsapp.css

    在部署合约时初始化参与竞争的候选人名单。从之前的课程中,相信你已经知道了如何实现这一点。 我们将会在迁移脚本2_deploy_contracs.js中完成这个任务。

    由于投票人需要先持有公司股票。所以,我们还需要在部署合约时初始化公司发行的股票总量。 这些股票就是构成公司的数字资产。在以太坊的世界中,这些数字资产被称为通证(Token)。 因此,从现在开始,我们将会把这些股票称为股票通证。

    需要指出的是,股票可以看做是一种通证,但是并非所有的以太坊通证都是股票。股票仅仅是 我们前一节中提到的通证使用场景的一种。

    我们还需要向投票合约中增加一个新的方法,以便任何人都可以购买这些通证。容易理解,投票人 给候选人投票时将使用(消耗)这些股票通证。

    接下来还需要添加一个方法来查询投票人的信息,以及他们分别给谁投了票、总共持有多少股票通证、 还有多少可用的通证余额等等。

    为了跟踪所有这些数据,我们需要使用几个mapping类型的字段,同时还需要引入新的数据类型struct(结构体) 来组织投票人信息。

    项目初始化

    和原来一样,我们使用trufflewebpack项目模版来初始化一个新项目, 并从contracts目录下移除无用的合约文件:

    ~$ mkdir -p ~/repo/tkapp
    ~$ cd ~/repo/tkapp
    ~/repo/tkapp$ truffle unbox webpack
    ~/repo/tkapp$ rm contracts/ConvertLib.sol contracts/MetaCoin.sol
    

    思考与练习:

    参考教程,在repo目录下建立新项目tkapp并进行初始化。

    4、加权投票合约设计

    新的投票合约要比之前复杂一些:

    weighted voting

    之前的投票合约仅仅包含两个状态:数组candidateList保存候选人名单,字典votesReceived跟踪每个候选人获得的投票。

    在加权投票合约中,我们需要额外跟踪一些数据:

    • 投票人信息:solidity的结构体(struct)类型可以将相关数据组织在一起。用结构体来存储投票人 信息非常好。如果你之前不了解struct类型,可以将其视为面向对象开发中没有方法的类。我们将使用 一个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声明
      }
    }
    

    可以在实验环境编辑器中打开~/repo/chapter3/contracts/Voting.sol来查看完成后的加权投票合约。

    思考与练习:

    阅读教程,理解加权投票合约与简单投票合约的区别。

    5、合约实现 —— 购买通证

    合约的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]})})
    

    思考与练习:

    参考教程,实现加权投票合约的buy()方法。

    6、合约实现 —— 加权投票

    如前所述,加权投票方法不仅要指定候选人名称,还要指定使用多少股票通证来支持该候选人。 我们分别用candidatevotesInTokens来表示这两个参数:

    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;
    

    参考教程,实现加权投票合约的voteForCandidate()方法。

    7、合约实现 —— 转账

    当一个用户调用buy()方法发送以太来购买了合约发行的股票通证后,合约收到的资金去了哪里?

    所有收到的资金(以太)都在这个投票合约里。每个合约都有它自己的地址,因此也是一个账户。 在以太坊里,这种账户被称为合约账户(Contract Account),而之前的人员账户,则被称为外控账户 (External Controlled Account)。

    因此,合约的地址里存着这些销售收益。

    我们新增加的transferTo()方法,可以将合约里的资金转移到指定账户:

    function transferTo(address account) public {
      account.transfer(this.balance);
    }
    

    注意!transferTo()方法的当前实现,并没有限制调用者,因此任何人都可以调用该方法从而 转走投票合约账户里的资金!在生产系统中,你必须添加一些限制条件来避免上面的资金漏洞,例如, 检查目标账户是否在一个白名单里。

    其他

    合约里面剩下的方法都是辅助性的getter方法,仅仅用来返回合约变量的值。

    注意tokensSold()等方法声明中的constant修饰符,这表明该方法是只读的,即方法的执行 并不会改变区块链的状态,因此执行这些交易不会耗费任何gas

    参考教程,实现加权投票合约的transferTo()方法。

    8、合约部署

    与之前的课程类似,我们修改迁移脚本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...
    

    参考教程,编译并部署加权投票合约。

    9、控制台交互

    成功地将合约部署到了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)})})
    

    在控制台里查一下,现在合约里有多少以太币?

    10、网页交互

    现在,你已经知道了新的投票合约可以如约工作。让我们开始构建前端逻辑,以便用户能够通过网页浏览器与合约交互:

    voting-webapp

    HTML

    在实验环境编辑器中打开~/repo/chapter3/app/index.html来查看已经完成的网页。

    如果仔细审查代码的话,你会发现网页中已经没有硬编码的值了。候选人的名字将通过向部署好 的合约查询来进行填充。

    网页也会显示公司发行的股票通证总量,以及已售出和剩余的通证量。

    Javascript

    在实验环境编辑器中打开~/repo/chapter3/app/javascripts/app.js来查看已经完成的JS脚本。

    通过移除候选者姓名等等的硬编码,我们已经大幅改进了 HTML 文件。我们会使用javascript/web3js来填充 HTML页面里的所有值,并实现查询投票人信息的额外功能。

    如果你对 JavaScript 不太熟悉,这些代码可能略显复杂。那么最好先理解populateCandidates()函数的实现。

    我们推荐用 JavaScript 自己实现,预置代码仅作参考之用。可以按照下述指引帮助实现:

    1. 创建一个 Voting 合约的实例
    2. 在页面加载时,初始化并创建 web3 对象。(第一步和第二步与之前的课程一模一样)
    3. 创建一个在页面加载时调用的函数,它需要:
      • 使用 Voting 合约对象,向区块链查询来获取所有的候选者姓名并填充表格。
      • 再次查询区块链得到每个候选人所获得的所有投票并填充表格的列。
      • 填充 token 信息,比如所有初始化的 token,剩余 token,已售出的 token 以及 token 成本。
      • 实现 buyTokens 函数,它在上一节的 html 里面调用。你已经在控制台交互一节中购买了 token。buyTokens 代码与那一节一样不可或缺。
      • 类似地,实现 lookupVoterInfo 函数来打印一个投票人的细节。

    CSS

    在实验环境编辑器中打开~/repo/chapter3/app/stylesheetss/app.css来查看已经完成的样式表。

    启动web服务

    和之前一样,执行以下命令进行构建:

    ~/repo/tkapp$ webpack
    

    然后进入build目录,启动轻量web服务器:

    ~/repo/tkapp/build$ python -m SimpleHTTPServer
    

    现在,在试验环境的嵌入浏览器中点击刷新按钮。如果一切顺利,你可以看到网页, 可以输入一个账户地址(投票人的地址),观察他们的投票行为和股票通证数量的变化。 并且可以购买更多的股票通证,为任意候选者投票并查看投票人信息。

    思考与练习:

    现在合约的实现方式,用户购买股票通证并用通证投票。但是他们投票的方式是向合约发送通证。如果他们必须在未来的 选举中投票怎么办?他们所有的通证都转移到了合约中!

    进一步改进合约的方式是,添加加入一个方法以便于用户能够在投票结束后拿回他们的通证。请在合约中实现这一方法: 查询用户投票的所有通证,并将这些通证返还给用户。

    转载于:https://my.oschina.net/ruoli/blog/1805840

    展开全文
  • 今天,我将向你展示如何在以太坊区块链上构建你的第一个去中心化应用程序或dApp。我将告诉你如何编写你的第一个以太坊智能合约,我们将在两个候选人之间举行选举。我们将针对智能合约编写测试,将其部署到以太坊...

    今天,我将向你展示如何在以太坊区块链上构建你的第一个去中心化应用程序或dApp。我将告诉你如何编写你的第一个以太坊智能合约,我们将在两个候选人之间举行选举。我们将针对智能合约编写测试,将其部署到以太坊区块链,并开发允许帐户投票的客户端应用程序。我们还将研究关键概念,如“什么是区块链?”,“什么是智能合约?”,以及“dApp如何工作?”。

    什么是区块链?

    让我们用一个类比来理解区块链是什么以及它是如何工作的。我们来看一个Web应用程序。

    11831773-49c53674d61f9c1f.png
    image

    通常,当你与Web应用程序交互时,你使用Web浏览器通过网络连接到中心服务器。此Web应用程序的所有代码都位于此中心服务器上,并且所有数据都位于中央数据库中。无论何时与应用程序进行交易,都必须与Web上的此中心服务器进行通信。

    如果我们要在网上构建我们的投票应用程序,我们会遇到一些问题:

      1. 可以更改数据库上的数据:可以多次计数,也可以完全删除。
      1. Web服务器上的源代码也可以随时更改。

    我们不想在网络上构建我们的应用程序。我们希望在区块链上构建它,任何连接到网络的人都可以参与选举。我们希望确保他们的投票得到统计,并且他们只计算一次。那么让我们来看看它是如何工作的。

    区块链不是拥有网络,中心服务器和数据库,而是一个网络和数据库。区块链是计算机的点对点网络,称为节点,共享网络中的所有数据和代码。因此,如果你是连接到区块链的设备,则你是网络中的节点,并且你可以与网络中的所有其他计算机节点进行通信。你现在拥有区块链上所有数据和代码的副本。没有更多的中心服务器。只是一堆在同一网络上相互通信的计算机。

    11831773-e700359e319598fb.png
    image

    而不是集中式数据库,区块链中节点之间共享的所有交易数据都包含在称为块的记录包中,这些记录捆绑在一起以创建公共分类帐。此公共分类帐表示区块链中的所有数据。公共分类帐中的所有数据都通过加密散列来保护,并通过一致性算法进行验证。网络上的节点参与以确保通过网络分发的所有数据副本是相同的。这是我们在区块链上构建投票申请的一个非常重要的原因,因为我们希望确保我们的投票得到计算,并且它没有改变。

    我们的应用程序用户对区块链进行投票会是什么样子?那么,对于初学者来说,用户需要一个带有钱包地址的帐户,其中有一些Ether,以太坊的加密货币。一旦他们连接到网络,他们就会投票并支付一笔小额交易费用来将此交易写入区块链。这笔交易费用称为gas。无论何时投票,网络上的一些节点(称为矿工)都会竞争完成此交易。完成此交易的矿工将获得我们付款投票的以太币。

    作为回顾,当我投票时,我支付gas价格进行投票,当我的投票被记录时,网络上的一台计算机获得我的以太网费用。我反过来相信我的投票是永远准确记录的。

    所以同样重要的是要注意,对区块链进行投票会花费以太币,但只是看到候选人名单没有。这是因为从区块链中读取数据是免费的,但写入区块链则不是。

    什么是智能合约?

    这就是投票过程的工作原理,但我们如何实际编写应用程序代码呢?好吧,以太坊区块链允许我们使用名为智能合约的东西在区块链上使用以太坊虚拟机(EVM)执行代码。

    智能合约是我们应用程序的所有业务逻辑所在。这是我们实际编写应用程序去中心化部分的地方。智能合约负责向区块链读取和写入数据,以及执行业务逻辑。智能联系人使用名为Solidity的编程语言编写,看起来很像Javascript。它是一个完整的编程语言,它允许我们执行Javascript所能提供的许多相同类型的事物,但由于它的用例,它的行为有点不同,正如我们在本教程中将看到的那样。

    区块链上智能合约的功能与网络上的微服务非常相似。如果公共分类帐表示区块链的数据库层,那么智能合约就是与该数据进行交易的所有业务逻辑所在的位置。

    此外,他们被称为智能合约,因为他们代表契约或协议。在我们的投票dApp的情况下,这是一项协议,我的投票将被计算,其他投票只计算一次,并且投票最多的候选人实际上将赢得选举。

    现在让我们快速浏览一下我们正在构建的dApp的结构。

    11831773-b17ac6e62868568a.png
    image

    我们将拥有一个用HTML,CSS和Javascript编写的传统前端客户端。该客户端将连接到我们将安装的本地以太坊区块链,而不是与后端服务器通信。我们将使用Solidity编程语言在选举智能合约中编写关于dApp的所有业务逻辑。我们将这个智能合约部署到我们当地的Etherum区块链,并允许账户开始投票。

    现在我们已经看到了区块链是什么以及它是如何工作的。我们已经看到了为什么我们要在区块链而不是当前的网络上构建我们的投票dApp。我们已经看到,我们希望通过编写将部署到以太坊区块链的智能合约来编写我们的dApp代码。现在让我们跳进去开始编程吧!

    我们将要构建什么

    以下是我们将要构建的投票dApp的演示。

    11831773-325142e748b21547.gif
    image

    我们将构建一个客户端应用程序,它将与我们在区块链上的智能合约进行对话。此客户端应用程序将有一个候选人表,列出每个候选人的ID,姓名和投票计数。它将有一个表格,我们可以为我们想要的候选人投票。它还会在你的帐户your account下显示我们与区块链关联的帐户。

    安装依赖项

    为了构建我们的dApp,我们首先需要一些依赖项。

    节点包管理器(NPM)

    我们需要的第一个依赖是Node Package Manager,或NPM,它随Node.js一起提供。你可以通过转到终端并键入以下内容来查看是否已安装节点:

    $ node -v
    

    Truffle框架

    下一个依赖是Truffle Framework,它允许我们在以太坊区块链上构建去中心化的应用程序。它提供了一套工具,允许我们使用Solidity编程语言编写智能联系人。它还使我们能够测试我们的智能合约并将其部署到区块链。它还为我们提供了开发客户端应用程序的空间。

    你可以在命令行中使用NPM安装Truffle,如下所示:

    $ npm install -g truffle
    

    Ganache

    下一个依赖项是Ganache,一个本地内存中的区块链。你可以通过从Truffle Framework网站下载来安装Ganache。它将为我们提供10个外部账户,并在我们当地的以太坊区块链上提供地址。每个帐户都预装了100个假以太坊币。

    Metamask

    下一个依赖项是Google Chrome的Metamask扩展。为了使用区块链,我们必须连接到它(记住,我说块链是一个网络)。我们必须安装一个特殊的浏览器扩展才能使用以太坊区块链。这就是metamask的用武之地。我们将能够通过我们的个人账户连接到我们当地的以太坊区块链,并与我们的智能合约进行互动。

    我们将在本教程中使用Metamask chrome扩展,因此如果你还没有安装Google Chrome浏览器,则还需要安装它。要安装Metamask,请在Google Chrome网上应用店中搜索Metamask Chrome插件。安装完成后,请确保在扩展列表中选中它。安装后,你会在Chrome浏览器的右上角看到狐狸图标。如果卡住了,请参考视频演示!

    语法突出显示

    依赖项是可选的,但建议使用。我建议为Solidity编程语言安装语法高亮显示。大多数文本编辑器和IDE没有开箱即用的Solidity语法高亮显示,因此你必须安装一个软件包才能支持此功能。我正在使用Sublime Text,我已经下载了Ethereum软件包,它为Solidity提供了很好的语法高亮。

    Smoke测试 - 第1步

    现在我们已经安装了依赖项,让我们开始构建我们的dApp!

    首先,找到你下载Ganache的地方,然后打开它。现在Ganache启动了,你有一个本地区块链运行。

    11831773-f3a7adfc5e5d7188.gif
    image

    Ganache为我们提供了10个预装了100个假以太坊的帐户(这在主要的以太坊网络上是不值得的)。每个帐户都有一个唯一的地址和一个私钥。每个帐户地址将作为我们选举中每位选民的唯一标识符。

    现在让我们在命令行中为dApp创建一个项目目录,如下所示:

    $ mkdir election
    $ cd election
    

    现在我们已经进入了我们的项目,我们可以使用Truffle box快速启动和运行。我们将在本教程中使用Pet Shop box。从项目目录中,从命令行安装Pet Shop box,如下所示:

    $ truffle unbox pet-shop
    

    让我们看看Pet Shop box给了我们什么:

    11831773-610a10882d64517a.png
    image
    • 合约目录:这是所有智能联系人所在的地方。我们已经有一个迁移合约来处理我们到区块链的迁移。
    • 迁移目录:这是所有迁移文件所在的位置。这些迁移类似于需要迁移来更改数据库状态的其他Web开发框架。每当我们将智能合约部署到区块链时,我们都会更新区块链的状态,因此需要迁移。
    • node_modules目录:这是我们所有Node依赖项的主页。
    • src目录:这是我们开发客户端应用程序的地方。
    • 测试目录:这是我们为智能合约编写测试的地方。
    • truffle.js文件:这是我们的Truffle项目的主要配置文件。

    现在让我们开始写我们的智能合约!这个智能合约将包含我们的dApp的所有业务逻辑。它将负责读取和写入以太坊区块链。它将允许我们列出将参加选举的候选人,并跟踪所有选票和选民。它还将管理所有选举规则,例如强制执行只投票一次的帐户。从项目的根目录开始,在contract目录中创建一个新的合约文件,如下所示:

    $ touch contracts/Election.sol
    

    让我们首先创建一个冒烟测试,确保我们正确设置项目,并且我们可以成功地将合约部署到区块链。打开文件并从以下代码开始:

    pragma solidity 0.4.2;
    
    contract Election {
        // Read/write candidate
        string public candidate;
    
        // Constructor
        function Election () public {
            candidate = "Candidate 1";
        }
    }
    

    让我解释一下这段代码。我们首先使用pragma solidity语句声明solidity版本。接下来,我们使用contract关键字声明智能合约,然后是合约名称。接下来,我们声明一个状态变量,它将存储候选名称的值。状态变量允许我们将数据写入区块链。我们已声明此变量将是一个字符串,并且我们已将其可见性设置为public。因为它是公开的,所以solidity将为我们提供免费的getter功能,这将允许我们在合约之外访问此值。我们稍后会在控制台中看到它!

    然后,我们创建一个构造函数,只要我们将智能合约部署到区块链,就会调用它。这是我们将设置候选状态变量的值,该变量将在迁移时存储到区块链中。请注意,构造函数与智能合约具有相同的名称。这就是Solidity知道函数是构造函数的方式。

    现在我们已经为智能合约创建了基础,让我们看看我们是否可以将它部署到区块链。为此,我们需要在迁移目录中创建一个新文件。从项目根目录,从命令行创建一个新文件,如下所示:

    $ touch migrations/2_deploy_contracts.js
    

    请注意,我们使用数字对迁移目录中的所有文件进行编号,以便Truffle知道执行它们的顺序。让我们创建一个新的迁移来部署合约,如下所示:

    var Election = artifacts.require("./Election.sol");
    
    module.exports = function(deployer) {
      deployer.deploy(Election);
    };
    

    首先,我们需要我们创建的合约,并将其分配给名为Election的变量。接下来,我们将其添加到已部署合约的清单中,以确保在我们运行迁移时部署它。现在让我们从命令行运行我们的迁移,如下所示:

    $ truffle migrate
    

    现在我们已成功将智能合约迁移到当地以太坊区块链,让我们打开控制台与智能合约进行互动。你可以从命令行打开松露控制台,如下所示:

    $ truffle console
    

    现在我们已进入控制台,让我们获取已部署智能合约的实例,看看我们是否可以从合约中读取候选人的姓名。从控制台,运行以下代码:

    Election.deployed().then(function(instance) { app = instance })
    

    这里Election是我们在迁移文件中创建的变量的名称。我们使用deployed()函数检索了部署的合约实例,并将其分配给promise的回调函数中的app变量。

    现在我们可以像这样读取候选变量的值:

    app.candidate()
    // => 'Candidate 1'
    

    恭喜!你刚刚编写了第一份智能合约,已部署到区块链,并检索了部分数据。继续学习以太坊Dapp终极教程——如何构建一个完整的全栈去中心化应用(二)

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

    分享一些比特币、以太坊、EOS、Fabric等区块链相关的交互式在线编程实战教程:

    • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
    • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
    • c#比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在C#代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是C#工程师不可多得的比特币开发学习课程。
    • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
    • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
    • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
    • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
    • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
    • ERC721以太坊通证实战,课程以一个数字艺术品创作与分享DApp的实战开发为主线,深入讲解以太坊非同质化通证的概念、标准与开发方案。内容包含ERC-721标准的自主实现,讲解OpenZeppelin合约代码库二次开发,实战项目采用Truffle,IPFS,实现了通证以及去中心化的通证交易所。
    • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
    • EOS入门教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
    • 深入浅出玩转EOS钱包开发,本课程以手机EOS钱包的完整开发过程为主线,深入学习EOS区块链应用开发,课程内容即涵盖账户、计算资源、智能合约、动作与交易等EOS区块链的核心概念,同时也讲解如何使用eosjs和eosjs-ecc开发包访问EOS区块链,以及如何在React前端应用中集成对EOS区块链的支持。课程内容深入浅出,非常适合前端工程师深入学习EOS区块链应用开发。
    • Hyperledger Fabric 区块链开发详解,本课程面向初学者,内容即包含Hyperledger Fabric的身份证书与MSP服务、权限策略、信道配置与启动、链码通信接口等核心概念,也包含Fabric网络设计、nodejs链码与应用开发的操作实践,是Nodejs工程师学习Fabric区块链开发的最佳选择。
    • Hyperledger Fabric java 区块链开发详解,课程面向初学者,内容即包含Hyperledger Fabric的身份证书与MSP服务、权限策略、信道配置与启动、链码通信接口等核心概念,也包含Fabric网络设计、java链码与应用开发的操作实践,是java工程师学习Fabric区块链开发的最佳选择。
    • tendermint区块链开发详解,本课程适合希望使用tendermint进行区块链开发的工程师,课程内容即包括tendermint应用开发模型中的核心概念,例如ABCI接口、默克尔树、多版本状态库等,也包括代币发行等丰富的实操代码,是go语言工程师快速入门区块链开发的最佳选择。

    汇智网原创翻译,转载请标明出处。这里是以太坊Dapp终极教程——如何构建一个完整的全栈去中心化应用(一)

    展开全文
  • 以太坊dappIn 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. 在...

    以太坊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.solTestStoryDao.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 。 输出应与此类似:

    A successful 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

    展开全文
  • 本视频课程面向初学者,介绍什么是区块链,什么是智能合约,内容涵盖以太坊开发相关的基本概念,Gregory(Dapp University 创始人,专注于讲解在以太坊区块链上构建DApp应用程序教程。Gregory大神的以太坊课程在...

    本视频课程面向初学者,介绍什么是区块链,什么是智能合约,内容涵盖以太坊开发相关的基本概念,Gregory(Dapp University 创始人,专注于讲解在以太坊区块链上构建DApp应用程序教程。Gregory大神的以太坊课程在youtube上超过五千位订阅者,所有课程超过10万人次观看,深受区块链技术开发用户的喜爱 )大神在课程中手把手地教大家如何构建一个基于以太坊的完整去中心化应用 —— 区块链投票系统。


    课程介绍

    本视频课程面向初学者,介绍什么是区块链,什么是智能合约,内容涵盖以太坊开发相关的基本概念,Gregory大神在课程中手把手地教大家如何构建一个基于以太坊的完整去中心化应用 —— 区块链投票系统。

    通过本课程的学习,你将掌握:

    1、以太坊区块链开发的基本知识

    2、开发和部署以太坊合约所需的软件环境

    3、使用高级语言(solidity)编写以太坊智能合约

    4、使用NodeJS编译、部署合约并与之交互

    5、使用Truffle框架开发分布式应用

    6、使用控制台或网页与合约进行交互


    课程内容


    【精品】以太坊DApp开发入门实战-区块链投票系统视频教程



    什么是区块链?

    我们用一个比喻来明白什么是区块链,它的工作原理是怎样?让我们先看一下网页应用程序。

    当我们和网页应用程序交互时,你使用网页浏览器去连接我们的应用中心服务器。所有的网页应用都基于中心服务器,所有的数据都在中心数据库。任何时候在应用程序上交易,你都需要和网页中心服务器沟通。

    如果我们在网页应用程序上投票选举,投票选举时我们会有以下问题:

    1. 数据库里的数据可能会被改变:或者重复投票:也可能投票数据全部被删除。

    2. 网页服务器上的源代码在任何时候都可能会被改变。

    我们不愿意在网页上创建应用程序。我们更愿意把它创建在大家能连接的区块链上,确保一人投票一次,且不可篡改。让我们看看在区块链上怎么去实现它。


    区块链不是一个中央服务器和一个数据库,而是一个网络和一个数据库。区块链是计算机的对等网络,称为节点,共享网络中的所有数据和代码。因此,如果您是连接到区块链的设备,则您是网络中的一个节点,并且您可以与网络中的所有其他计算机节点通话。您也拥有区块链上所有数据和代码的副本。区块链上没有中央服务器,只有不同设备之间在同属性下的网络点对点交流。

    区块链取代中心化数据库,所有的交易都在区块链上,所有包含的记录称之为区块。所有连接起来的区块叫区块链,区块链之间创建公共账本,公共账本代表区块链里面的所有数据。


    公共账本的所有数据都通过加密技术哈希密码进行保护,并通过共识算法进行验证。网络上节点的参与确保网络上分散的所有数据副本都是一样的。这就是我们为什么要在区块链上构建投票应用程序,因为我们希望确保我们的投票都被计算在内,而且没有被篡改。

    如果我们的投票应用程序应用在区块链上会怎么样?

    对于新用户来讲,新用户需要一个带钱包地址和有ETH的账户。ETH也就是以太坊的加密货币。一旦连接到区块链网络,投票会在区块链上消耗一些ETH。

    这些交易费用称之为“燃气”。当开始投票是,网络上帮助完成这笔交易的矿工将获得这笔交易费用的ETH。我投票的记录就永远被记录下来了。

    记住一点:投票是一种交易,交易会消耗ETH,但读取数据是免费的。


    什么是智能合约?

    智能合约就是在以太坊虚拟机(EVM) 上执行我们代码的过程。

    智能合约的编程语言是Solidity,有点像Javascript,但有点不一样。Solidity编程语言可以实现我们所有的商务交易逻辑。

    如果说公共账本代表网页的数据库,那么智能合约就是实现所有商务逻辑交易的地方。

    现在让我们快速的看看我们构建的DApp的结构。

    传统的前端应用程序用的是HTML, CSS, 和 Javascript.语言。

    代替传统的前端应用程序是后端服务器,客户安装连接本地以太坊区块链,去用Solidity语言编译去中心化选举。部署智能合约到本地区块链上进行投票选举。

    让我们一起来看看区块链的工作原理,为什么我们要用区块链代替下当前的中心化应用程序。


    我们要创建什么?

    我们将构建一个客户端应用程序,与我们在区块链上的智能合约进行对话。这个客户端应用程序将有一个候选人列表,列出每个候选人的ID,姓名和投票数。 它会有一个表格,我们可以为我们想要的候选人投一票。 它还显示我们在“您的帐户”下连接到区块链的帐户。


    安装依赖项

    创建DApp之前, 首先需要安装依赖项.


    节点包管理器 (NPM)

    我们需要的第一个依赖是Node Package Manager或NPM,它随Node.js一起的。你可以看看你的节点是否已经安装了,你可以去你的终端并输入:

    $ node -v


    Truffle 框架

    下一个依赖项是Truffle框架它可以使我们在以太坊区块链上创建分布式应用,提供一套用solidity编译的智能合约工具,也可以帮助我们测试智能合约部署区块链,它也为我们开发客户端应用程序提供了一个地方。在命令行中这样安装truffle和NPM:

    $ npm install -g truffle


    Ganache

    下一个依赖项是Ganache,一个本地内存区块链。你也可以在Truffle Framework 网址下载安装Ganache。它将会提供以太坊外部账户,每个账户里都有100个假的ETH和它的钱包地址。


    Metamask谷歌扩展插件

    我们下一个要安装的依赖项是:Metamask谷歌扩展插件。Metamask帮助我们用个人账户连接本地区块链网络进行交互。开始之前,记得安装Metamask谷歌扩展插件哦。


    语法高亮(Syntax Highlighting)

    下一个依赖项是语法高亮,很多编辑器和IEDs没有语法高亮,我建议在用solidity编程是安装语法高亮。所以你要安装一个这样的安转包,教程里用我用的是Sublime Text,我已经在“以太坊”包下载了这个语法高亮。


    1. Smoke Test - 步骤一

    好了,现在我们所以的依赖项都安装好了,让我们开始创建我们的DApp吧。

    首先,下载Ganache并打开,确保你的本地区块链正常运作

    Ganache给我们10个账户,每个账户100个假的ETH。

    个帐户都有一个唯一的地址和一个私钥。每个帐户地址将作为我们选举中每个选民的唯一标识符。现在让我们创建一个Dapp的项目目录:

    $ mkdir election

    $ cd election


    现在我们进入项目内部了,Truffle box使其快速运行。这个教程我们使用PET Shop box.从您的项目目录中, 像这样从命令栏安装 pet shop box:

    $ truffle unbox petshop


    看看 pet shop box给我们带来什么:

    · contracts 目录: this is where all smart contacts live. We already have a Migration contract that handles our migrations to the blockchain.

    这就是智能合约所在的地方。在区块链上我们已经有Migration(迁移)合约处理我们的迁移。


    · migrations 目录:是所有migration文件所在地方。当在区块链上更新区块链状态时就需要migration。就像网页开发框架里面需要migration来更新数据状态一样。


    · node_modules 目录: 这是所有的节点依赖项的大本营。


    · src 目录: 这是我们开发客户端应用程序的地方。


    · test 目录: 这是我们为智能合约编写测试的地方。


    · truffle.js 文件:这是 Truffle项目的主要配置文件.


    让我们一起来在合约目录下创建新合约吧:

    $ touch contracts/Election.sol


    我们首先创建一个“烟雾测试”,以确保我们已经正确设置了项目,并成功地将合约部署到区块链。打开文件并从下面的代码开始:

    pragma solidity 0.4.2;

    contract Election {

    // Read/write candidate

    string public candidate;


    // Constructor

    function Election () public {

    candidate = "Candidate 1";

    }

    }


    现在我们已经为智能合约创建了基础,让我们看看是否可以将其部署到区块链。因此,我们需要在migrations目录中创建一个新文件。从您的项目根目录中,像这样从命令行中创建一个新文件:

    $ touch migrations/2_deploy_contracts.js


    请注意,我们使用数字为migrations目录中的所有文件编号,以便Truffle知道执行它们的顺序。让我们创建一个这样新的migration部署的合约:

    var Election = artifacts.require("./Election.sol");

    module.exports = function(deployer) {

    deployer.deploy(Election);

    };


    首先,我们需要创建合约,并将其分配给名为“Election”的变量。 接下来,我们将其添加到已部署合同的清单中,以确保在我们运行migrations时将其部署。 现在让我们像这样从命令行中运行migrations

    $ truffle migrate:


    现在我们已经成功将我们的智能合约迁移到当地的以太坊区块链, 让我们打开控制台与智能合约交互。 你可以像这样从命令栏中打开truffle控制器:

    $ truffle console


    现在我们进入了控制台,让我们来看看我们部署的智能合约的一个实例,看看我们是否可以从合约中读取候选人的名字。从控制台运行此代码:

    Election.deployed().then(function(instance) { app = instance })


    这里的Election 是我们在migration文件中创建的变量的名称。我们在这个合约例子里检索了一个部署deployed() 函数,分配给一个app变量在promise的回调函数中。现在我们可以像这样读取候选变量的值:

    app.candidate()

    // => 'Candidate 1'


    恭喜你!你刚刚编写了第一份智能合约,并将其部署到区块链中,并检索了一些数据。


    步骤二:候选人名单。

    我们需要一种方法来存储多个候选人,并存储关于每个候选人的多个属性。我们希望跟踪候选人的ID,姓名和投票数。 以下是我们将如何对候选人进行模拟:

    contract Election {

    // Model a Candidate

    struct Candidate {

    uint id;

    string name;

    uint voteCount;

    }

    // ...

    }


    我们使用Solidity Struct为候选人建模,Solidity允许我们创建自己的结构类型,就像我们在这里为候选人所做的那样。 我们指定这个结构有一个无符号整数类型的ID,字符串类型的名称和无符号整数类型的voteCount。 简单地声明这个结构体实际上不会给我们一个候选人。 我们需要实例化它并在将它写入存储之前将其分配给一个变量。


    接下来我们需要的是一个储存候选人的地方。我们需要一个地方来存储我们刚刚创建的一种结构类型。我们可以使用Solidity映射进行此操作。 Solidity中的映射类似于关联数组或哈希,它将键 - 值对关联起来。 我们可以像这样创建这个映射:

    contract Election {

    // Model a Candidate

    struct Candidate {

    uint id;

    string name;

    uint voteCount;

    }

    // Read/write Candidates

    mapping(uint => Candidate) public candidates;

    // ...

    }


    在这种情况下,映射的关键是一个无符号整数,并且该值是我们刚刚定义的Candidate结构类型。 这基本上给了我们一个基于id的查找每个候选人。 由于该映射被分配给一个状态变量,因此只要我们为其分配新的键值对,我们就会将数据写入区块链。 接下来,我们将该映射的可见性设置为public,以获得getter函数,就像我们在烟雾测试中使用候选名称一样。


    接下来,我们通过一个像这样的计数器缓存状态变量来跟踪选举中存在多少候选者:

    contract Election {

    // Model a Candidate

    struct Candidate {

    uint id;

    string name;

    uint voteCount;

    }

    // Read/write Candidates

    mapping(uint => Candidate) public candidates;

    // Store Candidates Count

    uint public candidatesCount;

    // ...

    }


    在Solidity中,无法确定映射的大小,也无法对它进行迭代。 这是因为尚未分配值的映射中的任何键都会返回默认值(在这种情况下为空候选)。 例如,如果我们在这次选举中只有2名候选人,并且我们试图查找候选人#99,那么映射将返回一个空的候选人结构。 这种行为使得不可能知道有多少候选者存在,因此我们必须使用计数器缓存

    接下来,我们创建一个函数,将候选添加到我们创建的映射中:

    contract Election {

    // ...

    function addCandidate (string _name) private {

    candidatesCount ++;

    candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);

    }

    }


    我们已经声明了函数addCandidate,它接受一个表示候选人姓名的字符串类型参数。 在函数内部,我们递增候选计数器缓存以表示添加了新的候选项。 然后我们用一个新的Candidate结构更新映射,使用当前的候选计数作为关键。 这个Candidate结构体使用来自当前候选计数的候选id,函数参数中的名称以及初始投票计数初始化为0.请注意,此函数的可见性是私有的,因为我们只想在合同中调用它。

    现在,我们可以通过在构造函数内两次调用“addCandidate”函数来添加两个候选人,如下所示:

    contract Election {

    // ...

    function Election () public {

    addCandidate("Candidate 1");

    addCandidate("Candidate 2");

    }

    // ...

    }


    当我们将合同部署到区块链时,这个迁移将会执行,并且会有两名候选人参加我们的选举。此时,您的完整合同代码应如下所示:

    pragma solidity ^0.4.2;

    contract Election {

    // Model a Candidate

    struct Candidate {

    uint id;

    string name;

    uint voteCount;

    }

    // Read/write candidates

    mapping(uint => Candidate) public candidates;

    // Store Candidates Count

    uint public candidatesCount;

    function Election () public {

    addCandidate("Candidate 1");

    addCandidate("Candidate 2");

    }

    function addCandidate (string _name) private {

    candidatesCount ++;

    candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);

    }

    }


    现在让我们像这样迁移我们的合同:

    $ truffle migrate --reset


    现在尝试与控制台内的候选人进行交互。

    现在让我们编写一些测试来确保我们的智能合约正确初始化。首先,让我解释为什么在开发智能合同时测试如此重要。


    Testing

    现在我们来写一些测试。确保你有Ganache第一次运行。 然后,从项目根目录的命令行中创建一个新的测试文件,如下所示::

    $ touch test/election.js

    我们将用the Mocha testing framwork和the Chai assertion library在这个文件中使用Javascript编写我们所有的测试。这些与Truffle框架捆绑在一起。 我们将使用Javascript编写所有这些测试,以模拟客户端与我们的智能合约的交互,就像我们在控制台中所做的一样。 这里是测试的所有代码:

    var Election = artifacts.require("./Election.sol");

    contract("Election", function(accounts) {

    var electionInstance;

    it("initializes with two candidates", function() {

    return Election.deployed().then(function(instance) {

    return instance.candidatesCount();

    }).then(function(count) {

    assert.equal(count, 2);

    });

    });

    it("it initializes the candidates with the correct values", function() {

    return Election.deployed().then(function(instance) {

    electionInstance = instance;

    return electionInstance.candidates(1);

    }).then(function(candidate) {

    assert.equal(candidate[0], 1, "contains the correct id");

    assert.equal(candidate[1], "Candidate 1", "contains the correct name");

    assert.equal(candidate[2], 0, "contains the correct votes count");

    return electionInstance.candidates(2);

    }).then(function(candidate) {

    assert.equal(candidate[0], 2, "contains the correct id");

    assert.equal(candidate[1], "Candidate 2", "contains the correct name");

    assert.equal(candidate[2], 0, "contains the correct votes count");

    });

    });

    });


    第一个测试通过检查候选人数量是否等于2来检查合同是否被初始化了正确的候选人数。

    下一个考试考查每个候选人在选举中的价值,确保每个候选人都有正确的ID,姓名和投票数。

    现在让我们像这样从命令行运行测试:

    $ truffle test


    看,编译通过。



    展开全文
  • 一、理解以太坊工具 以太坊有很多工具,随着时间的推移有些会过时,但早期的网络文章会有介绍,从而与目前的工具造成时差上的迷惑,有些工具是随着工程化的需要逐渐流行起来的。有必要梳理以下: 1、go-ethereum:...
  • 简单来说,以太坊Dapp是一个去中心化的web应用,这个应用可以被内嵌到以太坊的网络中。与一般的web应用相比,以太坊Dapp在以下两点有所不同。 以太坊Dapp是与以太网的网络进行交互,而不是服务器。 以太坊Dapp...
  • 本篇文章记录着自己接触区块链,学习以太坊的一些笔记以及自己的学习路径,让跟我一样刚学习的人,更容易地学习区块链技术。学完下面部分内容,我们会把左脚迈进区块链开发的大门,理清区块链世界的核心术语,以及...
  • 以太坊分析之 智能合约和DAPP

    千次阅读 2018-05-28 10:19:29
     智能合约 以太坊是一个运行着智能合约的分布式平台,智能合约本质就是脚本,这些脚本可以处理各种业务逻辑来利用以太坊区块链的能力,比如彩票、拍卖、物流跟踪等等。这也正是以太坊区别于比特币的最重要的属性,...
  • 以太坊前端dapp开发的最小类型优先样板。 Dapp样板 包括: 基本原理 Next.js,因为它执行自动代码拆分并且具有非常小的API。 与Create-React-App相比,性能更高,最低。 打字稿,因为您不想猜测一切都是什么类型。 ...
  • 以太坊宠物商店 - 记录第一个Dapp

    千次阅读 2019-09-19 11:21:33
    记录以前练习的第一个Dapp宠物商店,使用以太坊作为处理宠物收养的方式。该商店在特定时间可容纳16只宠物,并且他们已经拥有宠物数据库。所以我们做一个将以太坊地址与宠物相关联的dapp,官方提供了网站结构和样式。...
  • 内容|Space-O Technologies编译|Aholiab以太坊是区块链开发领域最好的编程平台,而Truffle是以太坊(Ethereum)最受欢迎的一个...
  • 基于以太坊上实现DApp的登录注册

    千次阅读 2018-05-29 00:41:45
    接下来将会从零开始搭建基于以太坊web3js项目,开始阅读之前,你需要熟练前端或后台JavaScript语法,熟悉区块链思想和原理,如果能了解solidity语法更好,因为接下来我们会用到它,和js很像的一门语言。 为了能够...
  • 来源 | H ackernoon ...出品 | 区块链大本营(blockchain_camp) ...一致性Hash的原理和实践 吴子宁: 手握 280 多项专利的斯坦福技术先锋 | 人物志 猛戳 "阅读原文" 有惊喜哟 老铁在看了吗? ?
  • 作者 |dfuse责编 | Carol出品 |区块链大本营(blockchain_camp)在本文中,我们将重点介绍以太坊上复杂的交易生命周期;开发者在这些情况下尝试让 dapp 提...
  • 以太坊设计原理

    万次阅读 2019-03-27 09:30:00
    尽管以太坊的许多理念在早先的加密货币(如比特币)上已经运用并测试了5年之久,但从处理某些协议功能上来说,以太坊的处理方式与常见方式仍有许多不同。很多时候,以太坊会被用来建立全新的经济方法,因为它具有...
  • 今天,就带你轻松学习以太坊的工作原理。 1 以太坊概览 使用Solidity编程语言编写智能合约。智能合约完全按照程序运行,而且防停机、防审查、防欺诈、防第三方干扰。部署智能合约或者调用其方法需要用到以太币...
  • 区块链开发(二)部署并运行第一个以太坊智能合约 李赫2016年8月10日  网络上不少部署智能合约的文章,但是都有一个共同的特点,就是采用命令行的方式来部署,先是建立SOLC的编译环境,然后部署Geth或者Eth节点,...
  • 既了解区块链底层原理、又熟悉以太坊架构、还能基于以太坊开发DApp的专业人才,也成为了各大公司发力区块链技术储备的重点对象。【课程简介】本套以太坊课程,对以太坊基础理论知识和架构做了系统的梳理和深入的阐述...
  • 「基于以太坊的版权交易平台开发实战课程」 , 限时八折优惠! 师资团队 区块链资深专家 孟岩带队授课,亲自讲授DAPP经济系统设计部分 ,团队成员均在行业内具备多年技术架构及开发实战经验, 正在深度参与柏链道捷...
  • 以太坊白皮书(更新于2021年2月9日版本) 这篇介绍性论文最初由以太坊创始人Vitalik Buterin于2013年发表,该项目于2015年启动。值得注意的是,以太坊和许多社区驱动的开源软件项目一样,从一开始就在不断发展。 ...
  • 以太坊要在互联网世界中把原来对人的信任转变为对数学原理、对技术和对机器的信任,显然并不简单,但这是通过一整套清晰、严谨而又复杂的技术体系来实现的。 一、以太坊基本术语 区块(Block) 由区块头...
  • 以太坊 EVM原理与实现以太坊底层通过EVM模块支持合约的执行与调用,调用时根据合约地址获取到代码,生成环境后载入到EVM中运行。通常智能合约的开发流程是用solidlity编写逻辑代码,再通过编译器编译元数据,最后再...
  • 在整个加密货币市场的市值超过7000...CryptoKitties以其使以太坊区块链拥挤而闻名,是dApp的一个很好的例子,它将可养殖和可收藏的概念与区块链相结合。这个耸人听闻的游戏只是一个创造性的例子,几乎有无限的机会。...
  • 五、 智能合约的运行原理  智能合约是部署在区块链的代码,区块链本身不能执行代码,代码的执行是在本地的EVM中,实际上,部署在区块链上代码是能够在本地产生原智能合约代码的代码,可以理解区块链为一个...
  • 很多人迷惑于区块链和以太坊,不知如何学习,本文简单说了一下学习的一些方法和资源。 一、 以太坊和区块链的关系 从区块链历史上来说,先诞生了比特币,当时并没有区块链这个技术和名词,然后业界从比特币中提取了...
  • &#13; &#13; &#13; &#13; &#13; &#13; &#13; 写给前端开发者的第一本区块链开发入门指南,通过从 0 到 1 实战开发一个 ICO DApp 项目(...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,244
精华内容 497
关键字:

以太坊dapp运行原理