跳转至

web3.js 入门

在本章中,我们将学习 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 也就是说,它不仅仅支持所有以太坊相关的 APIs 它还支持与 Whisper 和 Swarm 相关的 API。

随着我们构建各种项目,您将会对 web3.js 了解得越来越多,但是现在,让我们先来看看一些最常用的 web3.js API,然后我们将使用 web 3 . js 为我们的所有权智能合约构建一个前端。

在撰写本文时,web3.js 的最新版本是 0.16.0。我们将学习关于这个版本的一切。

web3.js 托管在https://github.com/ethereum/web3.js,完整的文档托管在https://github.com/ethereum/wiki/wiki/JavaScript-API

导入 web3.js

要在 Node.js 中使用 web3.js,只需在项目目录中运行npm install web3,在源代码中,可以使用require("web3");将其导入。

要在客户端 JavaScript 中使用 web3.js,可以将web3.js文件入队,该文件可以在项目源代码的dist目录中找到。现在你将拥有全球可用的Web3对象。

连接到节点

web3.js 可以使用 HTTP 或 IPC 与节点通信。我们将使用 HTTP 来建立与节点的通信。web3.js 允许我们与多个节点建立连接。web3的实例表示与节点的连接。该实例公开了 API。

当一个应用程序在 mist 中运行时,它会自动提供一个连接到 Mist 节点的web3实例。实例的变量名是web3

以下是连接到节点的基本代码:

if (typeof web3 !== 'undefined') { 
  web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); 
} 

首先,我们在这里通过检查web3是否为undefined来检查代码是否在 mist 内部运行。如果定义了web3,那么我们使用已经可用的实例;否则,我们通过连接到自定义节点来创建一个实例。如果您想连接到自定义节点,而不管应用程序是否在 mist 中运行,那么从前面的代码中删除if条件。这里,我们假设我们的定制节点在本地端口号8545上运行。

Web3.providers对象公开构造函数(在这个上下文中称为提供者)来建立连接,并使用各种协议传输消息。Web3.providers.HttpProvider让我们建立一个 HTTP 连接,而Web3.providers.IpcProvider让我们建立一个 IPC 连接。

web3.currentProvider属性被自动分配给当前的提供者实例。创建 web3 实例后,您可以使用web3.setProvider()方法更改它的提供者。它接受一个参数,即新提供程序的实例。

记住 geth 默认禁用 HTTP-RPC。所以在运行 geth 时通过传递--rpc选项来启用它。默认情况下,HTTP-RPC 在端口 8545 上运行。

web3公开了一个isConnected()方法,可以用来检查它是否连接到节点。根据连接状态返回truefalse

API 结构

web3包含一个专门用于以太坊区块链交互的eth对象(web3.eth)和一个用于耳语交互的shh对象(web3.shh)。web3.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 天生就不擅长正确处理大数字。因此,需要你处理大数,需要完美计算的应用程序使用BigNumber.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()方法返回地址在 wei 单元中的余额。

单位换算

web3.js 提供了将 wei 余额转换成任何其他单位以及将任何其他单位余额转换成 wei 的 API。

web3.fromWei()方法用于将魏数转换成任何其他单位,而web3.toWei()方法用于将任何其他单位的数转换成魏。这里有一个例子来说明这一点:

web3.fromWei("1000000000000000000", "ether"); 
web3.toWei("0.000000000000000001", "ether"); 

在第一行,我们把卫转换成以太,在第二行,我们把以太转换成卫。两种方法中的第二个参数可以是以下字符串之一:

  • 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("0x407d73d8a49eeb85d32cf465507dd71d507100c1", 45).toString()); 

console.log(web3.eth.getTransactionReceipt("0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b")); 

输出将采用以下形式:

20000000000 
30000000000 
{ 
  "transactionHash": "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ", 
  "transactionIndex": 0, 
  "blockHash": "0xef95f2f1ed3ca60b048b4bf67cde2195961e0bba6f70bcbea9a2c4e133e34b46", 
  "blockNumber": 3, 
  "contractAddress": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", 
  "cumulativeGasUsed": 314159, 
  "gasUsed": 30234 
} 

以下是上述方法的工作原理:

  • web3.eth.gasPrice():根据 x 个最新区块的气价中值确定气价。
  • web3.ethgetBalance():返回任意给定地址的余额。所有的散列应该以十六进制字符串的形式提供给 web 3 . js API,而不是十六进制文字。solidity address类型的输入也应该是十六进制字符串。
  • web3.eth.getTransactionReceipt():这是用来获得一个交易的详细信息,使用它的散列。如果在区块链中找到交易,则它返回交易收据对象;否则,它返回 null。交易收据对象包含以下属性:
    • blockHash:该事务所在的块的散列
    • blockNumber:该交易所在的块号
    • transactionHash:交易的哈希
    • transactionIndex:块中事务索引位置的整数
    • from:发件人的地址
    • to:收款人的地址;null当是合同创建交易时
    • cumulativeGasUsed:在区块中执行该交易时使用的气体总量
    • gasUsed:该笔具体交易单独使用的气体量
    • contractAddress:如果交易是合同创建,则创建的合同地址;否则为空
    • logs:该事务生成的日志对象数组

发送以太网

让我们看看如何将以太发送到任何地址。要发送以太,需要使用web3.eth.sendTransaction()方法。此方法可用于发送任何类型的事务,但主要用于发送以太网,因为使用此方法部署协定或调用协定的方法很麻烦,因为它需要您生成事务的数据,而不是自动生成。它采用具有以下属性的事务对象:

  • from:发送账户的地址。如果未指定,则使用web3.eth.defaultAccount属性。
  • 这是可选的。它是消息的目的地址,对于契约创建事务来说是未定义的。
  • 这是可选的。如果是合同创建交易,则为 wei 中的交易以及捐赠转移价值。
  • 这是可选的。这是交易中使用的汽油量(未使用的汽油将被退还)。如果没有提供,那么它是自动确定的。
  • 这是可选的。是本次交易在卫的气价,默认为平均网气价。
  • 这是可选的。它或者是一个包含消息相关数据的字节字符串,或者在契约创建事务的情况下,是初始化代码。
  • 这是可选的。它是一个整数。每个事务都有一个与之相关联的随机数。nonce 是指示由事务的发送者发送的事务的数量的计数器。如果没有提供,则自动确定。它有助于防止重放攻击。该随机数不是与块相关联的随机数。如果我们使用的 nonce 大于事务应该拥有的 nonce,那么该事务将被放入队列中,直到其他事务到达。例如,如果下一个事务 nonce 应该是 4,如果我们将 nonce 设置为 10,那么 geth 将在广播这个事务之前等待中间的六个事务。nonce 为 10 的事务称为排队事务,它不是挂起事务。

让我们看一个如何将以太网发送到一个地址的示例:

var txnHash = web3.eth.sendTransaction({ 
  from: web3.eth.accounts[0], 
  to: web3.eth.accounts[1], 
  value: web3.toWei("1", "ether") 
}); 

这里,我们从账号 0 向账号 1 发送一个以太。运行 geth 时,确保使用unlock选项解锁两个帐户。在 geth 交互控制台中,它提示输入密码,但是如果帐户被锁定,交互控制台之外的 web3.js API 将抛出一个错误。此方法返回事务的事务哈希。然后,您可以使用getTransactionReceipt()方法检查事务是否被挖掘。

您还可以使用web3.personal.listAccounts()web3.personal.unlockAccount(addr, pwd)web3.personal.newAccount(pwd)API 在运行时管理帐户。

使用合同

让我们学习如何部署一个新的契约,使用它的地址获取对已部署契约的引用,向一个契约发送以太网,发送一个事务来调用一个契约方法,以及估计方法调用的 gas。

为了部署一个新的契约或者获取对一个已经部署的契约的引用,您需要首先使用web3.eth.contract()方法创建一个契约对象。它将合同 ABI 作为参数,并返回合同对象。

下面是创建契约对象的代码:

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"}]); 

一旦有了契约,就可以使用契约对象的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。一旦契约被开采,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()相同的属性,除了datato属性被忽略。

如果您想在节点本身上调用一个方法,而不是创建一个事务并广播它,那么您可以使用call而不是sendTransaction。这里有一个例子来说明这一点:

var returnValue = proof.get.call("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); 

有时,有必要找出调用一个方法所需的气体,以便您可以决定是否调用它。web3.eth.estimateGas可用于此目的。但是直接使用web3.eth.estimateGas()需要你生成交易的数据;因此,我们可以使用与方法同名的对象的estimateGas()方法。这里有一个例子来说明这一点:

var estimatedGas = proof.get.estimateGas("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); 

如果你想在不调用任何方法的情况下发送一些以太到一个契约,那么你可以简单地使用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)

前面的代码是这样工作的:

  1. 首先,我们通过在一个契约实例上调用事件同名的方法来获取事件对象。此方法将两个对象作为参数,用于筛选事件:

    • 第一个对象用于根据索引返回值过滤事件:例如,{'valueA': 1, 'valueB': [myFirstAddress, mySecondAddress]}。默认情况下,所有滤波器值都设置为null。这意味着它们将匹配从此协定发送的给定类型的任何事件。
    • 下一个对象可以包含三个属性:fromBlock(最早的块;默认是"latest"),toBlock(最新区块;默认情况下,它是"latest",和address(一个地址列表,只从中获取日志;默认为合同地址)。
  2. event对象公开了三种方法:getwatchstopWatchingget用于获取 block 范围内的所有事件。watch类似于get,但是它在获取事件后观察变化。而stopWatching可以用来停止观察变化。

  3. 然后,我们有了契约实例的allEvents方法。它用于检索合同的所有事件。
  4. 每个事件都由包含以下属性的对象表示:
    • 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了解更多信息。

为所有权合同构建客户端

现在,是时候为我们的智能契约构建一个客户端了,这样用户就可以轻松地使用它。

我们将构建一个客户端,用户选择一个文件并输入所有者的详细信息,然后单击 Submit 来广播一个事务,用文件散列和所有者的详细信息调用契约的set方法。一旦交易成功广播,我们将显示交易散列。用户还可以选择一个文件,并从智能合同中获取所有者的详细信息。客户端还会实时显示最近挖掘的set交易。

我们将使用 sha1.js 在前端获取文件的散列,使用 jQuery 进行 DOM 操作,使用 Bootstrap 4 创建响应性布局。我们将在后端使用 express.js 和 web3.js。我们将使用 socket.io,以便后端将最近挖掘的事务推送到前端,而前端不会在每个相等的时间间隔后请求数据。

web3.js 可以用在前端。但是对于这个应用来说,会有安全风险;也就是说,我们使用存储在 geth 中的帐户,并将 geth 节点 URL 暴露给前端,这将使存储在这些帐户中的 ether 面临风险。

项目结构

在本章的练习文件中,您会发现两个目录:FinalInitial. Final包含项目的最终源代码,而Initial包含空的源代码文件和库,以便快速开始构建应用程序。

为了测试Final目录,您需要在其中运行npm install,并用部署契约后获得的契约地址替换app.js中的硬编码契约地址。然后,使用Final目录中的node app.js命令运行应用程序。

Initial目录中,你会发现一个public目录和两个名为app.jspackage.json的文件。package.json包含我们应用的后端依赖项,而app.js是你放置后端源代码的地方。

public目录包含与前端相关的文件。在public/css里面,你会发现bootstrap.min.css,这是引导库;在public/html里面,你会找到index.html,在那里你会放置我们应用的 HTML 代码;在public/js目录中,您将找到 jQuery、sha1 和 socket.io 的 JS 文件。在public/js中,您还将找到一个main.js文件,您将在其中放置我们应用程序的前端 JS 代码。

构建后端

我们先来搭建 app 的后端。首先,在Initial目录中运行npm install,为我们的后端安装所需的依赖项。在我们开始编写后端代码之前,请确保 geth 运行时启用了 rpc。如果您在专用网络上运行 geth,那么请确保还启用了挖掘。最后,确保帐户 0 存在并且已解锁。您可以在启用了 rpc 和挖掘的专用网络上运行 geth,并解锁帐户 0:

geth --dev --mine --rpc --unlock=0

在开始编码之前,您需要做的最后一件事是使用我们在第 4 章中看到的代码部署所有权契约,并复制契约地址。

现在让我们创建一个单独的服务器,它将为浏览器提供 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); 

这里,我们将expresssocket.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; 
proof.set.sendTransaction(owner, fileHash, { 
from: web3.eth.accounts[0], 
}, 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); 
  } 
} 
}) 

在这里,我们检查状态是否为真,如果为真,我们才把事件广播给所有连接的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> 
                        <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 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> 

下面是代码的工作原理:

  1. 首先,我们显示 Bootstrap 的文件输入字段,以便用户可以选择一个文件。
  2. 然后,我们显示一个文本字段,用户可以在其中输入所有者的详细信息。
  3. 然后,我们有两个按钮。第一个按钮是在契约中存储文件散列和所有者的详细信息,第二个按钮是从契约中获取文件的信息。点击提交按钮触发submit()方法,而点击获取信息按钮触发getInfo()方法。

  4. 然后,我们有一个警告框来显示消息。

  5. 最后,我们显示一个有序列表,以显示当用户在页面上时挖掘的合同的事务。

现在让我们编写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 reader = new FileReader(); 
  reader.onload = function (event) { 
  var hash = sha1(event.target.result); 
  $.get("/submit?hash=" + hash + "&owner=" + owner, 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>"); 
} 
  }); 
}); 

前面的代码是这样工作的:

  1. 首先,我们定义了submit()方法。在submit方法中,我们确保选择了一个文件,并且文本字段不为空。然后,我们将文件的内容作为数组缓冲区读取,并将数组缓冲区传递给 sha1.js 公开的sha1()方法,以获取数组缓冲区内内容的哈希。一旦我们有了散列,我们使用 jQuery 向/submit路由发出一个 AJAX 请求,然后我们在警告框中显示事务散列。
  2. 接下来我们定义getInfo()方法。它首先确保选择了一个文件。然后,它生成与之前生成的散列相似的散列,并向/getInfo端点发出请求以获取关于该文件的信息。
  3. 最后,我们使用由socket.io库公开的io()方法建立一个socket.io连接。然后,我们等待触发器的连接事件,这表明连接已经建立。连接建立后,我们监听来自服务器的消息,并向用户显示有关事务的详细信息。

我们没有在以太坊区块链存储文件,因为存储文件非常昂贵,因为它需要大量的气体。对于我们的例子,我们实际上不需要存储文件,因为网络中的节点将能够看到该文件;因此,如果用户想要对文件内容保密,那么他们将无法做到。我们的应用程序的目的只是证明文件的所有权,而不是像云服务一样存储和提供文件。

测试客户端

现在运行app.js节点来运行应用服务器。打开您最喜欢的浏览器并访问http://localhost:8080/。您将在浏览器中看到以下输出:

现在选择一个文件,输入所有者的名字,然后点击提交。屏幕将变成这样:

在这里,您可以看到显示了事务散列。现在等待,直到事务被挖掘。一旦交易被挖掘,您将能够在实时交易列表中看到该交易。屏幕看起来是这样的:

现在再次选择同一个文件,然后点击“获取信息”按钮。您将看到以下输出:

在这里,您可以看到时间戳和所有者的详细信息。现在我们已经完成了第一个 DApp 的客户端构建。

摘要

在本章中,我们首先通过例子了解了 web3.js 的基础知识。我们学习了如何连接到一个节点、基本的 API、发送各种事务以及监视事件。最后,我们为我们的所有权契约构建了一个合适的生产使用客户机。现在,您将能够轻松地编写智能合约并为其构建 UI 客户端,以便于使用。

在第九章中,我们将建立一个钱包服务,用户可以很容易地创建和管理以太坊钱包,这也是离线的。我们将专门使用 LightWallet 库来实现这一点。


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组