构建以太坊区块链应用
在前一章中,我们回顾了智能合同的基本特征以及如何编写众筹智能合同示例。在我们将智能合同部署到区块链之后,我们需要编写 web 应用程序来与智能合同进行交互。以太坊区块链通过调用智能合约函数和 getters 来提供 web3 API。
在本章中,我们将讨论以下主题:
- 什么是分散应用 ( DApp )
- web3js 快速概述
- 设置以太坊开发环境
- 开发和测试 DApp
分散式应用概述
一个分散式应用(或 DApp )是一个使用智能合约运行的应用。智能合约部署在以太坊虚拟机 ( EVM )上。它类似于客户端-服务器低层架构。DApp 可以有一个前端(web ),它通过 web3.js API 调用其后端(智能合约)。
下面的结构是我们要为我们的众筹 DApp 建立的:
我们将为 DApp 众筹项目建造的建筑
web3.js 快速概述
web3.js 是一个以太坊 JavaScript API,它提供了一组库来与本地或远程以太坊网络进行交互。web3js 和以太坊之间的连接是通过使用 HTTP 或 IPC 协议实现的。在下表中,我们快速回顾了一些重要的 web3.js API 概念:
| API 引用 | 描述 | 例子 |
| web3-eth | 这个包提供了一个与以太坊区块链和智能合约交互的 API | getBalance
、sendTransaction, coinbase
、getBlockNumber, getAccounts
|
| 网络 3-嘘 | 这个包提供了一个 API 来与 whisper 协议交互以进行广播 |
web3.shh.post({
symKeyID: identities[0],
topic: '0xffaadd11',
payload: '0xffffffdddddd1122'
}).then(h => console.log(`Message with hash ${h} was successfuly sent`))
|
| web3-bzz | 这个包提供了一个 API 来与以太坊群交互,以太坊群是一个分散的文件存储平台 | web3.bzz.currentProvider``web3.bzz.download(bzzHash [, localpath])
|
| web3-utils | 这个包为 Ethereum DApps 和其他 web3.js 包提供了一组实用函数 | web3.utils.toWei(number [, unit])``web3.utils.isAddress(address)
|
供应者
一个提供者抽象出一个与以太坊区块链对话的连接。它将发出查询并将交易发送到区块链。web3 提供了JsonRpcProvider
和IpcProvider
,可以让你连接到一个本地或者远程的以太坊节点,包括 Mainnet、Ropsten testnet、Kovan testnet、Rinkeby testnet,以及自定义的远程过程调用 ( RPC ),比如 Ganache。下面的代码片段展示了我们如何使用 web3 API 来连接以太坊节点。
var Web3 = require('web3');
var web3 = new Web3('http://localhost:8545');
// or
var web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));
// change provider
web3.setProvider('ws://localhost:8546');
// or
web3.setProvider(new Web3.providers.WebsocketProvider('ws://localhost:8546'));
DApp 开发工具
有一些流行的区块链 web 开发工具被开发人员用来创建 DApp 项目的基本结构。下面几节列出了其中的一些。
松露
Truffle 是一个以太坊 DApp 端到端开发工具,它为编写、编译和部署测试智能合约和 dapp 提供了一个开发环境。可以为前端写 HTML、CSS、JavaScriptSolidity 用于智能合约,使用 web3.js API 与 UI 和智能合约进行交互。Truffle Boxes 提供了有用的样板文件,其中包含有用的模块、solidity 契约和库、前端代码和许多其他有用的文件。块菌盒帮助开发者快速开始他们的 DApp 项目。
Truffle 命令行使用以下格式:
truffle [command] [options]
以下是命令行工具中常用的选项:
| 命令 | 描述 |
| compile
| 编制合同文件。 |
| console
| 与部署的智能合约进行交互的命令行界面。 |
| create
| 该命令有助于创建新合同、新迁移文件和基本测试。 |
| debug
| 在调试器会话中试验特定的事务。 |
| deploy
/ migration
| 将合同部署到区块链网络。 |
| develop
| 在本地开发环境中通过命令行与合同交互。 |
| init
| 从以太坊包注册表安装一个包。 |
加纳切
Ganache 是一个私人以太坊区块链环境,允许您模仿以太坊区块链,以便您可以在自己的私人区块链中与智能合约进行交互。以下是 Ganache 提供的一些功能:
- 显示区块链日志输出
- 提供高级采矿控制
- 内置块资源管理器
- 以太坊区块链环境
- Ganache 有一个桌面应用程序和命令行工具
这是桌面版的 Ganache 的样子:
命令行使用以下格式:
ganache-cli <options>
这些是命令行工具的常用选项:
| 选项 | 描述 |
| -a or --accounts
| 启动时生成的帐户数量。 |
| -e or --defaultBalanceEther
| 配置默认测试帐户乙醚量。默认为100
。 |
| -b or --blockTime
| 以秒为单位指定块时间作为挖掘间隔。如果未指定该选项,Ganache 将在调用事务时立即挖掘新的块。 |
| -h or --host or --hostname
| 指定要监听的主机名。默认为127.0.0.1
。 |
| -p or --port
| 指定端口号。默认为8545
。 |
| -g or --gasPrice
| 以卫为单位指定气价(默认为20000000000
)。 |
| -l or --gasLimit
| 阻塞气体限制(默认为0x6691b7
)。 |
| --debug
| 显示用于调试目的的虚拟机操作码。 |
| -q or --quiet
| 运行ganache-cli
而没有任何日志。 |
设置以太坊开发环境
按照这些说明获取以太坊开发工具,并启动以太坊私有本地区块链环境(主要用于将您的智能合约运行/部署到本地区块链)。
安装 Truffle
打开命令行并运行以下命令:
npm install -g truffle
安装 Ganache
打开命令行并安装 Ganache 的命令行界面,如下所示:
npm install -g ganache-cli
创建松露项目
为了初始化一个新的 DApp 项目,我们可以运行 truffle init
命令来初始化一个空的 truffle 项目。这将创建 DApp 目录结构,包括应用程序、合同和带有 Truffle 配置的测试。由于 Truffle Boxes 提供了许多工作模板,在我们的 DApp 示例中,我们将使用 pet-shop box——JavaScript UI 库的 JQuery 版本——来开发我们的众筹 DApp 示例。
创建一个名为Crowdfunding
的文件夹,打开命令行提示符,导航到Crowdfunding
文件夹,并运行以下命令:
truffle unbox pet-shop
项目结构如下:
我们在前一章写了众筹智能合约。让我们将CrowdFunding.sol
文件复制到Crowdfunding
下的 contracts 文件夹中。
启动 Ganache 环境
打开新的终端窗口并运行以下命令:
Ganache-cli
这将在端口8545
上运行Ganache-cli
,Ganache 将为我们创建 10 个默认帐户。默认情况下,每个帐户有 100 个以太网。您应该会在控制台中看到类似这样的内容:
在我们的 Truffle 项目中,truffle.js
将7545
定义为默认端口号。我们需要将端口号更新为8545
,以匹配 Ganache 端口号,如下所示:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 8545,
network_id: "*" // Match any network id
}
}
};
部署智能合同
您可能已经注意到,前面的命令创建了两个与迁移相关的文件,Migrations.sol
和1_initial_migration.js
。Migrations.sol
存储与上次应用的“迁移”脚本相对应的号码。当您添加新合同并部署合同时,商店中最后一次部署的数量将会增加。合同运行一次后,将不会再次运行。编号约定为x_script_name.js
,x 从 1 开始,即1_initial_migration.js
。你的新合同通常会在2_...
开始。
在我们的例子中,我们将添加一个新的迁移契约来部署CrowdFunding
。让我们创建一个名为2_deploy_contracts.js
的文件。
CrowdFunding.sol
定义构造函数如下:
constructor (address _owner, uint _minimumToRaise, uint _durationProjects,
string _name, string _website)
要部署一个带有可选构造函数参数的契约,可以调用 truffle deploy 函数deployer.deploy(contract, args..., options)
。
我们将使用 Ganache 给我们的第一个帐户作为所有者帐户,如下所示:
var CrowdFunding = artifacts.require("./CrowdFunding.sol");
module.exports = (deployer, network, accounts) => {
const ownerAddress = accounts[0];
deployer.deploy(CrowdFunding, ownerAddress, 30, 60, "smartchart", "smartchart.tech");
}
让我们将智能合约部署到我们的网络中。运行truffle
命令,如下所示:
truffle migrate
下面的截图显示了运行truffle migrate
命令的结果:
这将我们的众筹智能合同部署在当地的加纳切区块链环境中。
要启动本地节点服务器,请运行以下命令,这将在我们的浏览器中启动 pet store 页面:
npm run dev
编写活动分散应用程序
我们刚刚在本地加纳切区块链环境中部署了智能合同。现在,我们将开始编写 UI 代码,通过 RPC 调用来触发智能合约功能。本章的源代码可在 https://bit.ly/2X8xPBL 获得。
选择 web3 提供商
当我们加载一个网页时,我们需要连接到一个 web3 提供者。如果您已经安装了诸如 MetaMask 之类的提供程序,则可以使用正确的提供程序选项,如下所示:
App.web3Provider = web3.currentProvider;
在我们的众筹示例中,为了简单起见,我们将直接连接到我们的本地 Ganache 服务器,如下所示:
App.web3Provider = new Web3.providers.HttpProvider('http://localhost:8545');
正在加载帐户信息
为了加载帐户,我们定义了一个包含空内容的下拉菜单,如下所示:
<div class="form-group">
<label for="exampleFormControlSelect1">Accounts</label>
<select class="form-control" id="accts">
</select>
</div>
当我们加载页面时,我们将使用web3.eth.accounts
来获取所有 10 个默认帐户。请注意,第一个帐户的余额为 99.84;这是因为我们使用了第一个帐户作为所有者帐户来部署合同,并消耗了一些汽油作为交易费用,如以下代码所示:
web3.eth.accounts.forEach( function(e){
$('#accts').append($('<option>', {
value:e,
text : e + " (" +web3.fromWei(web3.eth.getBalance(e), "ether") + " ether)"
}));
})
帐户加载后,将显示如下:
正在加载项目信息
在众筹中,我们定义了一个包含筹款信息的项目结构,如下所示:
struct Project {
address addr;
string name;
string website;
uint totalRaised;
uint minimumToRaise;
uint currentBalance;
uint deadline;
uint completeAt;
Status status;
}
让我们在 HTML 中定义一些相关信息,例如:
<table class="table table-hover table-striped">
<tbody>
<tr>
<th scope="row">address</th>
<td><span class="text-info" id="address"></span
</td>
</tr>
<tr>
<th scope="row">name</th>
<td><span class="text-info" id="name"></span></td>
</tr>
<tr>
<th scope="row">website</th>
<td><span class="text-info" id="website"></span></td>
</tr>
<tr>
<th scope="row">totalRaised</th>
<td><span class="text-info" id="totalRaised"></span></td>
…
</tbody>
</table>
CrowdFunding.deployed()
函数将创建一个代表由CrowdFunding
管理的默认地址的CrowdFunding
实例。这里的代码向我们展示了如何显示项目信息:
App.contracts.CrowdFunding.deployed().then(function(instance) {
crowdFundingInstance = instance;
return crowdFundingInstance.project();
}).then(function(projectInfo) {
$("#address").text(projectInfo[0].toString());
$("#name").text(projectInfo[1]);
$("#website").text(projectInfo[2]);
$("#totalRaised").text(projectInfo[3].toString());
..
if(projectInfo[6].toString().length>0) {
var deadline = new
Date(Number(projectInfo[6].toString())*1000);
deadlineDate = moment(deadline).format("YYYY-MM-DD h:mm:ss");
$("#deadline").text(deadlineDate);
}
if(projectInfo[7].toString().length>0 && projectInfo[7].toString()!='0') {
console.log(projectInfo[7].toString());
var completeAt = new Date(Number(projectInfo[7].toString())*1000);
completeAtDate = moment(completeAt).format("YYYY-MM-DD h:mm:ss");
$("#completeAt").text(completeAtDate);
}
}).catch(function(error) {
..
});
结果将显示如下:
处理资金职能
要筹集资金,需要调用基金函数,这个函数在我们的众筹智能合约中有定义。在我们的网页中,我们使用 HTML 范围输入滑块组件来提供基金金额,如下所示:
<form id="fund-form" method="post" role="form" style="display: block;">
<div class="form-group row">
<div class="row">
<div class="col-lg-12">
<input type="range" name="ageInputName" id="ageInputId" value="0" min="1" max="100" oninput="ageOutputId.value = ageInputId.value">
<div style="display: inline;"><output name="ageOutputName" id="ageOutputId">0</output> <span>ether</span></div>
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-lg-12">
<button type="button" id="fundBtn" class="btn btn-primary pull-left">Submit</button>
</div>
</div>
</div>
</form>
Crowdfunding fund
功能是应付回退功能;因此,我们需要从 UI 中传递msg.sender
和msg.value
来调用它,如下所示。
function fund() public atStage(Status.Fundraising) payable {
contributions.push(
Contribution({
addr: msg.sender,
amount: msg.value
})
);
……
}
您可以如下定义发送地址和值参数:
handleFund: function(event) {
event.preventDefault();
var fundVal = $('#ageOutputId').val();
var selectAcct = $('#accts').find(":selected").val();
$("#displayMsg").html("");
App.contracts.CrowdFunding.deployed().then(function(instance) {
return instance.fund({ from: selectAcct, value:web3.toWei(fundVal, "ether"), gas:3500000});
}).then(function(result) {
App.loadProject();
}).catch(function(err) {
console.error(err);
$("#displayMsg").html(err);
});
},
一旦我们得到结果,我们将调用loadProject
函数来刷新项目信息。我们可以看到当前结余资金增加了,如下图所示:
达到检查目标
一旦达到筹资目标,众筹所有者将通过运行checkGoalReached
方法收集所有资金的。
HTML 只是一个简单的按钮,如下面的代码所示:
<button type="button" id="checkGoal" class="btn btn-success">CheckGoal</button>
类似于基金函数,我们使用以下代码在 JavaScript 中调用智能合约:
instance.checkGoalReached({ from: selectAcct, gas:3500000});
下面是详细的逻辑:
handleCheckGoal: function(event) {
event.preventDefault();
$("#displayMsg").html("");
var selectAcct = $('#accts').find(":selected").val();
App.contracts.CrowdFunding.deployed().then(function(instance) {
return instance.checkGoalReached({ from: selectAcct, gas:3500000});
}).then(function(result) {
App.loadProject();
}).catch(function(err) {
console.error(err);
$("#displayMsg").html(err);
});
},
结果将显示如下:
如果您遵循了整个示例并运行了这一步,那么恭喜您!你现在能够编写和运行一个众筹 DApp。
摘要
在这一章中,我们学习了 DApp 基础知识,现在我们了解了 web3.js API。通过运行 Ganache 作为我们的本地以太坊环境,我们可以使用 Truffle 开发工具来创建一个众筹项目并编写一个 DApp 组件。最后,我们部署并推出了众筹 DApp。在下一章中,我们将开始探索最流行的企业区块链——Hyperledger Fabric。