当前位置:   article > 正文

区块链项目构建指南(二)_入门区块链项目

入门区块链项目

原文:zh.annas-archive.org/md5/e61d4f5cf7a1ecdfea6a6e32a165bf64

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:构建钱包服务

钱包服务用于发送和接收资金。构建钱包服务的主要挑战是安全性和信任。用户必须感到他们的资金是安全的,并且钱包服务的管理员不会窃取他们的资金。我们将在本章中构建的钱包服务将解决这两个问题。

在本章中,我们将涵盖以下主题:

  • 在线和离线钱包的区别

  • 使用 hooked-web3-provider 和 ethereumjs-tx 可以更轻松地创建和签署使用由以太坊节点管理的帐户以外的帐户的交易。

  • 理解 HD 钱包以及其用途。

  • 使用 lightwallet.js 创建 HD 钱包和交易签名器

  • 构建钱包服务

在线和离线钱包的区别

钱包是帐户的集合,而帐户是地址及其关联私钥的组合。

当钱包连接到互联网时,称为在线钱包。例如,存储在 geth 中的钱包、任何网站/数据库等称为在线钱包。在线钱包也称为热钱包、网页钱包、托管钱包等。至少在存储大量以太币或长时间存储以太币时,不建议使用在线钱包,因为它们存在风险。此外,根据钱包存储的位置不同,可能需要信任第三方。

例如,大多数流行的钱包服务都会将钱包的私钥与自己存储,并允许您通过电子邮件和密码访问钱包,因此基本上您无法真正访问钱包,如果他们愿意,他们可以窃取钱包中的资金。

当钱包未连接到互联网时,称为离线钱包。例如,存储在闪存驱动器、纸张、文本文件等中的钱包。离线钱包也称为冷钱包。离线钱包比在线钱包更安全,因为要窃取资金,某人需要对存储物理接触。离线存储的挑战在于您需要找到一个您不会意外删除或忘记的位置,或者其他人无法访问。许多人将钱包存储在纸上,并将纸放在保险柜中,如果他们想要长期安全地保留一些资金。如果您想经常从您的帐户发送资金,那么您可以将其存储在受密码保护的闪存驱动器中,并且还可以放在安全柜中。仅在数字设备中存储钱包有一点风险,因为数字设备随时可能损坏,您可能无法访问您的钱包;因此,除了存储在闪存驱动器中,您还应该将其放在安全柜中。根据您的需求,您还可以找到更好的解决方案,但请确保它是安全的,并且不要意外丢失对其的访问权限。

hooked-web3-provider 和 ethereumjs-tx 库

到目前为止,我们看到的 Web3.js 库的 sendTransaction() 方法的所有示例都是使用 Ethereum 节点中存在的 from 地址;因此,在广播之前以太坊节点能够签署交易。但是如果你有钱包的私钥存储在其他地方,那么 geth 就无法找到它。因此,在这种情况下,您需要使用 web3.eth.sendRawTransaction() 方法来广播交易。

web3.eth.sendRawTransaction() 用于广播原始交易,也就是说,您将不得不编写代码来创建和签署原始交易。以太坊节点将直接广播它,而不对交易进行其他处理。但是编写使用 web3.eth.sendRawTransaction() 广播交易的代码很困难,因为它需要生成数据部分、创建原始交易,还要对交易进行签名。

Hooked-Web3-Provider 库为我们提供了一个自定义的提供程序,它使用 HTTP 与 geth 进行通信;但这个提供程序的独特之处在于,它允许我们使用我们的密钥对合约实例的 sendTransaction() 调用进行签名。因此,我们不再需要创建交易的数据部分。这个自定义提供程序实际上覆盖了 web3.eth.sendTransaction() 方法的实现。所以基本上,它允许我们对合约实例的 sendTransaction() 调用以及 web3.eth.sendTransaction() 调用进行签名。合约实例的 sendTransaction() 方法内部生成交易的数据,并调用 web3.eth.sendTransaction() 来广播交易。

EthereumJS 是与以太坊相关的那些库的集合。ethereumjs-tx 是其中之一,提供了与交易相关的各种 API。例如,它允许我们创建原始交易、签署原始交易、检查交易是否使用正确的密钥签名等。

这两个库都适用于 Node.js 和客户端 JavaScript。从 www.npmjs.com/package/hooked-web3-provider 下载 Hooked-Web3-Provider,从 www.npmjs.com/package/ethereumjs-tx 下载 ethereumjs-tx。

在编写本书时,Hooked-Web3-Provider 的最新版本是 1.0.0,ethereumjs-tx 的最新版本是 1.1.4。

让我们看看如何将这些库一起使用,从一个不由 geth 管理的账户发送交易。

var provider = new HookedWeb3Provider({ 
 host: "http://localhost:8545", 
 transaction_signer: { 
  hasAddress: function(address, callback){ 
   callback(null, true); 
  }, 
  signTransaction: function(tx_params, callback){ 
   var rawTx = { 
          gasPrice: web3.toHex(tx_params.gasPrice), 
          gasLimit: web3.toHex(tx_params.gas), 
          value: web3.toHex(tx_params.value) 
          from: tx_params.from, 
          to: tx_params.to, 
          nonce: web3.toHex(tx_params.nonce) 
      }; 

   var privateKey = EthJS.Util.toBuffer('0x1a56e47492bf3df9c9563fa7f66e4e032c661de9d68c3f36f358e6bc9a9f69f2', 'hex'); 
   var tx = new EthJS.Tx(rawTx); 
   tx.sign(privateKey); 

   callback(null, tx.serialize().toString('hex')); 
  } 
 } 
}); 

var web3 = new Web3(provider); 

web3.eth.sendTransaction({ 
 from: "0xba6406ddf8817620393ab1310ab4d0c2deda714d", 
 to: "0x2bdbec0ccd70307a00c66de02789e394c2c7d549", 
 value: web3.toWei("0.1", "ether"), 
 gasPrice: "20000000000", 
 gas: "21000" 
}, function(error, result){ 
 console.log(error, result) 
})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

以下是代码的工作原理:

  1. 首先,我们创建了一个 HookedWeb3Provider 实例。这由 Hooked-Web3-Provider 库提供。这个构造函数接受一个对象,该对象必须提供两个属性。host 是节点的 HTTP URL,transaction_signer 是一个与自定义提供程序通信以获取交易签名的对象。

  2. transaction_signer对象有两个属性:hasAddresssignTransactionhasAddress被调用来检查交易是否可以签名,也就是检查交易签名者是否拥有from地址账户的私钥。该方法接收地址和一个回调函数。回调函数应该以错误消息作为第一个参数调用,而私钥如果未找到则应为false作为第二个参数。如果找到了私钥,那么第一个参数应该为null,第二个参数应为true

  3. 如果找到了地址的私钥,那么自定义提供者将调用signTransaction方法来对交易进行签名。该方法有两个参数,即交易参数和一个回调。在方法内部,首先,我们将交易参数转换为原始交易参数,也就是将原始交易参数值编码为十六进制字符串。然后,我们创建一个缓冲区来保存私钥。该缓冲区是使用EthJS.Util.toBuffer()方法创建的,该方法是ethereumjs-util库的一部分。ethereumjs-util库是ethereumjs-tx库导入的。然后我们创建一个原始交易并对其进行签名,之后我们对其进行序列化并转换为十六进制字符串。最后,我们需要使用回调将签名的原始交易的十六进制字符串提供给自定义提供者。如果方法内部有错误,则回调的第一个参数应该是一个错误消息。

  4. 现在自定义提供者获取原始交易并使用web3.eth.sendRawTransaction()来广播它。

  5. 最后,我们调用web3.eth.sendTransaction函数向另一个账户发送一些以太币。在这里,我们需要提供除了nonce之外的所有交易参数,因为自定义提供者可以计算 nonce。之前,许多这些都是可选的,因为我们让以太坊节点来计算它们,但现在当我们自己签名时,我们需要提供所有这些。当交易没有任何与之关联的数据时,gas始终为 21,000。

公钥呢?

在上面的代码中,我们从未提及签名地址的公钥。你一定想知道矿工如何在没有公钥的情况下验证交易的真实性。矿工使用 ECDSA 的一个独特属性,该属性允许您从消息和签名中计算公钥。在交易中,消息指示了交易的意图,而签名用于确定消息是否使用了正确的私钥进行签名。这就是使 ECDSA 如此特殊的地方。ethereumjs-tx 提供了一个 API 来验证交易。

什么是分层确定性钱包?

分层确定性钱包是从称为种子的单个起始点派生地址和密钥的系统。 确定性意味着对于相同的种子,将生成相同的地址和密钥,并且分层意味着将以相同的顺序生成地址和密钥。 这使得更容易备份和存储多个帐户,因为您只需要存储种子,而不需要存储单独的密钥和地址。

为什么用户需要多个帐户?

您可能想知道为什么用户需要多个帐户。 原因是为了隐藏他们的财富。 帐户的余额是公开可见的。 因此,如果用户 A 与用户 B 共享一个地址来接收一些以太币,那么用户 B 可以查看该地址中有多少以太币。 因此,用户通常会在各种帐户之间分配他们的财富。

有各种类型的 HD 钱包,它们在种子格式和生成地址和密钥的算法方面有所不同,例如 BIP32、Armory、Coinkite、Coinb.in 等。

什么是 BIP32、BIP44 和 BIP39?

比特币改进提案BIP)是一份向比特币社区提供信息的设计文件,或者描述比特币或其流程或环境的新功能。 BIP 应该提供对该功能的简明技术规范和功能的基本原理。 在撰写本书时,有 152 个 BIPS(比特币改进提案)。 BIP32 和 BIP39 分别提供了有关实现 HD 钱包和助记种子规范的算法的信息。 您可以在github.com/bitcoin/bips上了解更多信息。

导出密钥的功能简介

非对称加密算法定义了其密钥的性质以及如何生成密钥,因为密钥需要相关联。 例如,RSA 密钥生成算法是确定性的。

对称加密算法仅定义密钥大小。 生成密钥交给我们。 有各种算法来生成这些密钥。 这样的算法之一是 KDF。

密钥派生函数KDF)是一种确定性算法,用于从某个秘密值(例如主密钥、密码或口令)派生对称密钥。 有各种类型的 KDF,例如 bcrypt、crypt、PBKDF2、scrypt、HKDF 等。 您可以在en.wikipedia.org/wiki/Key_derivation_function上了解更多关于 KDF 的信息。

要从单个秘密值生成多个密钥,可以连接一个数字并递增它。

基于密码的密钥派生函数接受一个密码并生成对称密钥。由于用户通常使用弱密码,基于密码的密钥派生函数被设计为更慢且占用大量内存,以使启动暴力攻击和其他类型的攻击变得困难。基于密码的密钥派生函数被广泛使用,因为很难记住秘密密钥,将它们存储在某处是有风险的,因为它可能被窃取。PBKDF2 是基于密码的密钥派生函数的一个例子。

主密钥或密码短语很难通过暴力攻击破解;因此,如果您想要从主密钥或密码短语生成对称密钥,可以使用非基于密码的密钥派生函数,例如 HKDF。与 PBKDF2 相比,HKDF 要快得多。

为什么不直接使用散列函数而不是 KDFs?

散列函数的输出可以用作对称密钥。那么你一定会想到为什么需要 KDFs。嗯,如果您使用的是主密钥、密码短语或强密码,您可以简单地使用散列函数。例如,HKDF 简单地使用散列函数生成密钥。但如果不能保证用户将使用强密码,最好使用基于密码的散列函数。

LightWallet 介绍

LightWallet 是一个实现了 BIP32、BIP39 和 BIP44 的 HD 钱包。LightWallet 提供了使用其生成的地址和密钥创建和签名交易或加密和解密数据的 API。

LightWallet API 分为四个命名空间,即 keystoresigningencryptiontxutilssigningencryptiontxutils 分别提供签署交易、非对称加密和创建交易的 API,而 keystore 命名空间用于创建 keystore、生成的种子等。keystore 是一个保存种子和加密密钥的对象。如果我们使用 Hooked-Web3-Provider,keystore 命名空间实现了需要签署 we3.eth.sendTransaction() 调用的交易签名方法。因此,keystore 命名空间可以自动为其中找到的地址创建和签署交易。实际上,LightWallet 主要用于成为 Hooked-Web3-Provider 的签名提供者。

keystore 实例可以配置为创建和签名交易或加密和解密数据。对于签名交易,它使用 secp256k1 参数,而对于加密和解密,它使用 curve25519 参数。

LightWallet 的种子是一个包含 12 个单词的助记词,易于记忆但难以破解。这不是任意的 12 个单词;相反,它应该是由 LightWallet 生成的种子。由 LightWallet 生成的种子在选择单词和其他方面具有特定属性。

HD 派生路径

HD 派生路径是一个字符串,它使得易于处理多个加密货币(假设它们都使用相同的签名算法)、多个区块链、多个账户等等。

HD 派生路径可以具有任意多个参数,并且可以使用不同值的参数,我们可以产生不同组的地址及其相关密钥。

LightWallet 默认使用m/0'/0'/0'派生路径。这里,/n'是一个参数,n是参数值。

每个 HD 派生路径都有一个curvepurposepurpose可以是signasymEncryptsign表示该路径用于签署交易,而asymEncrypt表示该路径用于加密和解密。curve指示 ECC 的参数。对于签名,参数必须是secp256k1,对于非对称加密,曲线必须是curve25591,因为 LightWallet 强制我们使用这些参数,由于其在这些用途上的好处。

构建钱包服务

现在我们已经学习了足够多关于 LightWallet 的理论知识,是时候使用 LightWallet 和 hooked-web3-provider 构建一个钱包服务。我们的钱包服务将允许用户生成唯一的种子,显示地址及其关联余额,最后,该服务将允许用户向其他账户发送以太币。所有操作都将在客户端完成,这样用户就可以轻松地信任我们。用户要么必须记住种子,要么将其存放在某处。

先决条件

在开始构建钱包服务之前,请确保您正在运行开采的 geth 开发实例,已启用 HTTP-RPC 服务器,允许来自任何域的客户端请求,并最终已解锁账户 0。您可以通过运行以下命令来做到这一切:

    geth --dev --rpc --rpccorsdomain "*" --rpcaddr "0.0.0.0" --rpcport "8545" --mine --unlock=0

  • 1
  • 2

在这里,--rpccorsdomain用于允许特定域与 geth 通信。我们需要提供一个用空格分隔的域列表,例如"http://localhost:8080 https://mySite.com *"。它也支持*通配符字符。--rpcaddr表示 geth 服务器可到达的 IP 地址。默认值为127.0.0.1,因此如果它是托管服务器,您将无法使用服务器的公共 IP 地址来访问它。因此,我们将其值更改为0.0.0.0,表示服务器可以使用任何 IP 地址访问。

项目结构

在本章的练习文件中,您会找到两个目录,分别是FinalInitialFinal包含项目的最终源代码,而Initial包含了空的源代码文件和库,以便快速开始构建应用程序。

要测试Final目录,您需要在其中运行npm install,然后使用Final目录内的node app.js命令运行应用。

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

public目录包含与前端相关的文件。在public/css中,您将找到bootstrap.min.css,这是 bootstrap 库。在public/html中,您将找到index.html,在那里您将放置我们应用的 HTML 代码,最后,在public/js目录中,您将找到用于 Hooked-Web3-Provider、web3js 和 LightWallet 的.js文件。在public/js中,您还将找到一个main.js文件,其中您将放置我们应用的前端 JS 代码。

构建后端

让我们首先构建应用程序的后端。首先,在初始目录中运行npm install来安装我们后端所需的依赖项。

这是运行 express 服务并提供index.html文件和静态文件的完整后端代码:

var express = require("express");   
var app = express();   

app.use(express.static("public")); 

app.get("/", function(req, res){ 
 res.sendFile(__dirname + "/public/html/index.html"); 
}) 

app.listen(8080);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

上述代码是不言自明的。

构建前端

现在让我们构建应用的前端。前端将包括主要功能,即生成种子、显示种子地址和发送以太。

现在让我们编写应用程序的 HTML 代码。将此代码放入index.html文件中:

<!DOCTYPE html> 
 <html lang="en"> 
     <head> 
         <meta charset="utf-8"> 
         <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 
         <meta http-equiv="x-ua-compatible" content="ie=edge"> 
         <link rel="stylesheet" href="/css/bootstrap.min.css"> 
     </head> 
     <body> 
         <div class="container"> 
             <div class="row"> 
                 <div class="col-md-6 offset-md-3"> 
                     <br> 
                     <div class="alert alert-info" id="info" role="alert"> 
                           Create or use your existing wallet. 
                     </div> 
                     <form> 
                         <div class="form-group"> 
                             <label for="seed">Enter 12-word seed</label> 
                             <input type="text" class="form-control" id="seed"> 
                         </div> 
                         <button type="button" class="btn btn-primary" onclick="generate_addresses()">Generate Details</button> 
                         <button type="button" class="btn btn-primary" onclick="generate_seed()">Generate New Seed</button> 
                     </form> 
                     <hr> 
                     <h2 class="text-xs-center">Address, Keys and Balances of the seed</h2> 
                     <ol id="list"> 
                     </ol> 
                     <hr> 
                     <h2 class="text-xs-center">Send ether</h2> 
                     <form> 
                         <div class="form-group"> 
                             <label for="address1">From address</label> 
                             <input type="text" class="form-control" id="address1"> 
                         </div> 
                         <div class="form-group"> 
                             <label for="address2">To address</label> 
                             <input type="text" class="form-control" id="address2"> 
                         </div> 
                         <div class="form-group"> 
                             <label for="ether">Ether</label> 
                             <input type="text" class="form-control" id="ether"> 
                         </div> 
                         <button type="button" class="btn btn-primary" onclick="send_ether()">Send Ether</button> 
                     </form> 
                 </div> 
             </div> 
         </div> 

            <script src="img/web3.min.js"></script> 
            <script src="img/hooked-web3-provider.min.js"></script> 
         <script src="img/lightwallet.min.js"></script> 
         <script src="img/main.js"></script> 
     </body> 
 </html>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

代码的工作原理如下:

  1. 首先,我们将一个 Bootstrap 4 样式表加入队列。

  2. 然后我们显示一个信息框,我们将在其中向用户显示各种消息。

  3. 然后我们有一个带有输入框和两个按钮的表单。输入框用于输入种子,或者在生成新种子时,在那里显示种子。

  4. 生成详细信息按钮用于显示地址,而生成新种子按钮用于生成新的唯一种子。当点击生成详细信息时,我们调用generate_addresses()方法,当点击生成新种子按钮时,我们调用generate_seed()方法。

  5. 稍后,我们有一个空的有序列表。在这里,当用户单击生成详细信息按钮时,我们将动态显示种子的地址、余额和关联的私钥。

  6. 最后,我们有另一种形式,需要一个发送地址和一个接收地址以及要转移的以太数量。发送地址必须是当前在无序列表中显示的地址之一。

现在让我们编写 HTML 代码调用每个函数的实现。首先,让我们编写代码以生成新种子。将此代码放入main.js文件中:

function generate_seed() 
{ 
 var new_seed = lightwallet.keystore.generateRandomSeed(); 

 document.getElementById("seed").value = new_seed; 

 generate_addresses(new_seed); 
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

keystore命名空间的generateRandomSeed()方法用于生成随机种子。它接受一个可选参数,该参数是一个指示额外熵的字符串。

熵是应用程序收集用于某些算法或其他需要随机数据的地方的随机性。通常,熵是从硬件源收集的,可以是现有的源,例如鼠标移动或专门提供的随机性生成器。

要生成唯一的种子,我们需要真正的高熵。LightWallet 已经内置了产生唯一种子的方法。LightWallet 用于生成熵的算法取决于环境。但是,如果您认为可以生成更好的熵,则可以将生成的熵传递给generateRandomSeed()方法,并且它将与内部由generateRandomSeed()生成的熵连接在一起。

生成随机种子后,我们调用generate_addresses方法。此方法接受一个种子并在其中显示地址。在生成地址之前,它会提示用户询问他们想要多少地址。

以下是generate_addresses()方法的实现。将此代码放入main.js文件中:

var totalAddresses = 0; 

function generate_addresses(seed) 
{ 
 if(seed == undefined) 
 { 
  seed = document.getElementById("seed").value; 
 } 

 if(!lightwallet.keystore.isSeedValid(seed)) 
 { 
  document.getElementById("info").innerHTML = "Please enter a valid seed"; 
  return; 
 } 

 totalAddresses = prompt("How many addresses do you want to generate"); 

 if(!Number.isInteger(parseInt(totalAddresses))) 
 { 
  document.getElementById("info").innerHTML = "Please enter valid number of addresses"; 
  return; 
 } 

 var password = Math.random().toString(); 

 lightwallet.keystore.createVault({ 
  password: password, 
    seedPhrase: seed 
 }, function (err, ks) { 
    ks.keyFromPassword(password, function (err, pwDerivedKey) { 
      if(err) 
      { 
       document.getElementById("info").innerHTML = err; 
      } 
      else 
      { 
       ks.generateNewAddress(pwDerivedKey, totalAddresses); 
       var addresses = ks.getAddresses();  

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

       var html = ""; 

       for(var count = 0; count < addresses.length; count++) 
       { 
     var address = addresses[count]; 
     var private_key = ks.exportPrivateKey(address, pwDerivedKey); 
     var balance = web3.eth.getBalance("0x" + address); 

     html = html + "<li>"; 
     html = html + "<p><b>Address: </b>0x" + address + "</p>"; 
     html = html + "<p><b>Private Key: </b>0x" + private_key + "</p>"; 
     html = html + "<p><b>Balance: </b>" + web3.fromWei(balance, "ether") + " ether</p>"; 
        html = html + "</li>"; 
       } 

       document.getElementById("list").innerHTML = html; 
      } 
    }); 
 }); 
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

以下是代码的工作原理:

  1. 首先,我们有一个名为totalAddresses的变量,其中包含一个数字,表示用户想要生成的地址总数。

  2. 然后我们检查seed参数是否已定义。如果未定义,则从输入字段获取种子。我们这样做是为了在生成新种子时以及用户单击生成详细信息按钮时,可以使用generate_addresses()方法来显示信息种子。

  3. 然后我们使用keystore命名空间的isSeedValid()方法验证种子。

  4. 然后我们要求用户输入有关他们想要生成和显示多少地址的输入。然后我们验证输入。

  5. keystore命名空间中的私钥始终以加密形式存储。在生成密钥时,我们需要对其进行加密,而在签署交易时,我们需要解密密钥。从用户处获取对称加密密钥的密码,或通过提供随机字符串作为密码来获得。为了提供更好的用户体验,我们生成一个随机字符串并将其用作密码。对称密钥不存储在keystore命名空间内;因此,我们需要在执行与私钥相关的操作时(如生成密钥、访问密钥等),从密码生成密钥。

  6. 然后我们使用createVault方法创建一个keystore实例。createVault接受一个对象和一个回调函数。对象可以有四个属性:passwordseedPharsesalthdPathStringpassword是必需的,其他全部是可选的。如果我们不提供seedPharse,它会生成并使用一个随机种子。salt被连接到密码上,以增加对称密钥的安全性,因为攻击者还必须找到盐和密码。如果未提供盐,它将被随机生成。keystore命名空间保存未加密的盐。hdPathString用于为keystore命名空间提供默认派生路径,即在生成地址、签署交易等中使用。如果我们不提供派生路径,将使用此派生路径。如果我们不提供hdPathString,则默认值为m/0'/0'/0'。此派生路径的默认目的是sign。您可以使用keystore实例的addHdDerivationPath()方法创建新的派生路径或覆盖现有派生路径的目的。您也可以使用keystore实例的setDefaultHdDerivationPath()方法更改默认派生路径。最后,一旦创建了keystore命名空间,实例会通过回调返回。因此,在这里,我们只使用密码和种子创建了一个keystore

  7. 现在我们需要生成用户需要的地址数量及其关联的密钥。因为我们可以从种子生成数百万个地址,所以keystore在我们需要它之前不会生成任何地址,因为它不知道我们要生成多少个地址。创建了keystore之后,我们使用keyFromPassword方法从密码生成对称密钥。然后我们调用generateNewAddress()方法生成地址及其关联的密钥。

  8. generateNewAddress()方法接受三个参数:从密码派生的密钥、要生成的地址数和派生路径。由于我们没有提供派生路径,它使用keystore的默认派生路径。如果多次调用generateNewAddress(),它将从上一次创建的地址处继续。例如,如果两次调用这个方法,每次生成两个地址,你将得到前四个地址。

  9. 然后我们使用getAddresses()来获取keystore中存储的所有地址。

  10. 我们使用exportPrivateKey方法解密并检索地址的私钥。

  11. 我们使用web3.eth.getBalance()来获取地址的余额。

  12. 最后,我们在无序列表中显示所有信息。

现在我们知道如何从种子生成地址及其私钥了。现在让我们编写send_ether()方法的实现,该方法用于从种子生成的地址中发送以太币。

这是用于此的代码。将此代码放入main.js文件中:

function send_ether() 
{ 
 var seed = document.getElementById("seed").value; 

 if(!lightwallet.keystore.isSeedValid(seed)) 
 { 
  document.getElementById("info").innerHTML = "Please enter a valid seed"; 
  return; 
 } 

 var password = Math.random().toString(); 

 lightwallet.keystore.createVault({ 
  password: password, 
    seedPhrase: seed 
 }, function (err, ks) { 
    ks.keyFromPassword(password, function (err, pwDerivedKey) { 
      if(err) 
      { 
       document.getElementById("info").innerHTML = err; 
      } 
      else 
      { 
       ks.generateNewAddress(pwDerivedKey, totalAddresses); 

       ks.passwordProvider = function (callback) { 
          callback(null, password); 
       }; 

       var provider = new HookedWeb3Provider({ 
       host: "http://localhost:8545", 
       transaction_signer: ks 
    }); 

       var web3 = new Web3(provider); 

       var from = document.getElementById("address1").value; 
    var to = document.getElementById("address2").value; 
       var value = web3.toWei(document.getElementById("ether").value, "ether"); 

       web3.eth.sendTransaction({ 
        from: from, 
        to: to, 
        value: value, 
        gas: 21000 
       }, function(error, result){ 
        if(error) 
        {  
         document.getElementById("info").innerHTML = error; 
        } 
        else 
        { 
         document.getElementById("info").innerHTML = "Txn hash: " + result; 
        } 
       }) 
      } 
    }); 
 }); 
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

到生成地址种子的代码部分为止都是自说明的。之后,我们将一个回调分配给kspasswordProvider属性。在交易签名期间,此回调会被调用以获取解密私钥所需的密码。如果我们不提供此回调,LightWallet 会提示用户输入密码。然后,我们通过将keystore作为交易签名者传递来创建一个HookedWeb3Provider实例。现在,当自定义提供程序想要签署交易时,它会调用kshasAddresssignTransactions方法。如果要签署的地址不在生成的地址列表中,ks将向自定义提供程序返回错误。最后,我们使用web3.eth.sendTransaction方法发送一些以太币。

测试中

现在我们已经完成了建立钱包服务的工作,让我们来测试一下以确保它按预期运行。首先,在初始目录中运行node app.js,然后在喜爱的浏览器中访问http://localhost:8080。你会看到这个屏幕:

现在点击"Generate New Seed"按钮生成一个新种子。你将被提示输入表示要生成的地址数量的数字。你可以提供任意数字,但出于测试目的,提供一个大于 1 的数字。现在屏幕会看起来像这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在要测试发送以太币,你需要从 coinbase 账户向生成的地址中的一个发送一些以太币。一旦你向其中一个生成的地址发送了一些以太币,点击"Generate Details"按钮刷新界面,尽管用钱包服务发送以太币并不需要这样做。确保再次生成相同的地址。现在屏幕会看起来如下:

现在,在"from"地址字段中输入列表中具有余额的账户地址。然后在"to"地址字段中输入另一个地址。为了测试目的,你可以输入显示的其他任何地址。然后输入小于或等于"from"地址账户的以太坊余额的一些以太币数量。现在你的屏幕会看起来如下:

现在点击"Send Ether"按钮,你会在信息框中看到交易哈希。等待一段时间让它被挖掘。同时,你可以通过在非常短的时间内点击"Generate Details"按钮来检查交易是否已被挖掘。一旦交易被挖掘,你的屏幕将看起来像这样:

如果一切按照说明的方式进行,你的钱包服务就已经准备好了。你可以将这个服务部署到自定义域名上,并让它对外提供使用。它是完全安全的,用户会信任它。

摘要

在本章中,你学习了三个重要的以太坊库:Hooked-Web3-Provider、ethereumjs-tx 和 LightWallet。这些库可用于在以太坊节点之外管理账户并签署交易。在开发大多数类型的 DApp 客户端时,你会发现这些库非常有用。

最后,我们创建了一个钱包服务,让用户可以管理与服务后端共享私钥或与其钱包相关的任何其他信息的账户。

在下一章中,我们将构建一个平台来构建和部署智能合约。

第六章:构建智能合约部署平台

一些客户端可能需要在运行时编译和部署合约。在我们的拥有权 DApp 中,我们手动部署了智能合约,并在客户端代码中硬编码了合约地址。但是一些客户端可能需要在运行时部署智能合约。例如,如果客户端允许学校在区块链中记录学生的出勤情况,那么每次注册新学校时都需要部署一个智能合约,以便每个学校完全控制其智能合约。在本章中,我们将学习如何使用 web3.js 编译智能合约,并使用 web3.js 和 EthereumJS 部署它。

在本章中,我们将涵盖以下主题:

  • 计算交易的 nonce

  • 使用交易池 JSON-RPC API

  • 生成用于合约创建和方法调用的交易数据

  • 估算交易所需的 gas

  • 查找账户的当前可花费余额

  • 使用 solcjs 编译智能合约

  • 开发一个平台来编写、编译和部署智能合约

计算交易的 nonce

对于由 geth 维护的账户,我们不需要担心交易 nonce,因为 geth 可以为交易添加正确的 nonce 并对其进行签名。当使用 geth 不管理的账户时,我们需要自己计算 nonce。

要自己计算 nonce,我们可以使用 geth 提供的 getTransactionCount 方法。第一个参数应该是我们需要交易计数的地址,第二个参数是我们需要交易计数直到的区块。我们可以提供 "pending" 字符串作为区块,以包括当前正在挖掘的区块中的交易。正如我们在之前的章节中讨论的那样,geth 维护一个交易池,其中包含挂起和排队的交易。为了挖掘一个区块,geth 从交易池中取出挂起的交易并开始挖掘新的区块。直到区块没有被挖掘,挂起的交易仍然保留在交易池中,一旦被挖掘,已挖掘的交易就会从交易池中删除。在挖掘一个区块时接收到的新进交易被放入交易池,并在下一个区块中被挖掘。因此,当我们在调用 getTransactionCount 时提供 "pending" 作为第二个参数时,它不会查看交易池内部;相反,它只考虑挂起区块中的交易。

因此,如果您试图从 geth 不管理的账户发送交易,则需计算区块链中账户的总交易数,并将其与交易池中挂起的交易相加。如果尝试使用挂起区块中的挂起交易,则在几秒钟的间隔内发送交易到 geth,您将无法获得正确的 nonce,因为平均需要 12 秒才能将交易包含在区块链中。

在上一章中,我们依赖hooked-web3-provider来为交易添加 nonce。不幸的是,hooked-web3-provider没有以正确的方式获取 nonce。它为每个账户维护一个计数器,并在你从该账户发送交易时递增该计数器。如果交易无效(例如,如果交易尝试发送的以太币多于它拥有的),则不会递减计数器。因此,该账户的其余交易将排队,并永远不会被挖掘,直到hooked-web3-provider被重置,即客户端被重新启动。如果你创建多个hooked-web3-provider实例,那么这些实例无法将一个账户的 nonce 与彼此同步,因此你可能会得到不正确的 nonce。但在你将 nonce 添加到交易之前,hooked-web3-provider总是获取到 pending 区块的交易计数,并将其与其计数器进行比较,并使用较大者。因此,如果由hooked-web3-provider管理的账户的交易是从网络中的另一个节点发送的,并包含在 pending 区块中,则hooked-web3-provider可以看到它。但总的来说,不能依赖hooked-web3-provider来计算 nonce。它非常适用于快速原型设计客户端应用程序,并适用于在用户可以看到并重新发送交易(如果它们未广播到网络并且hooked-web3-provider经常被重置)的应用程序中使用。例如,在我们的钱包服务中,用户会频繁加载页面,因此会频繁创建新的hooked-web3-provider实例。如果交易未被广播、无效或未被挖掘,那么用户可以刷新页面并重新发送交易。

介绍 solcjs

solcjs 是一个用于编译 solidity 文件的 Node.js 库和命令行工具。它不使用 solc 命令行编译器;相反,它纯粹使用 JavaScript 进行编译,因此比 solc 更容易安装。

Solc 是实际的 Solidity 编译器。Solc 是用 C++ 编写的。这个 C++ 代码使用 emscripten 编译成 JavaScript。每个 solc 版本都被编译成 JavaScript。在 github.com/ethereum/solc-bin/tree/gh-pages/bin,你可以找到每个 solidity 版本的基于 JavaScript 的编译器。solcjs只是使用这些基于 JavaScript 的编译器之一来编译 solidity 源代码。这些基于 JavaScript 的编译器可以在浏览器和 Node.js 环境中运行。

浏览器 Solidity 使用这些基于 JavaScript 的编译器来编译 Solidity 源代码。

安装 solcjs

solcjs 作为一个名为 solc 的 npm 包可供使用。你可以像安装其他任何 npm 包一样,本地或全局安装 solcjs npm 包。如果全局安装了此包,则将可用一个名为 solcjs 的命令行工具。因此,为了安装命令行工具,请运行此命令:

    npm install -g solc

  • 1
  • 2

现在,请继续运行此命令以查看如何使用命令行编译器编译 Solidity 文件:

    solcjs -help

  • 1
  • 2

我们不会探索 solcjs 命令行工具;而是学习如何使用 solcjs API 来编译 Solidity 文件。

默认情况下,solcjs 将使用匹配其版本的编译器版本。例如,如果您安装了 solcjs 版本 0.4.8,则默认情况下将使用 0.4.8 编译器版本进行编译。solcjs 还可以配置为使用其他编译器版本。在撰写本文时,solcjs 的最新版本是 0.4.8。

solcjs API

solcjs 提供了一个compiler方法,用于编译 Solidity 代码。该方法可以根据源代码是否有任何导入来以两种不同的方式使用。如果源代码没有任何导入,则它需要两个参数;即,第一个参数是 Solidity 源代码作为字符串,第二个参数是一个布尔值,表示是否优化字节码。如果源字符串包含多个合约,则它会将它们全部编译。

这里有一个示例来演示这一点:

var solc = require("solc"); 
var input = "contract x { function g() {} }"; 
var output = solc.compile(input, 1); // 1 activates the optimiser  
for (var contractName in output.contracts) { 
    // logging code and ABI  
    console.log(contractName + ": " + output.contracts[contractName].bytecode); 
    console.log(contractName + "; " + JSON.parse(output.contracts[contractName].interface)); 
} 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如果您的源代码包含导入,则第一个参数将是一个对象,其键是文件名,值是文件的内容。因此,每当编译器看到一个导入语句时,它不会在文件系统中查找文件;相反,它会通过与键匹配的文件名在对象中查找文件内容。以下是一个演示示例:

var solc = require("solc"); 
var input = { 
    "lib.sol": "library L { function f() returns (uint) { return 7; } }", 
    "cont.sol": "import 'lib.sol'; contract x { function g() { L.f(); } }" 
}; 
var output = solc.compile({sources: input}, 1); 
for (var contractName in output.contracts) 
    console.log(contractName + ": " + output.contracts[contractName].bytecode); 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如果您想在编译过程中从文件系统读取导入的文件内容,或者解析编译过程中的文件内容,则编译器方法支持第三个参数,该参数是一个方法,接受文件名并应返回文件内容。以下是一个演示示例:

var solc = require("solc"); 
var input = { 
    "cont.sol": "import 'lib.sol'; contract x { function g() { L.f(); } }" 
}; 
function findImports(path) { 
    if (path === "lib.sol") 
        return { contents: "library L { function f() returns (uint) { return 7; } }" } 
    else 
        return { error: "File not found" } 
} 
var output = solc.compile({sources: input}, 1, findImports); 
for (var contractName in output.contracts) 
    console.log(contractName + ": " + output.contracts[contractName].bytecode); 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

使用不同的编译器版本

要使用不同版本的 Solidity 编译合约,您需要使用useVersion方法获取不同编译器的引用。useVersion接受一个字符串,指示保存编译器的 JavaScript 文件名,并在/node_modules/solc/bin目录中查找该文件。

solcjs 还提供了另一个方法称为loadRemoteVersion,它接受与solc-bin存储库的solc-bin/bin目录中的文件名匹配的编译器文件名,并下载并使用它。

最后,solcjs 还提供了另一个方法称为setupMethods,它类似于useVersion但可以从任何目录加载编译器。

这里有一个示例来演示所有三种方法:

var solc = require("solc"); 

var solcV047 = solc.useVersion("v0.4.7.commit.822622cf"); 
var output = solcV011.compile("contract t { function g() {} }", 1); 

solc.loadRemoteVersion('soljson-v0.4.5.commit.b318366e', function(err, solcV045) { 
    if (err) { 
        // An error was encountered, display and quit 
    } 

    var output = solcV045.compile("contract t { function g() {} }", 1); 
}); 

var solcV048 = solc.setupMethods(require("/my/local/0.4.8.js")); 
var output = solcV048.compile("contract t { function g() {} }", 1); 

solc.loadRemoteVersion('latest', function(err, latestVersion) { 
    if (err) { 
        // An error was encountered, display and quit 
    } 
    var output = latestVersion.compile("contract t { function g() {} }", 1); 
}); 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

要运行上述代码,您首先需要从solc-bin存储库下载v0.4.7.commit.822622cf.js文件,并将其放置在node_modules/solc/bin目录中。然后,您需要下载 Solidity 版本 0.4.8 的编译器文件,并将其放置在文件系统的某个位置,并将路径指向setupMethods调用中的该目录。

链接库

如果你的 Solidity 源代码引用了库,那么生成的字节码将包含引用库的真实地址的占位符。这些必须通过一个称为链接的过程来更新,然后再部署合约。

solcjs 提供了linkByteCode方法来将库地址链接到生成的字节码上。

这里是一个示例来演示这一点:

var solc = require("solc"); 

var input = { 
    "lib.sol": "library L { function f() returns (uint) { return 7; } }", 
    "cont.sol": "import 'lib.sol'; contract x { function g() { L.f(); } }" 
}; 

var output = solc.compile({sources: input}, 1); 

var finalByteCode = solc.linkBytecode(output.contracts["x"].bytecode, { 'L': '0x123456...' }); 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

更新 ABI

合约的 ABI 提供了除实现以外的各种关于合约的信息。由于高版本支持更多的 Solidity 特性,因此两个不同版本的编译器生成的 ABI 可能不匹配;因此,它们会在 ABI 中包含额外的内容。例如,回退函数是在 Solidity 0.4.0 版本中引入的,因此使用版本低于 0.4.0 的编译器生成的 ABI 将不包含有关回退函数的信息,这些智能合约的行为就像它们有一个空主体和一个可支付修饰符的回退函数一样。因此,API 应该进行更新,以便依赖于较新 Solidity 版本的 ABI 的应用程序能够获得更好的合约信息。

solcjs 提供了一个 API 来更新 ABI。下面是一个示例代码来演示这一点:

var abi = require("solc/abi"); 

var inputABI = [{"constant":false,"inputs":[],"name":"hello","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"}]; 
var outputABI = abi.update("0.3.6", inputABI) 

  • 1
  • 2
  • 3
  • 4
  • 5

这里,0.3.6 表示 ABI 是使用 0.3.6 版本的编译器生成的。由于我们使用的是 solcjs 版本 0.4.8,因此 ABI 将被更新以匹配由 0.4.8 编译器版本生成的 ABI,而不是更新到更高版本。

上述代码的输出将如下所示:

[{"constant":false,"inputs":[],"name":"hello","outputs":[{"name":"","type":"string"}],"payable":true,"type":"function"},{"type":"fallback","payable":true}] 

  • 1
  • 2

构建合约部署平台

现在我们已经学会了如何使用 solcjs 编译 Solidity 源代码,是时候建立一个平台,让我们能够编写、编译和部署合约了。我们的平台将允许用户提供他们的账户地址和私钥,使用这些信息,我们的平台将部署合约。

在开始构建应用程序之前,请确保你正在运行 geth 开发实例,该实例正在挖矿,启用了rpc,并通过 HTTP-RPC 服务器公开了ethweb3txpool API。你可以通过运行以下命令来完成所有这些操作:

    geth --dev --rpc --rpccorsdomain "*" --rpcaddr "0.0.0.0" --rpcport "8545" --mine --rpcapi "eth,txpool,web3"

  • 1
  • 2

项目结构

在本章的练习文件中,你会找到两个目录,即FinalInitialFinal包含了项目的最终源代码,而Initial包含了空的源代码文件和库,可以快速开始构建应用程序。

要测试Final目录,你需要在其中运行npm install,然后在Final目录内使用node app.js命令运行应用程序。

Initial目录中,你会找到一个public目录和两个文件,分别命名为app.jspackage.jsonpackage.json文件包含了我们应用程序的后端依赖关系。app.js是你将放置后端源代码的地方。

public 目录包含与前端相关的文件。在 public/css 中,你会找到 bootstrap.min.css,这是 bootstrap 库,你还会找到 codemirror.css 文件,这是 codemirror 库的 CSS。在 public/html 中,你会找到 index.html,你会在这里放置我们应用的 HTML 代码,在 public/js 目录中,你会找到 codemirror 和 web3.js 的 .js 文件。在 public/js 中,你还会找到一个 main.js 文件,你会在这里放置我们应用的前端 JS 代码。

构建后端

让我们首先构建应用的后端。首先,在 Initial 目录中运行 npm install 安装我们后端所需的依赖项。

这是运行 express 服务并提供 index.html 文件和静态文件的后端代码:

var express = require("express");   
var app = express();   

app.use(express.static("public")); 

app.get("/", function(req, res){ 
 res.sendFile(__dirname + "/public/html/index.html"); 
}) 

app.listen(8080); 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

前述代码不言自明。现在让我们进一步进行。我们的应用将有两个按钮,即编译和部署。当用户单击编译按钮时,合约将被编译,当用户单击部署按钮时,合约将被部署。

我们将在后端进行编译和部署合约。虽然这可以在前端完成,但我们将在后端完成,因为 solcjs 仅适用于 Node.js(尽管它使用的基于 JavaScript 的编译器可以在前端工作)。

要了解如何在前端编译,请查看 solcjs 的源代码,这将让你了解 JavaScript-based 编译器暴露的 API。

当用户单击编译按钮时,前端将通过传递合约源代码向 /compile 路径发出 GET 请求。以下是路由的代码:

var solc = require("solc"); 

app.get("/compile", function(req, res){ 
 var output = solc.compile(req.query.code, 1); 
 res.send(output); 
}) 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

首先,我们在这里导入 solcjs 库。然后,我们定义 /compile 路由,在路由回调中,我们只是编译客户端发送的源代码,并启用优化器。然后,我们只需将 solc.compile 方法的返回值发送到前端,让客户端检查编译是否成功。

当用户单击部署按钮时,前端将通过传递合约源代码和构造函数参数从地址和私钥向 /deploy 路径发出 GET 请求。当用户单击此按钮时,将部署合约并将事务哈希返回给用户。

这是此代码的代码:

var Web3 = require("web3"); 
var BigNumber = require("bignumber.js"); 
var ethereumjsUtil = require("ethereumjs-util"); 
var ethereumjsTx = require("ethereumjs-tx"); 

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

function etherSpentInPendingTransactions(address, callback) 
{ 
 web3.currentProvider.sendAsync({ 
    method: "txpool_content", 
    params: [], 
    jsonrpc: "2.0", 
    id: new Date().getTime() 
 }, function (error, result) { 
  if(result.result.pending) 
  { 
   if(result.result.pending[address]) 
   { 
    var txns = result.result.pending[address]; 
    var cost = new BigNumber(0); 

    for(var txn in txns) 
    { 
     cost = cost.add((new BigNumber(parseInt(txns[txn].value))).add((new BigNumber(parseInt(txns[txn].gas))).mul(new BigNumber(parseInt(txns[txn].gasPrice))))); 
    } 

    callback(null, web3.fromWei(cost, "ether")); 
   } 
   else 
   { 
    callback(null, "0"); 
   } 
  } 
  else 
  { 
   callback(null, "0"); 
  } 
 }) 
} 

function getNonce(address, callback) 
{ 
 web3.eth.getTransactionCount(address, function(error, result){ 
  var txnsCount = result; 

  web3.currentProvider.sendAsync({ 
     method: "txpool_content", 
     params: [], 
     jsonrpc: "2.0", 
     id: new Date().getTime() 
  }, function (error, result) { 
   if(result.result.pending) 
   { 
    if(result.result.pending[address]) 
    { 
     txnsCount = txnsCount + Object.keys(result.result.pending[address]).length; 
     callback(null, txnsCount); 
    } 
    else 
    { 
     callback(null, txnsCount); 
    } 
   } 
   else 
   { 
    callback(null, txnsCount); 
   } 
  }) 
 }) 
} 

app.get("/deploy", function(req, res){ 
 var code = req.query.code; 
 var arguments = JSON.parse(req.query.arguments); 
 var address = req.query.address; 

 var output = solc.compile(code, 1); 

 var contracts = output.contracts; 

 for(var contractName in contracts) 
 { 
  var abi = JSON.parse(contracts[contractName].interface); 
  var byteCode = contracts[contractName].bytecode; 

  var contract = web3.eth.contract(abi); 

  var data = contract.new.getData.call(null, ...arguments, { 
   data: byteCode 
  }); 

  var gasRequired = web3.eth.estimateGas({ 
      data: "0x" + data 
  }); 

  web3.eth.getBalance(address, function(error, balance){ 
   var etherAvailable = web3.fromWei(balance, "ether"); 
   etherSpentInPendingTransactions(address, function(error, balance){ 
    etherAvailable = etherAvailable.sub(balance) 
    if(etherAvailable.gte(web3.fromWei(new BigNumber(web3.eth.gasPrice).mul(gasRequired), "ether"))) 
    { 
     getNonce(address, function(error, nonce){ 
      var rawTx = { 
             gasPrice: web3.toHex(web3.eth.gasPrice), 
             gasLimit: web3.toHex(gasRequired), 
             from: address, 
             nonce: web3.toHex(nonce), 
             data: "0x" + data 
         }; 

      var privateKey = ethereumjsUtil.toBuffer(req.query.key, 'hex'); 
      var tx = new ethereumjsTx(rawTx); 
      tx.sign(privateKey); 

      web3.eth.sendRawTransaction("0x" + tx.serialize().toString('hex'), function(err, hash) { 
       res.send({result: { 
        hash: hash, 
       }}); 
      }); 
     }) 
    } 
    else 
    { 
     res.send({error: "Insufficient Balance"}); 
    } 
   }) 
  }) 

     break; 
 } 
}) 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133

这是前述代码的工作方式:

  1. 首先,Web 导入 web3.jsBigNumber.jsethereumjs-utilethereumjs-tx 库。然后,我们创建了 Web3 的实例。

  2. 然后,我们定义一个名为etherInSpentPendingTransactions的函数,它计算地址的待处理交易中所花费的总以太币。由于web3.js不提供与事务池相关的 JavaScript API,我们使用web3.currentProvider.sendAsync来做一个原始的 JSON-RPC 调用。sendAsync用于异步进行原始的 JSON-RPC 调用。如果你想以同步的方式进行此调用,那么请使用send方法而不是sendAsync。在计算地址的待处理事务中的总以太币时,我们寻找事务池中的待处理事务而不是待处理区块,因为前面我们讨论的问题。在计算总以太币时,我们将每个交易的值和 gas 添加,因为 gas 也会减少以太币余额。

  3. 接下来,我们定义一个名为getNonce的函数,使用我们之前讨论的技术来检索地址的 nonce。它只是将已挖掘交易的总数加上待处理交易的总数。

  4. 最后,我们声明了/deploy端点。首先,我们编译合同。然后,我们只部署第一个合同。如果在提供的源代码中发现了多个合同,我们的平台设计为只部署第一个合同。你可以稍后增强应用程序来部署所有已编译的合同,而不仅仅是第一个合同。然后,我们使用web3.eth.contract创建一个合同对象。

  5. 由于我们没有使用 hooked-web3-provider 或任何拦截 sendTransactions 并将其转换为 sendRawTransaction 调用的方法,在部署合同时,我们现在需要生成交易数据部分,其中将合同字节码和构造函数参数组合并编码为十六进制字符串。实际上,合同对象让我们生成交易的数据。这可以通过调用具有函数参数的 getData 方法来完成。如果你想要获取部署合同的数据,然后调用 contract.new.getData,如果你想调用合同的一个函数,那么调用 contract.functionName.getData。在这两种情况下,提供参数给 getData 方法。因此,要生成交易的数据,你只需要合同的 ABI。要了解如何组合和编码函数名称和参数以生成数据,你可以查看github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#examples,但如果你有合同的 ABI 或知道如何手动创建 ABI,则这将不需要。

  6. 然后,我们使用web3.eth.estimateGas来计算部署合同所需的 gas 量。

  7. 稍后,我们检查地址是否有足够的以太来支付部署合约所需的 gas。我们通过检索地址的余额并将其减去在未决交易中花费的余额来找到这一点,然后检查剩余余额是否大于或等于所需 gas 的以太数量。

  8. 最后,我们获取 nonce,签名并广播交易。我们简单地将交易哈希返回给前端。

构建前端

现在让我们构建应用程序的前端。我们的前端将包含一个编辑器,用户使用该编辑器编写代码。当用户单击编译按钮时,我们将动态显示输入框,每个输入框将表示一个构造函数参数。当单击部署按钮时,将从这些输入框中获取构造函数参数值。用户将需要在这些输入框中输入 JSON 字符串。

我们将使用 codemirror 库来集成编辑器到我们的前端。要了解更多关于如何使用 codemirror 的信息,请参考codemirror.net/

这是我们应用程序的前端 HTML 代码。将此代码放入index.html文件中:

<!DOCTYPE html> 
<html lang="en"> 
    <head> 
        <meta charset="utf-8"> 
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 
        <meta http-equiv="x-ua-compatible" content="ie=edge"> 
        <link rel="stylesheet" href="/css/bootstrap.min.css"> 
        <link rel="stylesheet" href="/css/codemirror.css"> 
        <style type="text/css"> 
            .CodeMirror 
            { 
                height: auto; 
            } 
        </style> 
    </head> 
    <body> 
        <div class="container"> 
            <div class="row"> 
                <div class="col-md-6"> 
                    <br> 
                    <textarea id="editor"></textarea> 
                    <br> 
                    <span id="errors"></span> 
                    <button type="button" id="compile" class="btn btn-primary">Compile</button> 
                </div> 
                <div class="col-md-6"> 
                    <br> 
                    <form> 
                        <div class="form-group"> 
                            <label for="address">Address</label> 
                            <input type="text" class="form-control" id="address" placeholder="Prefixed with 0x"> 
                        </div> 
                        <div class="form-group"> 
                            <label for="key">Private Key</label> 
                            <input type="text" class="form-control" id="key" placeholder="Prefixed with 0x"> 
                        </div> 
                        <hr> 
                        <div id="arguments"></div> 
                        <hr> 
                        <button type="button" id="deploy" class="btn btn-primary">Deploy</button> 
                    </form> 
                </div> 
            </div> 
        </div> 
        <script src="img/codemirror.js"></script> 
        <script src="img/main.js"></script> 
    </body> 
</html> 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

在这里,您可以看到我们有一个textareatextarea标签将保存用户在 codemirror 编辑器中输入的内容。前面代码中的其他内容都是不言自明的。

这是完整的前端 JavaScript 代码。将此代码放入main.js文件中:

var editor = CodeMirror.fromTextArea(document.getElementById("editor"), { 
    lineNumbers: true, 
}); 

var argumentsCount = 0; 

document.getElementById("compile").addEventListener("click", function(){ 
 editor.save(); 
 var xhttp = new XMLHttpRequest(); 

 xhttp.onreadystatechange = function() { 
     if (this.readyState == 4 && this.status == 200) { 
      if(JSON.parse(xhttp.responseText).errors != undefined) 
      { 
       document.getElementById("errors").innerHTML = JSON.parse(xhttp.responseText).errors + "<br><br>"; 
      } 
      else 
      { 
       document.getElementById("errors").innerHTML = ""; 
      } 

      var contracts = JSON.parse(xhttp.responseText).contracts; 

      for(var contractName in contracts) 
      { 
       var abi = JSON.parse(contracts[contractName].interface); 

       document.getElementById("arguments").innerHTML = ""; 

       for(var count1 = 0; count1 < abi.length; count1++) 
       { 
        if(abi[count1].type == "constructor") 
        { 
         argumentsCount = abi[count1].inputs.length; 

         document.getElementById("arguments").innerHTML = '<label>Arguments</label>'; 

         for(var count2 = 0; count2 < abi[count1].inputs.length; count2++) 
         { 
          var inputElement = document.createElement("input"); 
          inputElement.setAttribute("type", "text"); 
          inputElement.setAttribute("class", "form-control"); 
          inputElement.setAttribute("placeholder", abi[count1].inputs[count2].type); 
          inputElement.setAttribute("id", "arguments-" + (count2 + 1)); 

          var br = document.createElement("br"); 

          document.getElementById("arguments").appendChild(br); 
          document.getElementById("arguments").appendChild(inputElement); 
         } 

         break; 
        } 
       } 

       break; 
      } 
     } 
 }; 

 xhttp.open("GET", "/compile?code=" + encodeURIComponent(document.getElementById("editor").value), true); 
 xhttp.send();  
}) 

document.getElementById("deploy").addEventListener("click", function(){ 
 editor.save(); 

 var arguments = []; 

 for(var count = 1; count <= argumentsCount; count++) 
 { 
  arguments[count - 1] = JSON.parse(document.getElementById("arguments-" + count).value);  
 } 

 var xhttp = new XMLHttpRequest(); 

 xhttp.onreadystatechange = function() { 
     if (this.readyState == 4 && this.status == 200)  
     { 
      var res = JSON.parse(xhttp.responseText); 

      if(res.error) 
      { 
       alert("Error: " + res.error) 
      } 
      else 
      { 
       alert("Txn Hash: " + res.result.hash); 
      }  
     } 
     else if(this.readyState == 4) 
     { 
      alert("An error occured."); 
     } 
 }; 

 xhttp.open("GET", "/deploy?code=" + encodeURIComponent(document.getElementById("editor").value) + "&arguments=" + encodeURIComponent(JSON.stringify(arguments)) + "&address=" + document.getElementById("address").value + "&key=" + document.getElementById("key").value, true); 
 xhttp.send();  
}) 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100

以下是前面代码的工作方式:

  1. 首先,我们将代码编辑器添加到网页中。代码编辑器将显示在textarea的位置,而textarea将被隐藏。

  2. 然后我们有编译按钮的点击事件处理程序。在其中,我们保存编辑器,将编辑器的内容复制到textarea中。当单击编译按钮时,我们向/compile路径发出请求,一旦获得结果,我们解析它并显示输入框,以便用户可以输入构造函数参数。在这里,我们只读取第一个合约的构造函数参数。但是,如果有多个合约,则可以增强 UI 以显示所有合约的构造函数的输入框。

  3. 最后,我们有部署按钮的点击事件处理程序。在这里,我们读取构造函数参数的值,解析并将它们放入一个数组中。然后,我们通过传递地址、密钥、代码和参数值向/deploy端点添加一个请求。如果有错误,则我们在弹出窗口中显示错误;否则,我们在弹出窗口中显示交易哈希。

测试

要测试应用程序,请在Initial目录内运行app.js节点,并访问localhost:8080。您将看到以下截图中显示的内容:

现在输入一些 Solidity 合约代码并按编译按钮。然后,您将能够看到新的输入框出现在右侧。例如,看一下以下截图:

现在输入一个有效地址及其关联的私钥。然后输入构造函数参数的值并点击部署。如果一切正常,你将看到一个带有交易哈希的警报框。例如,看一下下面的屏幕截图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

总结

在本章中,我们学习了如何使用交易池 API,如何计算正确的 nonce,计算可支出的余额,生成交易的数据,编译合约等等。然后我们构建了一个完整的合同编译和部署平台。现在你可以继续增强我们构建的应用程序,部署编辑器中找到的所有合同,处理导入,添加库等等。

在下一章中,我们将通过构建一个去中心化的赌博应用程序来学习 Oraclize。

第七章:构建一个投丨注应用

有时,智能合约需要访问其他 DApp 或来自万维网的数据是必要的。但是由于技术和共识方面的挑战,让智能合约访问外部数据确实非常复杂。因此,目前,以太坊智能合约没有原生支持访问外部数据。但是有第三方解决方案供以太坊智能合约访问一些流行的 DApp 和来自万维网的数据。在本章中,我们将学习如何使用 Oraclize 从以太坊智能合约中发出 HTTP 请求,以访问来自万维网的数据。我们还将学习如何访问存储在 IPFS 中的文件,使用 strings 库来处理字符串等等。我们将通过构建一个足球投丨注智能合约和其客户端来学习所有这些。

在本章中,我们将涵盖以下主题:

  • Oraclize 是如何工作的?

  • Oraclize 有哪些不同的数据源,它们每一个是如何工作的?

  • Oraclize 中的共识是如何工作的?

  • 将 Oraclize 集成到以太坊智能合约中

  • 使用 Solidity 库中的字符串库使字符串处理变得更加简单

  • 构建一个足球投丨注应用

介绍 Oraclize

Oraclize 是一个旨在使智能合约能够从其他区块链和万维网获取数据的服务。该服务目前在比特币和以太坊的测试网和主网上运行。Oraclize 的特殊之处在于,你无需信任它,因为它提供给智能合约的所有数据都有真实性证明。

在本章中,我们的目标是学习以太坊智能合约如何使用 Oraclize 服务从万维网获取数据。

它是如何工作的?

让我们看看以太坊智能合约如何使用 Oraclize 从其他区块链和万维网获取数据的过程。

要获取存在于以太坊区块链之外的数据,以太坊智能合约需要向 Oraclize 发送查询,提及数据源(表示从何处获取数据)和数据源的输入(表示要获取的内容)。

发送查询到 Oraclize 意味着向存在于以太坊区块链中的 Oraclize 合约发送合约调用(即,内部交易)。

Oraclize 服务器不断查找其智能合约的新进入查询。每当它看到一个新的查询时,它就会获取结果并通过调用您合约的 _callback 方法将其发送回您的合约。

数据源

以下是 Oraclize 允许智能合约获取数据的源列表:

  • URL: URL 数据源使您能够进行 HTTP GET 或 POST 请求,即从万维网获取数据。

  • WolframAlpha: WolframAlpha 数据源使您能够向 WolframAlpha 知识引擎提交查询并获得答案。

  • Blockchainblockchain 数据源提供了从其他区块链访问数据的能力。可以提交给blockchain数据源的可能查询包括bitcoin blockchain heightlitecoin hashratebitcoin difficulty1NPFRDJuEdyqEn2nmLNaWMfojNksFjbL4S balance等。

  • IPFSIPFS 数据源提供了获取存储在IPFS中文件内容的能力。

  • Nestednested 数据源是一个元数据源;它不提供对额外服务的访问。它的设计是为了提供一种简单的聚合逻辑,使单个查询能够利用基于任何可用数据源的子查询,并产生单个字符串作为结果;例如:

[WolframAlpha] ${[IPFS] QmP2ZkdsJG7LTw7jBbizTTgY1ZBeen64PqMgCAWz2koJBL}中的温度。

  • Computationcomputation 数据源使给定应用程序在安全的链下环境中可审计地执行;也就是说,它让我们获取应用程序链下执行的结果。这个应用程序必须在退出前在最后一行(标准输出上)打印查询结果。执行环境必须由 Dockerfile 描述,其中构建和运行应用程序应立即启动您的主应用程序。Dockerfile 初始化加上您的应用程序执行应尽快终止:在AWS t2.micro实例上的最大执行超时为 5 分钟。在这里,我们考虑的是AWS t2.micro实例,因为这是 Oraclize 将用来执行该应用程序的实例。由于数据源的输入是包含这些文件的 ZIP 存档的 IPFS 多哈希值(Dockerfile 加上任何外部文件依赖项,且 Dockerfile 必须放在存档根目录中),您应该注意准备这个存档并预先将其推送到 IPFS。

这些数据源在编写本书时可用。但未来可能会有更多的数据源可用。

真实性证明

虽然 Oraclize 是一个值得信赖的服务,但你可能仍然想要检查 Oraclize 返回的数据是否真实,即它是否在传输过程中被 Oraclize 或其他人篡改。

可选地,Oraclize 提供了从 URL、区块链、嵌套和计算数据源返回的 TLSNotary 结果证明。对于WolframAlphaIPFS数据源,这种证明是不可用的。目前,Oraclize 仅支持 TLSNotary 证明,但在将来,他们可能会支持其他一些身份验证方式。目前,TLSNotary 证明需要手动验证,但 Oraclize 已经在进行链上证明验证;也就是说,你的智能合约代码可以在从 Oraclize 接收数据时自行验证 TLSNotary 证明,以便在证明无效时丢弃这些数据。

这个工具(github.com/Oraclize/proof-verification-tool)是由 Oraclize 提供的开源工具,用于验证 TLSNotary 证明,如果你想要的话。

理解 TLSNotary 如何工作并不是使用 Oraclize 或验证证明所必需的。验证 TLSNotary 证明的工具是开源的;因此,如果它包含任何恶意代码,那么它很容易被发现,因此这个工具是可信的。

让我们来看看 TLSNotary 如何工作的概述。要理解 TLSNotary 的工作原理,首先需要了解 TLS 的工作原理。TLS 协议提供了一种方法,让客户端和服务器创建一个加密会话,这样其他人就无法阅读或篡改客户端和服务器之间传输的内容。服务器首先将其证书(由受信任的 CA 颁发给域所有者)发送给客户端。证书将包含服务器的公钥。客户端使用 CA 的公钥解密证书,以便可以验证证书实际上是由 CA 颁发的,并获取服务器的公钥。然后,客户端生成一个对称加密密钥和一个 MAC 密钥,并使用服务器的公钥对它们进行加密,然后将其发送给服务器。服务器只能解密此消息,因为它有解密它的私钥。现在客户端和服务器共享相同的对称和 MAC 密钥,除了它们以外,没有其他人知道这些密钥,它们可以开始相互发送和接收数据。对称密钥用于加密和解密数据,其中 MAC 密钥和对称密钥一起用于为加密消息生成签名,以便在消息被攻击者修改时,另一方可以知道它。

TLSNotary 是 TLS 的修改版,Oraclize 使用它来提供密码学证明,证明它们提供给您的智能合约的数据确实是数据源在特定时间提供给 Oraclize 的数据。实际上,TLSNotary 协议是 PageSigner 项目开发和使用的开源技术。

TLSNotary 的工作原理是将对称密钥和 MAC 密钥分配给三个参与方,即服务器、被审计者和审计员。TLSNotary 的基本思想是被审计者可以向审计员证明特定结果是服务器在特定时间返回的。

下面是 TLSNotary 如何实现这一点的概述。审计员计算对称密钥和 MAC 密钥,然后仅将对称密钥提供给受审者。由于 MAC 签名检查确保了服务器传输的 TLS 数据未被修改,因此受审者不需要 MAC 密钥。有了对称加密密钥,受审者现在可以解密服务器的数据。因为所有消息都是由银行使用 MAC 密钥“签名”的,而且只有服务器和审计员知道 MAC 密钥,正确的 MAC 签名可以作为证明某些消息来自银行且未被受审者伪造的证据。

在 Oraclize 服务的情况下,Oraclize 是受审者,而一个特制的、开源的 Amazon 机器映像的受限 AWS 实例充当审计员。

他们提供的证据数据是此 AWS 实例的签名证明,证明了一个正确的 TLSnotary 证明确实发生了。他们还提供了一些关于在 AWS 实例中运行的软件的额外证据,即它是否自初始化以来已被修改。

定价

任何以太坊地址发起的第一个 Oraclize 查询调用完全免费。在测试网络上使用 Oraclize 调用是免费的!这仅适用于测试环境中的适度使用。

从第二次调用开始,你必须支付以太币来进行查询。当向 Oraclize 发送查询(即进行内部交易调用)时,通过将以太币从调用合约转移到 Oraclize 合约来扣除费用。要扣除的以太币数量取决于数据源和证明类型。

下表显示了发送查询时扣除的以太币数量:

数据源无证明使用 TLSNotary 证明
URL$0.01$0.05
区块链$0.01$0.05
WolframAlpha$0.03$0.03
IPFS$0.01$0.01

如果你正在发起一个 HTTP 请求,并且想要 TLSNotary 证明,那么调用的合约必须有价值$0.05 的以太币;否则,将抛出异常。

使用 Oraclize API 入门

要使合约使用 Oraclize 服务,需要继承usingOraclize合约。你可以在github.com/oraclize/ethereum-api找到这个合约。

usingOraclize合约充当OraclizeIOraclizeAddrResolverI合约的代理。实际上,usingOraclize使调用OraclizeIOraclizeAddrResolverI合约变得容易,也就是说,它提供了更简单的 API。如果你感觉自如,你也可以直接调用OraclizeIOraclizeAddrResolverI合约。你可以查看这些合约的源代码,找到所有可用的 API。我们只会学习最必要的 API。

让我们看看如何设置证明类型、设置证明存储位置、进行查询、找到查询的成本等等。

设置证明类型和存储位置

无论你是否需要来自 Oraclize 的 TLSNotary 证明,你都必须在发出查询之前指定证明类型和证明存储位置。

如果你不需要证明,那么将这段代码放入你的合约中:

oraclize_setProof(proofType_NONE)

  • 1
  • 2

如果你需要证明,那么将这段代码放入你的合约中:

oraclize_setProof(proofType_TLSNotary | proofStorage_IPFS)

  • 1
  • 2

目前,proofStorage_IPFS是唯一可用的证明存储位置;也就是说,TLSNotary 证明只存储在IPFS中。

你可以仅执行这些方法之一,例如,在构造函数中或在其他任何时候,例如,如果你只需要某些查询的证明。

发送查询

要向 Oraclize 发送查询,你需要调用oraclize_query函数。该函数至少需要两个参数,即数据源和给定数据源的输入。数据源参数不区分大小写。

下面是oraclize_query函数的一些基本示例:

oraclize_query("WolframAlpha", "random number between 0 and 100"); 

oraclize_query("URL", "https://api.kraken.com/0/public/Ticker?pair=ETHXBT"); 

oraclize_query("IPFS", "QmdEJwJG1T9rzHvBD8i69HHuJaRgXRKEQCP7Bh1BVttZbU"); 

oraclize_query("URL", "https://xyz.io/makePayment", '{"currency": "USD", "amount": "1"}');

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

以下是上述代码的工作原理:

  • 如果第一个参数是一个字符串,则假定它是数据源,第二个参数则假定是数据源的输入。在第一次调用中,数据源是WolframAlpha,我们发送给它的搜索查询是random number between 0 and 100

  • 在第二次调用中,我们向第二个参数中的URL发出一个HTTP GET请求。

  • 在第三次调用中,我们从IPFS中获取QmdEJwJG1T9rzHvBD8i69HHuJaRgXRKEQCP7Bh1BVttZbU文件的内容。

  • 如果数据源之后的两个连续参数都是字符串,则假定它是一个 POST 请求。在最后一次调用中,我们向https://xyz.io/makePayment发出一个HTTP POST请求,而POST请求主体内容是第三个参数中的字符串。Oraclize 能够根据字符串格式检测到内容类型头部。

定时查询

如果你希望 Oraclize 在将来的某个预定时间执行你的查询,只需将延迟时间(以秒为单位)从当前时间作为第一个参数指定。

这是一个例子:

oraclize_query(60, "WolframAlpha", "random number between 0 and 100");

  • 1
  • 2

上述查询将在被看到后的 60 秒内由 Oraclize 执行。因此,如果第一个参数是一个数字,则假定我们正在安排一个查询。

自定义燃气

从 Oraclize 到你的__callback函数的交易需要燃气,就像任何其他交易一样。你需要支付给 Oraclize 燃气成本。oraclize_query 收取用于发出查询的以太币,同时也用于在调用__callback函数时提供燃气。默认情况下,Oraclize 在调用__callback函数时提供 200,000 gas。

返回的燃气成本实际上是由你控制的,因为你在__callback方法中编写代码,因此可以估算出来。因此,当向 Oraclize 发出查询时,你还可以指定__callback交易的gasLimit应该是多少。然而,请注意,由于 Oraclize 发送交易,任何未使用的 gas 都会退还给 Oraclize,而不是你。

如果默认值和最小值的 200,000 gas 不够用,你可以通过以下方式增加它:

oraclize_query("WolframAlpha", "random number between 0 and 100", 500000);

  • 1
  • 2

在这里,你可以看到如果最后一个参数是一个数字,那么就假设它是自定义 gas。在前面的代码中,Oraclize 将为回调交易使用 500k 的 gasLimit 而不是 200k。因为我们要求 Oraclize 提供更多的 gas,所以在调用 oraclize_query 时,Oraclize 将扣除更多的以太币(取决于需要多少 gas)。

请注意,如果你提供的 gasLimit 太低,并且你的 __callback 方法很长,你可能永远不会看到回调。另请注意,自定义 gas 必须大于 200k。

回调函数

一旦你的结果准备好了,Oraclize 将会发送一个交易回到你的合约地址并调用以下三种方法之一:

  • 或者 __callback(bytes32 myid, string result)Myid 是每个查询的唯一 ID。这个 ID 是由 oraclize_query 方法返回的。如果你的合约中有多个 oraclize_query 调用,那么这个 ID 就用于匹配此结果所属的查询。

  • 如果你请求了 TLS Notary 证明,这就是结果:__callback(bytes32 myid, string result, bytes proof)

  • 作为最后的手段,如果其他方法都不存在,回退函数是 function()

这是 __callback 函数的一个例子:

function __callback(bytes32 myid, string result) { 
    if (msg.sender != oraclize_cbAddress()) throw; // just to be sure the calling address is the Oraclize authorized one 

    //now doing something with the result.. 
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

解析辅助函数

从 HTTP 请求返回的结果可以是 HTML、JSON、XML、二进制等。在 Solidity 中,解析结果是困难且昂贵的。因此,Oraclize 提供了解析辅助函数,让它在其服务器上处理解析,并且你只获取你需要的结果的一部分。

要求 Oraclize 解析结果,你需要使用以下解析辅助函数包装 URL:

  • xml(..)json(..) 辅助函数让你可以要求 Oraclize 只返回 JSON 或 XML 解析后的响应的一部分;例如,看一下以下示例:

    • 为了获取整个响应,你可以使用 URL 数据源和 api.kraken.com/0/public/Ticker?pair=ETHUSD URL 参数

    • 如果你只想获取最后价格字段,你需要使用 JSON 解析调用,如 json(api.kraken.com/0/public/Ticker?pair=ETHUSD).result.XETHZUSD.c.0

  • html(..).xpath(..) 辅助函数对于 HTML 抓取非常有用。只需将你想要的 XPATH 指定为 xpath(..) 参数;例如,看一下以下示例:

    • 要获取特定推文的文本,使用 html(https://twitter.com/oraclizeit/status/671316655893561344).xpath(//*[contains(@class, 'tweet-text')]/text())
  • binary(..) 辅助函数对于获取二进制文件(例如证书文件)非常有用:

    • 要仅获取二进制文件的一部分,你可以使用 slice(offset,length);第一个参数是偏移量,而第二个参数是你希望返回的切片的长度(都以字节为单位)。

    • 示例:仅从二进制 CRL 中获取前 300 个字节,binary(https://www.sk.ee/crls/esteid/esteid2015.crl).slice(0,300)。二进制辅助程序必须与切片选项一起使用,只接受二进制文件(未编码)。

如果服务器无响应或无法访问,我们将发送空响应。您可以使用app.Oraclize.it/home/test_query测试查询。

获取查询价格

如果您想在实际查询之前知道查询的费用,那么您可以使用Oraclize.getPrice()函数来获取所需的 wei 数量。它接受的第一个参数是数据源,第二个参数是可选的,即自定义的 gas。

这个的一个流行用例是通知客户如果合同中没有足够的以太币来进行查询,则添加以太币。

加密查询

有时,您可能不希望揭示数据源和/或数据源的输入。例如:如果有的话,您可能不希望在 URL 中揭示 API 密钥。因此,Oraclize 提供了一种将查询加密存储在智能合约中,并且只有 Oraclize 的服务器有解密密钥的方法。

Oraclize 提供了一个 Python 工具(github.com/Oraclize/encrypted-queries),可用于加密数据源和/或数据输入。它生成一个非确定性的加密字符串。

加密任意文本字符串的 CLI 命令如下:

    python encrypted_queries_tools.py -e -p 044992e9473b7d90ca54d2886c7addd14a61109af202f1c95e218b0c99eb060c7134c4ae46345d0383ac996185762f04997d6fd6c393c86e4325c469741e64eca9 "YOUR DATASOURCE or INPUT"

  • 1
  • 2

您看到的长十六进制字符串是 Oraclize 服务器的公钥。现在您可以使用前述命令的输出来替代数据源和/或数据源的输入。

为了防止加密查询的滥用(即重放攻击),使用特定加密查询的第一个与 Oraclize 查询的合同将成为其合法所有者。任何其他重用完全相同字符串的合同将不被允许使用它,并将收到空结果。因此,请记住在重新部署使用加密查询的合同时始终生成新的加密字符串。

解密数据源

还有另一个名为解密的数据源。它用于解密加密的字符串。但是这个数据源不返回任何结果;否则,任何人都将有能力解密数据源和数据源的输入。

它专门设计用于在嵌套数据源内部使用,以实现部分查询加密。这是它唯一的用例。

Oraclize Web IDE

Oraclize 提供了一个 Web IDE,您可以在其中编写、编译和测试基于 Oraclize 的应用程序。您可以在dapps.Oraclize.it/browser-Solidity/找到它。

如果您访问链接,那么您会注意到它看起来与浏览器 Solidity 完全相同。实际上,它就是带有一个额外功能的浏览器 Solidity。要理解这个功能是什么,我们需要更深入地了解浏览器 Solidity。

浏览器 Solidity 不仅让我们编写、编译和为我们的合约生成 web3.js 代码,还可以在那里测试这些合约。直到现在,为了测试我们的合约,我们都是设置了以太坊节点并向其发送交易。但是浏览器 Solidity 可以在不连接到任何节点的情况下执行合约,所有操作都在内存中进行。它使用 ethereumjs-vm 实现了这一点,这是 EVM 的 JavaScript 实现。使用 ethereumjs-vm,你可以创建自己的 EVM 并运行字节码。如果需要,我们可以通过提供连接的 URL 来配置浏览器 Solidity 使用以太坊节点。用户界面非常信息丰富;因此,你可以自己尝试所有这些。

Oraclize Web IDE 的特殊之处在于它在内存执行环境中部署了 Oraclize 合约,因此您不必连接到测试网或主网节点,但如果您使用浏览器 Solidity,则必须连接到测试网或主网节点以测试 Oraclize API。

你可以在 dev.Oraclize.it/ 找到更多与集成 Oraclize 相关的资源。

处理字符串

在 Solidity 中处理字符串不像在其他高级编程语言(如 JavaScript、Python 等)中那样容易。因此,许多 Solidity 程序员提出了各种库和合约,以便轻松处理字符串。

strings 库是最流行的字符串实用程序库。它允许我们通过将字符串转换为称为切片的东西来连接、连接、拆分、比较等等。切片是一个结构,它保存了字符串的长度和字符串的地址。由于切片只需指定一个偏移量和一个长度,复制和操作切片要比复制和操作它们引用的字符串便宜得多。

为了进一步减少 gas 成本,大多数需要返回切片的切片函数会修改原始切片,而不是分配一个新的切片;例如,s.split(".") 将返回直到第一个 "." 的文本,并修改 s 以仅包含该 "." 后的字符串。在你不想修改原始切片的情况下,可以使用 .copy() 进行复制,例如,s.copy().split(".")。尽量避免在循环中使用这种习惯用法;由于 Solidity 没有内存管理,这将导致分配许多短暂的切片,稍后会被丢弃。

必须复制字符串数据的函数将返回字符串而不是切片;如果需要,可以将其强制转换回切片以进行进一步处理。

让我们看看使用 strings 库处理字符串的几个示例:

pragma Solidity ⁰.4.0; 

import "github.com/Arachnid/Solidity-stringutils/strings.sol"; 

contract Contract 
{ 
    using strings for *; 

    function Contract() 
    { 
        //convert string to slice 
        var slice = "xyz abc".toSlice(); 

        //length of string 
        var length = slice.len(); 

        //split a string 
        //subslice = xyz 
        //slice = abc 
        var subslice = slice.split(" ".toSlice()); 

        //split a string into an array 
        var s = "www.google.com".toSlice(); 
        var delim = ".".toSlice(); 
        var parts = new string[](s.count(delim)); 
        for(uint i = 0; i < parts.length; i++) { 
            parts[i] = s.split(delim).toString(); 
        } 

        //Converting a slice back to a string 
        var myString = slice.toString(); 

        //Concatenating strings 
        var finalSlice = subslice.concat(slice); 

        //check if two strings are equal 
        if(slice.equals(subslice)) 
        { 

        } 
    } 
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

前述代码是不言自明的。

返回两个切片的函数有两个版本:一个是不分配的版本,它将第二个切片作为参数,直接在原地修改它;另一个是分配并返回第二个切片的版本。例如,让我们来看看以下内容:

var slice1 = "abc".toSlice(); 

//moves the string pointer of slice1 to point to the next rune (letter) 
//and returns a slice containing only the first rune 
var slice2 = slice1.nextRune(); 

var slice3 = "abc".toSlice(); 
var slice4 = "".toSlice(); 

//Extracts the first rune from slice3 into slice4, advancing the slice to point to the next rune and returns slice4\. 
var slice5 = slice3.nextRune(slice4);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

您可以在github.com/Arachnid/Solidity-stringutils了解更多关于 strings 库的信息。

构建投丨注合同

在我们的投丨注应用程序中,两个人可以选择在一场足球比赛上进行投丨注,一个人支持主队,另一个人支持客队。他们都应该以相同的金额进行投丨注,赢家将拿走所有的钱。如果比赛是平局,那么他们两人都将拿回他们的钱。

我们将使用 FastestLiveScores API 来了解比赛的结果。它提供了一个免费的 API,让我们每小时可以免费进行 100 次请求。首先,去创建一个账户,然后生成一个 API 密钥。要创建一个账户,请访问 customer.fastestlivescores.com/register,一旦账户创建完成,您将在 customer.fastestlivescores.com/ 看到 API 密钥。您可以在 docs.crowdscores.com/ 找到 API 文档。

对于我们应用程序中两个人之间的每次投丨注,都会部署一个投丨注合同。合同将包含从 FastestLiveScores API 检索到的比赛 ID,双方需要投资的 wei 金额以及双方的地址。一旦双方都在合同中投资了,他们将得知比赛的结果。如果比赛尚未结束,则他们将在每隔 24 小时后尝试检查结果。

这是合同的代码:

pragma Solidity ⁰.4.0; 

import "github.com/Oraclize/Ethereum-api/oraclizeAPI.sol"; 
import "github.com/Arachnid/Solidity-stringutils/strings.sol"; 

contract Betting is usingOraclize 
{ 
    using strings for *; 

    string public matchId; 
    uint public amount; 
    string public url; 

    address public homeBet; 
    address public awayBet; 

    function Betting(string _matchId, uint _amount, string _url)  
    { 
        matchId = _matchId; 
        amount = _amount; 
        url = _url; 

        oraclize_setProof(proofType_TLSNotary | proofStorage_IPFS); 
    } 

    //1 indicates home team 
    //2 indicates away team 
    function betOnTeam(uint team) payable 
    { 

        if(team == 1) 
        { 
            if(homeBet == 0) 
            { 
                if(msg.value == amount) 
                { 
                    homeBet = msg.sender;    
                    if(homeBet != 0 && awayBet != 0) 
                    { 
                        oraclize_query("URL", url); 
                    } 
                } 
                else 
                { 
                    throw; 
                } 
            } 
            else 
            { 
                throw; 
            } 
        } 
        else if(team == 2) 
        { 
            if(awayBet == 0) 
            { 
                if(msg.value == amount) 
                { 
                    awayBet = msg.sender;           

                    if(homeBet != 0 && awayBet != 0) 
                    { 
                        oraclize_query("URL", url); 
                    } 
                } 
                else 
                { 
                    throw; 
                } 
            } 
            else 
            { 
                throw; 
            } 
        } 
        else 
        { 
            throw; 
        } 
    } 

    function __callback(bytes32 myid, string result, bytes proof) { 
        if (msg.sender != oraclize_cbAddress()) 
        { 
            throw;     
        } 
        else 
        { 
            if(result.toSlice().equals("home".toSlice())) 
            { 
                homeBet.send(this.balance); 
            } 
            else if(result.toSlice().equals("away".toSlice())) 
            { 
                awayBet.send(this.balance); 
            } 
            else if(result.toSlice().equals("draw".toSlice())) 
            { 
                homeBet.send(this.balance / 2); 
                awayBet.send(this.balance / 2); 
            } 
            else 
            { 
                if (Oraclize.getPrice("URL") < this.balance)  
                { 
                    oraclize_query(86400, "URL", url); 
                } 
            } 
        } 
    } 
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112

合同代码是自说明的。现在使用 solc.js 或浏览器 Solidity 编译上述代码,具体取决于您自己的喜好。您不需要链接 strings 库,因为其中的所有函数都设置为internal可见性。

在浏览器 Solidity 中,当指定从 HTTP URL 导入库或合同时,请确保它是托管在 GitHub 上;否则,它将无法获取。在 GitHub 文件 URL 中,请确保删除协议以及 blob/{branch-name}

为投丨注合同构建客户端

为了方便查找匹配的 ID、部署和投资于合同,我们需要构建一个 UI 客户端。所以让我们开始构建一个客户端,它将有两条路径,即主页路径用于部署合同和投丨注比赛,另一条路径用于查找比赛列表。我们将允许用户使用他们自己的离线账户进行部署和投丨注,以便整个投丨注过程以分散的方式进行,没有人能作弊。

在我们开始构建客户端之前,请确保您已经同步了测试网络,因为 Oraclize 仅在以太坊的测试网络/主网络上运行,而不在私有网络上运行。您可以切换到测试网络,并通过将--dev选项替换为--testnet选项来开始下载测试网络区块链。例如,看一下以下内容:

geth --testnet --rpc --rpccorsdomain "*" --rpcaddr "0.0.0.0" --rpcport "8545"

  • 1
  • 2

规划结构

在本章的练习文件中,您将找到两个目录,即 Final 和 Initial。Final 包含项目的最终源代码,而 Initial 包含空的源代码文件和库,以快速开始构建应用程序。

要测试Final目录,您需要在其中运行npm install,然后使用Final目录内的node app.js命令运行应用程序。

Initial目录中,您将找到一个public目录和两个名为app.jspackage.json的文件。package.json文件包含我们应用程序的后端依赖关系,app.js是您将放置后端源代码的地方。

public目录包含与前端相关的文件。在public/css内,您将找到bootstrap.min.css,这是 bootstrap 库。在public/html内,您将找到index.htmlmatches.ejs文件,您将在其中放置我们应用程序的 HTML 代码,并且在public/js目录内,您将找到 web3.js 和 ethereumjs-tx 的 js 文件。在public/js内,您还会找到一个main.js文件,您将在其中放置我们应用程序的前端 JS 代码。您还将在 Oraclize Python 工具中找到加密查询的内容。

构建后端

首先构建应用程序的后端。首先,在 Initial 目录内运行npm install以安装后端所需的依赖。

这是运行 express 服务并提供index.html文件和静态文件并设置视图引擎的后端代码:

var express = require("express"); 
var app = express(); 

app.set("view engine", "ejs"); 

app.use(express.static("public")); 

app.listen(8080); 

app.get("/", function(req, res) { 
    res.sendFile(__dirname + "/public/html/index.html"); 
})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

上述代码是不言自明的。现在让我们继续进行。我们的应用程序将有另一个页面,其中将显示最近的比赛列表,包括比赛的 ID 和结果(如果比赛已结束)。以下是端点的代码:

var request = require("request"); 
var moment = require("moment"); 

app.get("/matches", function(req, res) { 
    request("https://api.crowdscores.com/v1/matches?api_key=7b7a988932de4eaab4ed1b4dcdc1a82a", function(error, response, body) { 
        if (!error && response.statusCode == 200) { 
            body = JSON.parse(body); 

            for (var i = 0; i < body.length; i++) { 
             body[i].start = moment.unix(body[i].start / 
               1000).format("YYYY MMM DD hh:mm:ss"); 
            } 

            res.render(__dirname + "/public/html/matches.ejs", { 
                matches: body 
            }); 
        } else { 
            res.send("An error occured"); 
        } 
    }) 
})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在这里,我们正在进行 API 请求以获取最近比赛的列表,然后将结果传递给matches.ejs文件,以便它可以在用户友好的 UI 中渲染结果。API 结果以时间戳形式给出比赛开始时间;因此,我们正在使用 moment 将其转换为可读的人类格式。我们从后端而不是从前端发出此请求,以便我们不会向用户公开 API 密钥。

我们的后端将向前端提供 API,通过该 API 前端可以在部署合约之前加密查询。我们的应用程序不会提示用户创建 API 密钥,因为这将是不良的 UX 实践。应用程序的开发人员控制 API 密钥不会造成任何伤害,因为开发人员无法修改 API 服务器的结果;因此,即使应用程序的开发人员知道 API 密钥,用户仍将信任该应用程序。

下面是加密端点的代码:

var PythonShell = require("python-shell"); 

app.get("/getURL", function(req, res) { 
    var matchId = req.query.matchId; 

    var options = { 
        args: ["-e", "-p", "044992e9473b7d90ca54d2886c7addd14a61109af202f1c95e218b0c99eb060c7134c4ae46345d0383ac996185762f04997d6fd6c393c86e4325c469741e64eca9", "json(https://api.crowdscores.com/v1/matches/" + matchId + "?api_key=7b7a988932de4eaab4ed1b4dcdc1a82a).outcome.winner"], 
        scriptPath: __dirname 
    }; 

    PythonShell.run("encrypted_queries_tools.py", options, function 
      (err, results) { 
        if(err) 
        { 
            res.send("An error occured"); 
        } 
        else 
        { 
            res.send(results[0]); 
        } 
    }); 
})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

我们已经看到如何使用此工具。为了成功运行此端点,请确保在您的系统上安装了 Python。即使安装了 Python,此端点也可能显示错误,指示未安装 Python 的密码学和 base58 模块。因此,如果工具提示您安装这些模块,请确保您安装了这些模块。

构建前端

现在让我们构建应用程序的前端。我们的前端将让用户看到最近比赛的列表,部署投丨注合约,对游戏进行投丨注,并让他们看到有关投丨注合约的信息。

让我们首先实现matches.ejs文件,该文件将显示最近比赛的列表。这是这个文件的代码:

<!DOCTYPE html>
<html lang="en">
    <head> 
         <meta charset="utf-8"> 
         <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 
         <meta http-equiv="x-ua-compatible" content="ie=edge"> 
         <link rel="stylesheet" href="/css/bootstrap.min.css"> 
     </head> 
     <body> 
         <div class="container"> 
             <br> 
             <div class="row m-t-1"> 
                 <div class="col-md-12"> 
                     <a href="/">Home</a> 
                 </div> 
             </div> 
             <br> 
             <div class="row"> 
                 <div class="col-md-12"> 
                     <table class="table table-inverse"> 
                           <thead> 
                             <tr> 
                                 <th>Match ID</th> 
                                 <th>Start Time</th> 
                                 <th>Home Team</th> 
                                 <th>Away Team</th> 
                                 <th>Winner</th> 
                             </tr> 
                           </thead> 
                           <tbody> 
                               <% for(var i=0; i < matches.length; i++) { %> 
                                   <tr> 
                                       <td><%= matches[i].dbid %></td> 
                                       <% if (matches[i].start) { %> 
                                        <td><%= matches[i].start %></td> 
                                     <% } else { %> 
                                         <td>Time not finalized</td> 
                                     <% } %> 
                                       <td><%= matches[i].homeTeam.name %></td> 
                                       <td><%= matches[i].awayTeam.name %></td> 
                                       <% if (matches[i].outcome) { %> 
                                        <td><%= matches[i].outcome.winner %></td> 
                                     <% } else { %> 
                                         <td>Match not finished</td> 
                                     <% } %> 
                                 </tr> 
                             <% } %> 
                           </tbody> 
                     </table> 
                 </div> 
             </div> 
         </div> 
     </body> 
 </html>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

上述代码是不言自明的。现在让我们为我们的首页编写 HTML 代码。我们的首页将显示三个表单。第一个表单用于部署一个投丨注合约,第二个表单用于投资一个投丨注合约,第三个表单用于显示已部署投丨注合约的信息。

这是首页的 HTML 代码。将此代码放在index.html页面中:

<!DOCTYPE html> 
 <html lang="en"> 
     <head> 
         <meta charset="utf-8"> 
         <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 
         <meta http-equiv="x-ua-compatible" content="ie=edge"> 
         <link rel="stylesheet" href="/css/bootstrap.min.css"> 
     </head> 
     <body> 
         <div class="container"> 
             <br> 
             <div class="row m-t-1"> 
                 <div class="col-md-12"> 
                     <a href="/matches">Matches</a> 
                 </div> 
             </div> 
             <br> 
             <div class="row"> 
                 <div class="col-md-4"> 
                     <h3>Deploy betting contract</h3> 
                     <form id="deploy"> 
                         <div class="form-group"> 
                             <label>From address: </label> 
                             <input type="text" class="form-control" id="fromAddress"> 
                         </div> 
                         <div class="form-group"> 
                             <label>Private Key: </label> 
                             <input type="text" class="form-control" id="privateKey"> 
                         </div> 
                         <div class="form-group"> 
                             <label>Match ID: </label> 
                             <input type="text" class="form-control" id="matchId"> 
                         </div> 
                         <div class="form-group"> 
                             <label>Bet Amount (in ether): </label> 
                             <input type="text" class="form-control" id="betAmount"> 
                         </div> 
                         <p id="message" style="word-wrap: break-word"></p> 
                         <input type="submit" value="Deploy" class="btn btn-primary" /> 
                     </form> 
                 </div> 
                 <div class="col-md-4"> 
                     <h3>Bet on a contract</h3> 
                     <form id="bet"> 
                         <div class="form-group"> 
                             <label>From address: </label> 
                             <input type="text" class="form-control" id="fromAddress"> 
                         </div> 
                         <div class="form-group"> 
                             <label>Private Key: </label> 
                             <input type="text" class="form-control" id="privateKey"> 
                         </div> 
                         <div class="form-group"> 
                             <label>Contract Address: </label> 
                             <input type="text" class="form-control"
id="contractAddress"> 
                         </div> 
                         <div class="form-group"> 
                             <label>Team: </label> 
                             <select class="form-control" id="team"> 
                                 <option>Home</option> 
                                 <option>Away</option> 
                             </select> 
                         </div> 
                         <p id="message" style="word-wrap: break-word"></p> 
                         <input type="submit" value="Bet" class="btn btn-primary" /> 
                     </form> 
                 </div> 
                 <div class="col-md-4"> 
                     <h3>Display betting contract</h3> 
                     <form id="find"> 
                         <div class="form-group"> 
                             <label>Contract Address: </label> 
                             <input type="text" class="form-control"  
 d="contractAddress"> 
                         </div> 
                         <p id="message"></p> 
                         <input type="submit" value="Find" class="btn btn-primary" /> 
                     </form> 
                 </div> 
             </div> 
         </div> 

         <script type="text/javascript" src="img/web3.min.js"></script> 
         <script type="text/javascript" src="img/ethereumjs-tx.js"></script> 
         <script type="text/javascript" src="img/main.js"></script> 
     </body> 
</html>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89

上述代码是不言自明的。现在让我们编写 JavaScript 代码来实际部署合约,在合约上投资,并显示有关合约的信息。以下是所有这些的代码。将此代码放在main.js文件中:

var bettingContractByteCode = "6060604..."; 
var bettingContractABI = [{"constant":false,"inputs":[{"name":"team","type":"uint256"}],"name":"betOnTeam","outputs":[],"payable":true,"type":"function"},{"constant":false,"inputs":[{"name":"myid","type":"bytes32"},{"name":"result","type":"string"}],"name":"__callback","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"myid","type":"bytes32"},{"name":"result","type":"string"},{"name":"proof","type":"bytes"}],"name":"__callback","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"url","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"matchId","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"amount","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"homeBet","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"awayBet","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"inputs":[{"name":"_matchId","type":"string"},{"name":"_amount","type":"uint256"},{"name":"_url","type":"string"}],"payable":false,"type":"constructor"}]; 

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

function getAJAXObject() 
{ 
   var request; 
   if (window.XMLHttpRequest) { 
       request = new XMLHttpRequest(); 
   } else if (window.ActiveXObject) { 
       try { 
           request = new ActiveXObject("Msxml2.XMLHTTP"); 
       } catch (e) { 
           try { 
               request = new ActiveXObject("Microsoft.XMLHTTP"); 
           } catch (e) {} 
       } 
   } 

   return request; 
} 

document.getElementById("deploy").addEventListener("submit", function(e){ 
   e.preventDefault(); 

   var fromAddress = document.querySelector("#deploy #fromAddress").value; 
   var privateKey = document.querySelector("#deploy #privateKey").value; 
   var matchId = document.querySelector("#deploy #matchId").value; 
   var betAmount = document.querySelector("#deploy #betAmount").value; 

   var url = "/getURL?matchId=" + matchId; 

   var request = getAJAXObject(); 

   request.open("GET", url); 

   request.onreadystatechange = function() { 
       if (request.readyState == 4) { 
           if (request.status == 200) { 
               if(request.responseText != "An error occured") 
               { 
           var queryURL = request.responseText; 

           var contract = web3.eth.contract(bettingContractABI); 
           var data = contract.new.getData(matchId, 
             web3.toWei(betAmount, "ether"), queryURL, { 
               data: bettingContractByteCode 
                }); 

           var gasRequired = web3.eth.estimateGas({ data: "0x" + data
             }); 

      web3.eth.getTransactionCount(fromAddress, function(error, nonce){ 

       var rawTx = { 
            gasPrice: web3.toHex(web3.eth.gasPrice), 
             gasLimit: web3.toHex(gasRequired), 
              from: fromAddress, 
               nonce: web3.toHex(nonce), 
                data: "0x" + data, 
                 }; 

      privateKey = EthJS.Util.toBuffer(privateKey, "hex"); 

       var tx = new EthJS.Tx(rawTx); 
       tx.sign(privateKey); 

      web3.eth.sendRawTransaction("0x" + 
       tx.serialize().toString("hex"), function(err, hash) { 
            if(!err) 
                {document.querySelector("#deploy #message").
                   innerHTML = "Transaction Hash: " + hash + ". 
                     Transaction is mining..."; 

            var timer = window.setInterval(function(){ 
            web3.eth.getTransactionReceipt(hash, function(err, result){ 
            if(result) 
             {window.clearInterval(timer); 
       document.querySelector("#deploy #message").innerHTML = 
         "Transaction Hash: " + hash + " and contract address is: " + 
             result.contractAddress;} 
               }) 
                }, 10000) 
                 } 
             else 
           {document.querySelector("#deploy #message").innerHTML = err; 
             } 
           }); 
          }) 

          } 
           } 
       } 
   }; 

   request.send(null); 

}, false) 

document.getElementById("bet").addEventListener("submit", function(e){ 
   e.preventDefault(); 

   var fromAddress = document.querySelector("#bet #fromAddress").value; 
   var privateKey = document.querySelector("#bet #privateKey").value; 
   var contractAddress = document.querySelector("#bet #contractAddress").value; 
   var team = document.querySelector("#bet #team").value; 

   if(team == "Home") 
   { 
         team = 1; 
   } 
   else 
   { 
         team = 2; 
   }  

   var contract = web3.eth.contract(bettingContractABI).at(contractAddress); 
   var amount = contract.amount(); 

   var data = contract.betOnTeam.getData(team); 

   var gasRequired = contract.betOnTeam.estimateGas(team, { 
         from: fromAddress, 
         value: amount, 
         to: contractAddress 
   }) 

   web3.eth.getTransactionCount(fromAddress, function(error, nonce){ 

         var rawTx = { 
           gasPrice: web3.toHex(web3.eth.gasPrice), 
           gasLimit: web3.toHex(gasRequired), 
           from: fromAddress, 
           nonce: web3.toHex(nonce), 
           data: data, 
           to: contractAddress, 
           value: web3.toHex(amount) 
       }; 

       privateKey = EthJS.Util.toBuffer(privateKey, "hex"); 

       var tx = new EthJS.Tx(rawTx); 
         tx.sign(privateKey); 

         web3.eth.sendRawTransaction("0x" + tx.serialize().toString("hex"), function(err, hash) { 
               if(!err) 
               { 
    document.querySelector("#bet #message").innerHTML = "Transaction 
      Hash: " + hash; 
        } 
      else 
       { 
       document.querySelector("#bet #message").innerHTML = err; 
      } 
     }) 
   }) 
    }, false) 

document.getElementById("find").addEventListener("submit", function(e){ 
   e.preventDefault(); 

   var contractAddress = document.querySelector("#find 
     #contractAddress").value; 
   var contract =  
      web3.eth.contract(bettingContractABI).at(contractAddress); 

   var matchId = contract.matchId(); 
   var amount = contract.amount(); 
   var homeAddress = contract.homeBet(); 
   var awayAddress = contract.awayBet(); 

   document.querySelector("#find #message").innerHTML = "Contract balance is: " + web3.fromWei(web3.eth.getBalance(contractAddress), "ether") + ", Match ID is: " + matchId + ", bet amount is: " + web3.fromWei(amount, "ether") + " ETH, " + homeAddress + " has placed bet on home team and " + awayAddress + " has placed bet on away team"; 
}, false)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175

这是先前代码的工作原理:

  1. 首先,我们将合约的字节码和 ABI 存储在bettingContractByteCodebettingContractABI变量中。

  2. 然后,我们创建一个连接到我们的测试网节点的Web3实例。

  3. 然后,我们有getAJAXObject函数(一个跨浏览器兼容的函数),它返回一个 AJAX 对象。

  4. 然后,我们将submit事件侦听器附加到第一个表单上,该表单用于部署合约。在事件侦听器的回调中,我们通过传递matchId来向getURL端点发出请求,以获取加密的查询字符串。然后,我们生成用于部署合约的数据。然后,我们找出gasRequired。我们使用函数对象的estimateGas方法来计算所需的 gas,但您也可以使用web3.eth.estimateGas方法。它们在参数方面有所不同;即,在前面的情况下,您不需要传递交易数据。请记住,如果函数调用引发异常,estimateGas将返回块 gas 限制。然后,我们计算 nonce。在这里,我们只是使用getTransactionCount方法而不是我们之前学到的实际过程。我们之所以这样做,只是为了简化代码。然后,我们创建原始交易,对其进行签名并广播。一旦交易被挖掘,我们将显示合约地址。

  5. 然后,我们为第二个表单附加了一个submit事件监听器,用于投资合约。在这里,我们生成交易的data部分,计算所需的 gas,创建原始交易,签名并广播。在计算交易所需的 gas 时,我们传递了从账户地址和值对象属性到合约地址,因为它是一个函数调用,而 gas 取决于值、来自地址和合约地址。请记住,在找到调用合约函数所需的 gas 时,您可以传递tofromvalue属性,因为 gas 取决于这些值。

  6. 最后,我们为第三个表单添加了一个submit事件监听器,即显示部署的押注合约的信息。

测试客户端

现在我们已经完成了建立我们的押注平台,是时候测试它了。在测试之前,请确保测试网络区块链已完全下载并正在寻找新的入块。

现在使用我们之前构建的钱包服务,生成三个账户。使用 faucet.ropsten.be:3001/ 为每个账户添加一以太币。

然后,在Initial目录中运行node app.js,然后访问http://localhost:8080/matches,您将看到以下截图中显示的内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这里,您可以复制任何比赛 ID。假设您想使用第一场比赛进行测试,即 123945。现在访问 http://localhost:8080 您将看到以下截图中显示的内容:

现在通过填写第一个表单中的输入字段并单击“部署”按钮来部署合约,如下所示。使用您的第一个账户来部署合约。

现在从第二个账户对合约的主队和从第三个账户对客队进行押注,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在在第三个表单中输入合约地址并单击“查找”按钮以查看合约的详细信息。您将看到类似于以下截图的内容:

一旦两笔交易都被挖掘,再次检查合约的详细信息,您将看到类似于以下截图的内容:

在这里,您可以看到合约没有任何以太币,所有以太币都被转移到押注主队的账户。

总结

在本章中,我们深入学习了 Oraclize 和 strings 库。我们将它们一起使用,构建了一个去中心化的押注平台。现在您可以根据您的需求进一步定制合约和客户端。为了增强应用程序,您可以向合约添加事件并在客户端显示通知。我们的目标是了解去中心化押注应用程序的基本架构。

在下一章中,我们将学习如何使用 truffle 构建企业级以太坊智能合约,通过构建我们自己的加密货币。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/正经夜光杯/article/detail/832012
推荐阅读
相关标签
  

闽ICP备14008679号