web3.js 入门
在前一章中,我们学习了如何使用 Solidity 编写和部署智能合约。在本章中,我们将了解 web3.js 以及如何导入它、连接到 geth 以及在 Node.js 或客户端 JavaScript 中使用它。我们还将学习如何使用 web3.js 为我们在前一章中创建的智能合约构建 web 客户端。
在本章中,我们将讨论以下主题:
- 在 Node.js 和客户端 JavaScript 中导入 web3.js
- 连接到 geth
- 探索 web3.js
- 发现 web3.js 最常用的 API
- 为所有权契约构建 Node.js 应用程序
本章与作者前一本书项目区块链中的章节相同。这不是再版的书,它是用来向读者解释基本概念的。
web3.js 简介
web3.js 为我们提供了与 geth 通信的 JavaScript APIs。它在内部使用 JSON-RPC 与 geth 通信。web3.js 还可以与任何其他类型的支持 JSON-RPC 的以太坊节点通信。它将所有 JSON-RPC API 公开为 JavaScript APIs。它不仅支持所有以太坊相关的 API,还支持耳语和虫群相关的 API。
随着我们构建各种项目,您将不断了解关于 web3.js 的更多信息。不过,现在,让我们来看看一些最常用的 web3.js API。稍后,我们将使用 web 3 . js 为我们的所有权智能合约构建一个前端。
在写作的时候,web3.js 的最新版本是 1.0.0-beta.18,我们会用这个版本来学习一切。
web3.js 托管在https://github.com/ethereum/web3.js,完整的文档托管在https://github.com/ethereum/wiki/wiki/JavaScript-API。
导入 web3.js
只需在项目目录中运行npm install web3
即可使用 Node.js 中的 web3.js,在源代码中,可以使用require("web3");
导入它。
要在客户端 JavaScript 中使用 web3.js,可以将web3.js
文件入队,该文件可以在项目源代码的dist
目录中找到。现在,web3
对象将在全球范围内可用。
连接到节点
web3.js 可以使用 HTTP 或 IPC 与节点通信,并允许我们连接多个节点。我们将使用 HTTP 进行节点通信。web3
的实例表示与节点的连接。该实例公开了 API。
当一个应用程序在 mist 中运行时,它会自动提供一个连接到 mist 节点的web3
实例。实例的变量名是web3
。
以下是连接到节点的基本代码:
if (typeof web3 !== 'undefined') {
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
}
首先,我们通过检查web3
是否为undefined
来验证代码是否在 mist 内部运行。如果定义了web3
,那么我们使用已经可用的实例;否则,我们通过连接到自定义节点来创建一个实例。如果您想要连接到自定义节点,可以从前面的代码中删除if
条件,不管该应用程序是否在 mist 中运行。这里,我们假设我们的定制节点在本地端口号8545
上运行。
Web3.providers
对象公开构造函数(在这个上下文中称为providers
)来建立连接并使用各种协议传输消息。Web3.providers.HttpProvider
让我们建立一个 HTTP 连接,而Web3.providers.IpcProvider
让我们建立一个 IPC 连接。
web3.currentProvider
属性被自动分配给当前的提供者实例。在创建了一个web3
实例之后,您可以使用web3.setProvider()
方法来更改它的提供者。它接受一个参数,即新提供程序的实例。
记住 geth 默认禁用 HTTP-RPC。所以在运行 geth 时通过传递--rpc
选项来启用它。默认情况下,HTTP-RPC 在端口8545
上运行。
web3
公开了一个isConnected()
方法,可以用来检查它是否连接到节点。它根据连接状态返回一个true
或false
值。
API 结构
web3
包含一个专门用于以太坊区块链交互的eth
对象(web3.eth
)和一个用于耳语交互的shh
对象(web3.shh
)。大多数 web 3 . js API 都在这两个对象中。
默认情况下,所有 API 都是同步的。对于异步请求,您可以传递一个可选的回调作为大多数函数的最后一个参数。所有回调都使用错误优先的回调方式。
一些 API 有异步请求的别名。例如,web3.eth.coinbase()
是同步的,而web3.eth.getCoinbase()
是异步的。
这里有一个例子:
//sync request
try
{
console.log(web3.eth.getBlock(48));
}
catch(e)
{
console.log(e);
}
//async request
web3.eth.getBlock(48, function(error, result){
if(!error)
console.log(result)
else
console.error(error);
})
getBlock
用于使用块的编号或散列来获取块的信息。或者,它可以采用一个字符串,如"earliest"
(创世纪区块)"latest"
(区块链的顶部区块),或"pending"
(正在开采的区块)。如果没有传递参数,那么默认为web3.eth.defaultBlock
,默认赋给"latest"
。
所有需要块标识作为输入的 API 都可以接受一个数字、散列或一个可读的字符串。如果没有传递值,这些 API 默认使用web3.eth.defaultBlock
。
BigNumber.js
JavaScript 天生就不擅长处理大数字。所以,对于需要你处理大数,需要完美计算的应用,使用bignome . js库。
web3.js 也依赖于 BigNumber.js 并自动添加。web3.js 总是返回数字值的BigNumber
对象。它可以接受 JavaScript 数字、数字字符串和BigNumber
实例作为输入。
让我们演示一下,如下所示:
web3.eth.getBalance("0x27E829fB34d14f3384646F938165dfcD30cFfB7c")
.toString();
这里,我们使用web3.eth.getBalance()
方法来获得一个地址的余额。这个方法返回一个BigNumber
对象。我们需要在一个BigNumber
对象上调用toString()
,将它转换成一个数字字符串。
BigNumber.js 无法正确处理超过 20 个浮点数字的数字。因此,建议您以卫单位存储余额,并在显示时将其转换为其他单位。web3.js 本身总是回归,在魏身上取平衡。例如,getBalance()
方法返回卫单位的地址余额。
单位换算
web3.js 提供 API 将 wei 余额转换成任何其他单位,反之亦然。
web3.fromWei()
方法将魏数转换成另一个单位,而web3.toWei()
方法将任何其他单位的数转换成魏。这里有一个例子来说明这一点:
web3.fromWei("1000000000000000000", "ether");
web3.toWei("0.000000000000000001", "ether");
在第一行中,我们将 wei 转换为ether
;在第二行中,我们将ether
转换为 wei。两种方法中的第二个参数可以是以下字符串之一:
kwei
或ada
mwei
或babbage
gwei
或shannon
szabo
finney
ether
kether
/grand
/einstein
mether
gether
tether
检索天然气价格、余额和交易详细信息
让我们看一下检索汽油价格、地址余额和挖掘交易信息的 API:
//It's sync. For async use getGasPrice
console.log(web3.eth.gasPrice.toString());
console.log(web3.eth.getBalance("0x407d73d8a49eeb85d32cf465507dd71d5071
00c1", 45).toString());
console.log(web3.eth.getTransactionReceipt("0x9fc76417374aa880d4449a1f7
f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"));
输出将采用以下格式:
20000000000
30000000000
{
"transactionHash": "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ",
"transactionIndex": 0,
"blockHash": "0xef95f2f1ed3ca60b048b4bf67cde2195961e0bba6f70bcbea9a2c4e133e34b46",
"blockNumber": 3,
"contractAddress": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"cumulativeGasUsed": 314159,
"gasUsed": 30234
}
以下是上述方法的工作原理:
web3.eth.gasPrice()
:根据 x 最新区块气价中值确定气价。web3.eth.getBalance()
:返回任意给定地址的余额。所有的散列应该以十六进制字符串(不是十六进制文字)的形式提供给 web 3 . js API。可靠性地址类型的输入也应该是十六进制字符串。web3.eth.getTransactionReceipt()
:这是用来获得一个交易的详细信息,使用它的散列。如果在区块链中找到交易,则它返回交易收据对象;否则返回null
。交易收据对象包含以下属性:blockHash
:该事务所在的块的哈希。blockNumber
:该交易所在的块号。transactionHash
:事务的哈希。transactionIndex
:块中事务索引位置的整数。from
:寄件人地址。to
:收款人的地址;当它是一个合同创建事务时,它被保留为null
。cumulativeGasUsed
:在区块中执行该交易时使用的气体总量。gasUsed
:单笔该笔具体交易的用气量。contractAddress
:如果交易是合同创建,则创建合同地址。否则,这就剩下null
了。logs
:该事务生成的日志对象数组。
发送以太网
让我们看看如何将ether
发送到任意地址。要发送ether
,你需要使用web3.eth.sendTransaction()
方法。该方法可用于发送任何类型的事务,但主要用于发送ether
。这是因为使用这种方法部署契约或调用契约的方法很麻烦,因为它需要您手动生成事务的数据,而不是自动生成。它采用具有以下属性的事务对象:
from
:发送账户的地址。如果没有指定,这将使用web3.eth.defaultAccount
属性。- 这是可选的。它是消息的目的地址,对于契约创建事务来说是未定义的。
- 这是可选的。如果是合同创建交易,则交易的价值连同捐赠一起在 wei 中转移。
- 这是可选的。这是交易中使用的汽油量(未使用的汽油将被退还)。如果没有提供,则会自动确定。
- 这是可选的。是本次交易在卫的气价,默认为平均网气价。
- 这是可选的。它或者是一个包含消息相关数据的字节字符串,或者在契约创建事务的情况下,是初始化代码。
- 这是可选的。它是一个整数。每个交易都有与之相关联的
nonce
。nonce
是一个计数器,指示发送方进行的交易次数。如果没有提供,将自动确定。它有助于防止重放攻击。这个nonce
不是与块相关联的nonce
。如果我们使用的nonce
大于事务应有的nonce
,那么该事务将被放入队列,直到其他事务到达。例如,如果下一个事务的nonce
应该是 4,我们将nonce
设置为 10,那么 geth 将在广播这个事务之前等待剩余的 6 个事务。具有nonce
ten 的事务被称为排队事务,并且不是未决事务。
以下是如何将ether
发送到一个地址的示例:
var txnHash = web3.eth.sendTransaction({
from: web3.eth.accounts[0],
to: web3.eth.accounts[1],
value: web3.toWei("1", "ether")
});
这里,我们从账号0
向账号1
发送一个ether
。我们需要确保在运行 geth 时使用unlock
选项来解锁两个帐户。geth 交互控制台提示输入密码,但是如果帐户被锁定,交互控制台之外的 web3.js API 将抛出一个错误。此方法返回事务的事务哈希。然后,您可以使用getTransactionReceipt()
方法检查事务是否被挖掘。
您还可以使用web3.personal.listAccounts()
、web3.personal.unlockAccount(addr, pwd)
和web3.personal.newAccount(pwd)
API 在运行时管理帐户。
使用合同
让我们学习如何部署一个新的契约,使用它的地址获得一个对已部署契约的引用,发送ether
到一个契约,发送一个事务来调用一个contract
方法,以及估计一个方法调用的 gas。
为了部署一个新的契约或者获取对一个已经部署的契约的引用,您需要首先使用web3.eth.contract()
方法创建一个contract
对象。它将合同 ABI 作为参数,并返回contract
对象。
下面是创建一个contract
对象的代码:
var proofContract = web3.eth.contract([{"constant":false,"inputs":
[{"name":"fileHash","type":"string"}],"name":"get","outputs":
[{"name":"timestamp","type":"uint256"},
{"name":"owner","type":"string"}],"payable":false,"type":"function"},
{"constant":false,"inputs":[{"name":"owner","type":"string"},
{"name":"fileHash","type":"string"}],"name":"set","outputs":
[],"payable":false,"type":"function"},{"anonymous":false,"inputs":
[{"indexed":false,"name":"status","type":"bool"},
{"indexed":false,"name":"timestamp","type":"uint256"},
{"indexed":false,"name":"owner","type":"string"},
{"indexed":false,"name":"fileHash","type":"string"}],"name"
:"logFileAddedStatus","type":"event"}]);
一旦有了契约,就可以使用contract
对象的new
方法来部署它,或者使用at
方法来获取一个已经部署的、与 ABI 匹配的契约的引用。
让我们来看一个如何部署新合同的示例,如下所示:
var proof = proofContract.new({
from: web3.eth.accounts[0],
data: "0x606060405261068...",
gas: "4700000"
},
function(e, contract) {
if (e) {
console.log("Error " + e);
} else if (contract.address != undefined) {
console.log("Contract Address: " + contract.address);
} else {
console.log("Txn Hash: " + contract.transactionHash)
}
})
这里,new
方法被异步调用,因此如果事务被成功创建和广播,回调将被触发两次。第一种情况,在事务播出后调用,第二种情况,在事务挖掘后调用。如果不提供回调,那么proof
变量的address
属性将被设置为undefined
。一旦contract
被开采,address
属性将被设置。
在proof
契约中,没有构造函数,但是如果有,那么构造函数的参数应该放在new
方法的开始。我们传递的对象包含了from
地址、合同的字节码和要使用的最大值gas
。要创建事务,这三个属性必须存在。这个对象可以拥有传递给sendTransaction()
方法的对象中存在的属性,但是在这里,data
是契约字节码,to
属性被忽略。
您可以使用at
方法来获取对已经部署的契约的引用。下面是演示这一点的代码:
var proof =
proofContract.at("0xd45e541ca2622386cd820d1d3be74a86531c14a1");
现在让我们看一下发送一个事务来调用一个契约的方法。这里有一个例子来说明这一点:
proof.set.sendTransaction("Owner Name",
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", {
from: web3.eth.accounts[0],
}, function(error, transactionHash){
if (!err)
console.log(transactionHash);
})
这里,我们将对象的sendTransaction
方法称为同名方法。传递给这个sendTransaction
方法的对象具有与web3.eth.sendTransaction()
相同的属性,除了data
和to
属性被忽略。
如果您想在节点本身上调用一个方法,而不是创建一个事务并广播它,那么您可以使用call
而不是sendTransaction
。方法如下:
var returnValue = proof.get.call
("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
有时,有必要找出调用一个方法所需的气体量,以便您可以决定是否调用它。为此,您可以使用web3.eth.estimateGas
。但是使用web3.eth.estimateGas()
需要你直接生成交易的数据;因此,我们可以使用合同对象的estimateGas()
方法。这里有一个例子来说明这一点:
var estimatedGas = proof.get.estimateGas
("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
要在不调用任何方法的情况下向契约发送一些ether
,可以简单地使用web3.eth.sendTransaction
方法。
检索和监听合同事件
监视事件非常重要,因为事务调用方法的结果通常是通过触发事件返回的。
在我们开始如何检索和观察事件之前,我们需要了解事件的索引参数。一个事件最多可以有三个参数具有indexed
属性。此属性用于通知节点对其进行索引,以便应用程序客户端可以搜索具有匹配返回值的事件。如果不使用indexed
属性,那么它必须从节点中检索所有事件,并过滤出需要的事件。例如,您可以这样写logFileAddedStatus
事件:
event logFileAddedStatus(bool indexed status, uint indexed timestamp,
string owner, string indexed fileHash);
以下是演示如何监听合同事件的示例:
var event = proof.logFileAddedStatus(null, {
fromBlock: 0,
toBlock: "latest"
});
event.get(function(error, result) {
if (!error) {
console.log(result);
} else {
console.log(error);
}
})
event.watch(function(error, result) {
if (!error) {
console.log(result.args.status);
} else {
console.log(error);
}
})
setTimeout(function() {
event.stopWatching();
}, 60000)
var events = proof.allEvents({
fromBlock: 0,
toBlock: "latest"
});
events.get(function(error, result) {
if (!error) {
console.log(result);
} else {
console.log(error);
}
})
events.watch(function(error, result) {
if (!error) {
console.log(result.args.status);
} else {
console.log(error);
}
})
setTimeout(function() {
events.stopWatching();
}, 60000)
下面是上述代码的工作原理:
- 首先,我们通过在一个契约实例上调用同名事件的方法来获取
event
对象。此方法将两个对象作为参数,用于筛选事件:- 第一个对象用于根据索引返回值过滤事件,例如,
{'valueA': 1, 'valueB': [myFirstAddress, mySecondAddress]}
。默认情况下,所有过滤器值都设置为null
。这意味着它们将匹配从此协定发送的给定类型的任何事件。 - 下一个对象可以包含三个属性:
fromBlock
("earliest"
块;默认是"latest"
);toBlock
("latest"
屏蔽;默认为"latest"
);和address
(仅从中获取日志的地址列表;默认为合同地址)。
- 第一个对象用于根据索引返回值过滤事件,例如,
event
对象公开了三种方法:get
、watch
和stopWatching
。get
用于获取 block 范围内的所有事件。watch
类似于get
,但是它在获取事件后观察变化。stopWatching
可用于停止观察变化。- 然后,我们有了契约实例的
allEvents
方法。它用于检索合同的所有事件。
每个事件都由包含以下属性的对象表示:
args
:带有事件参数的对象。event
:表示事件名称的字符串。logIndex
:表示块中日志索引位置的整数。transactionIndex
:整数,表示创建索引位置日志的交易。transactionHash
:一个字符串,表示创建此日志的事务的哈希。-
address
:表示该日志来源地址的字符串。 -
blockHash
:表示该日志所在块的哈希的字符串。当它挂起时,它被保留为null
。 blockNumber
:该日志所在的块号。当它挂起时,它被输入为null
。
web3.js 提供了一个web3.eth.filter
API 来检索和观察事件。您可以使用这个 API,但是在早期的方法中处理事件的方式要简单得多。你可以在https://github . com/ether eum/wiki/wiki/JavaScript-API # web 3 eth filter了解更多信息。
为所有权合同构建客户端
在前一章中,我们为所有权契约编写了可靠性代码。在前一章和这一章中,我们学习了 web3.js 以及如何使用 web3.js 调用契约的方法。现在,是时候为我们的智能契约构建一个客户端了,以便用户可以轻松地使用它。
我们将构建一个客户端,企业用户在其中选择一个文件,输入所有者的详细信息,然后单击Submit
来广播一个事务,用文件散列和所有者的详细信息调用契约的set
方法。一旦交易成功广播,我们将显示交易散列。用户还可以选择一个文件,并从智能合同中获取所有者的详细信息。客户端还会实时显示最近挖掘出的set
交易。
我们将使用 sha1.js 在前端获取文件的散列,使用 jQuery 进行 DOM 操作,使用 Bootstrap 4 创建响应性布局。我们将在后端使用 Express.js 和 web3.js。我们将使用socket.io
以便后端将最近挖掘的事务推送到前端,而无需前端定期请求数据。
项目结构
在本章的练习文件中,您会发现两个目录:Final
和Initial
。Final
包含项目的最终源代码,而Initial
包含空的源代码文件和库,允许您快速开始构建应用程序。
为了测试Final
目录,您需要在其中运行npm install
,并用部署契约后获得的契约地址替换app.js
中的硬编码契约地址。然后,使用Final
目录中的node app.js
命令运行应用程序。
在Initial
目录中,你会发现一个public
目录和两个名为app.js
和package.json
的文件。package.json
包含我们应用的后端依赖项,而app.js
是你放置后端源代码的地方。
public
目录包含与前端相关的文件。在public/css
里面,你会发现bootstrap.min.css
,这是引导库;在public/html
里面,你会找到index.html
,在那里你会放置我们 app 的 HTML 代码;在public/js
目录中,你会找到 jQuery、sha1 和 socket.io 的 JS 文件,在public/js
中,你还会找到一个main.js
文件,在那里你会放置我们 app 的前端 JS 代码。
构建后端
首先,在Initial
目录中运行npm install
,为我们的后端安装所需的依赖项。在我们开始编写后端代码之前,确保 geth 运行时启用了rpc
。最后,确保帐户0
存在并且已解锁。
在开始编码之前,您需要做的最后一件事是使用我们在前一章看到的代码部署所有权契约,并复制契约地址。
现在让我们创建一个单独的服务器,它将为浏览器提供 HTML 并接受socket.io
连接:
var express = require("express");
var app = express();
var server = require("http").createServer(app);
var io = require("socket.io")(server);
server.listen(8080);
这里,我们将express
和socket.io
服务器集成到一个运行在端口8080
上的服务器中。
现在让我们创建路由来服务静态文件和应用程序的主页。下面是执行此操作的代码:
app.use(express.static("public"));
app.get("/", function(req, res){
res.sendFile(__dirname + "/public/html/index.html");
})
这里,我们使用express.static
中间件来服务静态文件。我们要求它在public
目录中查找静态文件。
现在,让我们连接到 geth 节点,并获取对已部署契约的引用,以便我们可以发送事务并监视事件。下面是执行此操作的代码:
var Web3 = require("web3");
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
var proofContract = web3.eth.contract([{"constant":false,"inputs":[{"name":"fileHash","type":"string"}],"name":"get","outputs":[{"name":"timestamp","type":"uint256"},{"name":"owner","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"owner","type":"string"},{"name":"fileHash","type":"string"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"status","type":"bool"},{"indexed":false,"name":"timestamp","type":"uint256"},{"indexed":false,"name":"owner","type":"string"},{"indexed":false,"name":"fileHash","type":"string"}],"name":"logFileAddedStatus","type":"event"}]);
var proof =
proofContract.at("0xf7f02f65d5cd874d180c3575cb8813a9e7736066");
代码是不言自明的。只需用您得到的地址替换合同地址。
现在让我们创建路由来广播事务并获取有关文件的信息。下面是执行此操作的代码:
app.get("/submit", function(req, res){
var fileHash = req.query.hash;
var owner = req.query.owner;
var pkeys = req.query.pkeys;
pkeys = pkeys.split(",")
proof.set.sendTransaction(owner, fileHash, {
from: web3.eth.accounts[0],
privateFor: pkeys
}, function(error, transactionHash){
if (!error)
{
res.send(transactionHash);
}
else
{
res.send("Error");
}
})
})
app.get("/getInfo", function(req, res) {
var fileHash = req.query.hash;
var details = proof.get.call(fileHash);
res.send(details);
})
这里,/submit
路由用于创建和广播事务。一旦我们得到了事务散列,我们就把它发送给客户机。我们没有做任何事情来等待事务挖掘。/getInfo
路由调用节点本身的契约的get
方法,而不是创建一个事务。它只是发送回它得到的任何响应。
现在让我们观察合同中的事件,并将它们广播给所有客户端。下面是执行此操作的代码:
proof.logFileAddedStatus().watch(function(error, result) {
if (!error) {
if (result.args.status == true) {
io.send(result);
}
}
})
在这里,我们检查status
是否是true
,只有当它是true
时,我们才向所有连接的socket.io
客户端广播该事件。
构建前端
让我们从应用程序的 HTML 开始。将此代码放入index.html
文件,如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-
scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3 text-xs-center">
<br>
<h3>Upload any file</h3>
<br>
<div>
<div class="form-group">
<label class="custom-file text-xs-left">
<input type="file" id="file"
class="custom-file-input">
<span class="custom-file-control">
</span>
</label>
</div>
<div class="form-group">
<label for="owner">Enter owner name</label>
<input type="text" class="form-control"
id="owner">
</div>
<div class="form-group">
<label for="owner">Enter Public Keys
<small>(comma Seperated)</small></label>
<input type="text" class="form-control"
id="pkeys">
</div>
<button onclick="submit()" class="btn btn-
primary">Submit</button>
<button onclick="getInfo()" class="btn btn-
primary">Get Info</button>
<br><br>
<div class="alert alert-info" role="alert"
id="message">
You can either submit the file's details or
get information about it.
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 offset-md-3 text-xs-center">
<br>
<h3>Live Transactions Mined</h3>
<br>
<ol id="events_list">No Transaction Found</ol>
</div>
</div>
</div>
<script type="text/javascript" src="/js/sha1.min.js"></script>
<script type="text/javascript" src="/js/jquery.min.js">
</script>
<script type="text/javascript" src="/js/socket.io.min.js">
</script>
<script type="text/javascript" src="/js/main.js"></script>
</body>
</html>
下面是代码的工作原理:
- 首先,我们显示 Bootstrap 的文件输入字段,以便用户可以选择一个文件。
- 然后,我们显示一个文本字段,用户可以在其中输入所有者的详细信息。
- 然后我们有两个按钮。第一个按钮是在契约中存储文件散列和所有者的详细信息,第二个按钮是从契约中获取文件的信息。点击
Submit
按钮触发submit()
方法,点击Get Info
按钮触发getInfo()
方法。 - 接下来,我们有一个警告框来显示消息。
- 最后,我们显示一个有序列表,显示当用户在页面上时挖掘的合同事务。
现在,让我们编写getInfo()
和submit()
方法的实现,建立与服务器的socket.io
连接,并监听来自服务器的socket.io
消息。将此代码放在main.js
文件中:
function submit()
{
var file = document.getElementById("file").files[0];
if(file)
{
var owner = document.getElementById("owner").value;
if(owner == "")
{
alert("Please enter owner name");
}
else
{
var publicKeys = document.getElementById("pkeys").value;
if(publicKeys == "")
{
alert("Please enter the other enterprise's public keys");
}
else
{
var reader = new FileReader();
reader.onload = function (event) {
var hash = sha1(event.target.result);
$.get("/submit?hash=" + hash + "&owner=" + owner +
"&pkeys=" + encodeURIComponent(publicKeys), function(data){
if(data == "Error")
{
$("#message").text("An error occured.");
}
else
{
$("#message").html("Transaction hash: " + data);
}
});
};
reader.readAsArrayBuffer(file);
}
}
}
else
{
alert("Please select a file");
}
}
function getInfo()
{
var file = document.getElementById("file").files[0];
if(file)
{
var reader = new FileReader();
reader.onload = function (event) {
var hash = sha1(event.target.result);
$.get("/getInfo?hash=" + hash, function(data){
if(data[0] == 0 && data[1] == "")
{
$("#message").html("File not found");
}
else
{
$("#message").html("Timestamp: " + data[0] + " Owner: " +
data[1]);
}
});
};
reader.readAsArrayBuffer(file);
}
else
{
alert("Please select a file");
}
}
var socket = io("http://localhost:8080");
socket.on("connect", function () {
socket.on("message", function (msg) {
if($("#events_list").text() == "No Transaction Found")
{
$("#events_list").html("<li>Txn Hash: " + msg.transactionHash +
"\nOwner: " + msg.args.owner + "\nFile Hash: " +
msg.args.fileHash + "</li>");
}
else
{
$("#events_list").prepend("<li>Txn Hash: " + msg.transactionHash
+ "\nOwner: " + msg.args.owner + "\nFile Hash: " +
msg.args.fileHash + "</li>");
}
});
});
前面的代码是这样工作的:
- 首先,我们定义
submit()
方法。在submit()
方法中,我们确保选择了一个文件,并且文本字段不为空。然后,我们将文件的内容作为数组缓冲区读取,并将数组缓冲区传递给 sha1.js 公开的sha1()
方法,以便获得数组缓冲区内内容的散列。一旦我们有了散列,我们使用 jQuery 向/submit
路由发出一个 AJAX 请求,然后我们在警告框中显示事务散列。 - 接下来我们定义
getInfo()
方法。它首先确保选择了一个文件。然后,它生成一个类似于之前生成的散列,并向/getInfo
端点发出请求以获取关于该文件的信息。 - 最后,我们使用由
socket.io
库公开的io()
方法建立一个socket.io
连接。然后,我们等待 connect 事件触发,这表明连接已经建立。连接建立后,我们监听来自服务器的消息,并向用户显示事务的细节。
我们不会把文件存放在区块链以太坊。存储文件是非常昂贵的,因为它需要大量的气体。在我们的例子中,我们不需要存储文件,因为网络中的节点将能够看到该文件;因此,如果用户想要对文件内容保密,那么他们将无法做到。我们的应用程序的目的只是证明文件的所有权,而不是像云服务那样存储和提供文件。
测试客户端
现在运行app.js
节点来运行应用服务器。打开你最喜欢的浏览器,访问http://localhost:8080/
。您将在浏览器中看到以下输出:
现在选择一个文件,输入所有者的名字,然后点击提交。浏览器窗口将变成这样:
在下图中,您可以看到显示了事务哈希。现在等到交易被挖掘出来。一旦交易被挖掘,您将能够在实时交易列表中看到该交易。浏览器窗口应该是这样的:
现在再次选择同一个文件,然后点击“获取信息”按钮。您将看到以下输出:
在前面的截图中,您可以看到时间戳和所有者的详细信息。现在我们已经完成了第一个 DApp 的客户端构建。
摘要
在本章中,我们首先学习了 web3.js 的基础知识,并看了一些例子。我们学习了连接到节点、基本 API、发送各种事务以及监视事件。最后,我们为我们的所有权契约构建了一个合适的生产使用客户机。现在,您应该能够轻松地编写智能合约并为它们构建 UI 客户端,以便于使用。
在下一章,我们将学习如何使用零知识安全层实现隐私保护。