构建钱包服务
钱包服务用于发送和接收资金。构建钱包服务的主要挑战是安全和信任。用户必须感到他们的资金是安全的,钱包服务的管理员不会窃取他们的资金。我们将在本章中构建的钱包服务将解决这两个问题。
在本章中,我们将讨论以下主题:
- 线上钱包和线下钱包的区别
- 使用 hooked-web3-provider 和 ethereumjs-tx,使得使用不受以太坊节点管理的帐户创建和签署交易变得更加容易
- 了解什么是高清钱包及其用途
- 使用
lightwallet.js
创建高清钱包和交易签名人 - 构建钱包服务
线上钱包和线下钱包的区别
钱包是帐户的集合,而帐户是地址及其相关私钥的组合。
当钱包连接到互联网时,它被称为在线钱包。比如存放在 geth、任何网站/数据库等等的钱包,就叫在线钱包。在线钱包也被称为热门钱包、网络钱包、托管钱包等等。至少在储存大量乙醚或长时间储存乙醚时,不建议使用在线钱包,因为它们有风险。此外,根据钱包存放的位置,可能需要信任第三方。
例如,大多数流行的钱包服务将钱包的私钥存储在自己身上,并允许你通过电子邮件和密码访问钱包,所以基本上,你没有实际访问钱包的权限,如果他们愿意,他们可以窃取钱包中的资金。
当钱包没有连接到互联网时,它被称为离线钱包。例如,存储在笔驱动器、纸张、文本文件等中的钱包。离线钱包也叫冷钱包。离线钱包比在线钱包更安全,因为要窃取资金,有人需要实际接触存储空间。离线存储面临的挑战是,你需要找到一个不会被意外删除或忘记的位置,否则其他人无法访问。许多人把钱包放在纸里,如果他们想在很长一段时间内安全地持有一些资金,就把纸放在保险箱里。如果你想经常从你的账户里汇款,你可以把钱存在一个有密码保护的笔驱动器里,也可以存在一个安全的柜子里。将钱包存储在数字设备中会有一点风险,因为数字设备随时可能损坏,你可能无法访问你的钱包;这就是为什么除了存储在笔驱动器中,你还应该把它放在一个安全的储物柜中。您也可以根据自己的需要找到更好的解决方案,但要确保它是安全的,并且您不会意外地失去对它的访问权限。
hooked-web3-provider 和 ethereumjs-tx 库
到目前为止,我们看到的所有 Web3.js 库的sendTransaction()
方法的例子都使用以太坊节点中的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-tx 是提供各种与事务相关的 API 的其中之一。例如,它让我们创建原始事务,签署原始事务,检查事务是否使用正确的密钥签署,等等。
这两个库都适用于 Node.js 和客户端 JavaScript。从https://www.npmjs.com/package/hooked-web3-provider下载 Hooked-Web3-Provider,从https://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)
})
下面是代码的工作原理:
- 首先,我们创建了一个
HookedWeb3Provider
实例。这是由 Hooked-Web3-Provider 库提供的。此构造函数接受一个具有两个属性的对象,这两个属性必须提供。host
是节点的 HTTP URL,transaction_signer
是一个对象,定制提供者与它通信以获得事务签名。 -
transaction_signer
对象有两个属性:hasAddress
和signTransaction
。调用hasAddress
检查交易是否可以签名,即检查交易签名人是否有from
地址账户的私钥。该方法接收地址和回调。如果没有找到地址的私有密钥,回调应该使用第一个参数作为错误消息,第二个参数作为false
来调用。而如果找到了私钥,第一个参数应该是null
,第二个参数应该是true
。 -
如果找到了该地址的私钥,那么定制提供者将调用
signTransaction
方法来对事务进行签名。这个方法有两个参数,即事务参数和一个回调。在该方法中,首先,我们将事务参数转换为原始事务参数,也就是说,原始事务参数值被编码为十六进制字符串。然后我们创建一个缓冲区来保存私钥。缓冲区是使用EthJS.Util.toBuffer()
方法创建的,它是ethereumjs-util
库的一部分。ethereumjs-util
库是由ethereumjs-tx
库导入的。然后,我们创建一个原始事务并对其进行签名,之后我们将它序列化并转换为十六进制字符串。最后,我们需要使用回调向自定义提供者提供已签名的原始事务的十六进制字符串。如果方法内部有错误,那么回调的第一个参数应该是一个错误消息。 - 现在,定制提供者获取原始事务并使用
web3.eth.sendRawTransaction()
广播它。 - 最后,我们调用
web3.eth.sendTransaction
函数向另一个帐户发送一些乙醚。这里,我们需要提供除了nonce
之外的所有事务参数,因为自定义提供者可以计算 nonce。早先,这些都是可选的,因为我们把它留给以太坊节点来计算,但是现在我们自己签名,我们需要提供所有这些。当事务没有任何相关数据时,gas
总是 21,000。
What about the public key? In the preceding code, nowhere did we mention anything about the public key of the signing address. You must be wondering how a miner will verify the authenticity of a transaction without the public key. Miners use a unique property of ECDSA, which allows you to calculate the public key from the message and signature. In a transaction, the message indicates the intention of the transaction, and the signature is used to find whether the message is signed using the correct private key. This is what makes ECDSA so special. ethereumjs-tx provides an API to verify transactions.
什么是分层确定性钱包?
分层确定性钱包是一种从称为种子的单个起始点导出地址和密钥的系统。确定性表示对于相同的种子,将生成相同的地址和密钥,而分层表示将按照相同的顺序生成地址和密钥。这使得备份和存储多个帐户更容易,因为您只需存储种子,而不是单个的密钥和地址。
Why will users need multiple accounts? You must be wondering why users will need multiple accounts. The reason is to hide their wealth. The balance of accounts is available publicly in the blockchain. So, if user A shares an address with user B to receive some ether, then user B can check how much ether is present in that address. Therefore, users usually distribute their wealth across various accounts.
有各种类型的 HD wallets,它们在种子格式和生成地址和密钥的算法方面有所不同,例如 BIP32、Armory、Coinkite、Coinb.in 等。
What are BIP32, BIP44, and BIP39? A Bitcoin Improvement Proposal (BIP) is a design document providing information to the Bitcoin community, or describing a new feature for Bitcoin or its processes or environment. The BIP should provide a concise technical specification of the feature and a rationale for the feature. At the time of writing this book, there are 152 BIPS (Bitcoin Improvement Proposals). BIP32 and BIP39 provide information about an algorithm to implement an HD wallet and mnemonic seed specification respectively. You can learn more about these at https://github.com/bitcoin/bips.
密钥派生函数介绍
非对称加密算法定义了其密钥的性质以及如何生成密钥,因为密钥需要相互关联。例如,RSA 密钥生成算法是确定性的。
对称加密算法只定义密钥大小。由我们来生成密钥。有各种算法来生成这些密钥。一个这样的算法是 KDF。
密钥导出函数 ( KDF )是一种确定性算法,用于从某个秘密值(如主密钥、密码或口令)中导出对称密钥。有各种类型的 KDF,比如 bcrypt、crypt、PBKDF2、scrypt、HKDF 等等。你可以在 https://en.wikipedia.org/wiki/Key_derivation_function 的了解更多关于 KDFs 的信息。
要从一个秘密值生成多个密钥,您可以连接一个数字并递增它。
基于密码的密钥派生函数获取密码并生成对称密钥。由于用户通常使用弱密码,基于密码的密钥派生函数被设计得较慢,并且占用大量内存,从而难以发起暴力攻击和其他类型的攻击。基于密码的密钥派生函数被广泛使用,因为很难记住秘密密钥,并且将它们存储在某个地方很危险,因为它可能会被窃取。PBKDF2 是基于密码的密钥派生函数的一个例子。
使用强力攻击很难破解主密钥或密码短语;因此,如果您想从主密钥或密码生成对称密钥,可以使用非基于密码的密钥派生函数,如 HKDF。HKDF 比 PBKDF2 快得多。
Why not just use a hash function instead of KDFs? The output of hash functions can be used as symmetric keys. So you must be wondering what is the need for KDFs. Well, if you are using a master key, passphrase, or a strong password, you can simply use a hash function. For example, HKDF simply uses a hash function to generate the key. But if you cannot guarantee that users will use a strong password, it's better to use a password derived hash function.
LightWallet 简介
LightWallet 是一个实现 BIP32、BIP39 和 BIP44 的高清钱包。LightWallet 提供 API 来创建和签署交易,或者使用使用它生成的地址和密钥来加密和解密数据。
LightWallet API 分为四个名称空间,即keystore
、signing
、encryption
和txutils
。signing
、encrpytion,
和txutils
分别提供 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 派生路径可以根据需要有多个参数,使用不同的参数值,我们可以生成不同的地址组及其相关的密钥。
默认情况下,LightWallet 使用m/0'/0'/0'
派生路径。这里,/n'
是参数,n
是参数值。
每个 HD 派生路径都有一个curve
和purpose
。purpose
可以是sign
也可以是asymEncrypt
。sign
表示该路径用于签署交易,asymEncrypt
表示该路径用于加密和解密。curve
表示 ECC 的参数。对于签名,参数必须是secp256k1
,对于非对称加密,曲线必须是curve25591
,因为 LightWallet 强迫我们使用这些参数,因为它们在这些目的中有好处。
构建钱包服务
现在我们已经了解了足够多的关于 LightWallet 的理论,是时候使用 LightWallet 和 hooked-web3-provider 来构建一个钱包服务了。我们的钱包服务将让用户生成一个唯一的种子,显示地址及其相关余额,最后,该服务将让用户向其他帐户发送以太网。所有的操作都将在客户端完成,这样用户就可以很容易地信任我们。用户要么必须记住种子,要么将它存储在某个地方。
先决条件
在开始构建 wallet 服务之前,请确保您正在运行 geth 开发实例,该实例正在挖掘,启用了 HTTP-RPC 服务器,允许来自任何域的客户端请求,并最终解锁了帐户 0。您可以通过运行以下命令来完成所有这些操作:
geth --dev --rpc --rpccorsdomain "*" --rpcaddr "0.0.0.0" --rpcport "8545" --mine --unlock=0
这里,--rpccorsdomain
用于允许某些域与 geth 通信。我们需要提供一个用空格分隔的域列表,比如"http://localhost:8080 https://mySite.com *"
。它还支持*
通配符。--rpcaddr
表示 geth 服务器可以到达哪个 IP 地址。默认为127.0.0.1
,所以如果它是一个托管服务器,你将无法使用服务器的公共 IP 地址访问它。因此,我们将它的值改为0.0.0.0
,这表示使用任何 IP 地址都可以到达服务器。
项目结构
在本章的练习文件中,你会发现两个目录,即Final
和Initial. Final
包含了项目的最终源代码,而Initial
包含了空的源代码文件和库,可以快速入门构建应用。
为了测试Final
目录,你需要在其中运行npm install
,然后在Final
目录中使用node app.js
命令运行应用程序。
在Initial
目录中,你会发现一个public
目录和两个名为app.js
和package.json
的文件。package.json
包含后端依赖项。我们的应用程序app.js
,是你放置后端源代码的地方。
public
目录包含与前端相关的文件。在public/css
里面,你会发现bootstrap.min.css
,这是引导库。在public/html
中,您将找到index.html
,在那里您将放置我们应用程序的 HTML 代码,最后,在public/js
目录中,您将找到 Hooked-Web3-Provider、web3js 和 LightWallet 的.js
文件。在public/js
里面,你还会找到一个main.js
文件,你将在那里放置我们应用的前端 JS 代码。
构建后端
我们先来搭建 app 的后端。首先,在初始目录中运行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);
前面的代码不言自明。
构建前端
现在让我们来构建应用程序的前端。前端将包括主要功能,即生成种子、显示种子地址和发送以太网。
现在我们来写 app 的 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="/js/web3.min.js"></script>
<script src="/js/hooked-web3-provider.min.js"></script>
<script src="/js/lightwallet.min.js"></script>
<script src="/js/main.js"></script>
</body>
</html>
下面是代码的工作原理:
- 首先,我们将 Bootstrap 4 样式表排入队列。
- 然后我们显示一个信息框,在这里我们将向用户显示各种消息。
- 然后我们有一个带有输入框和两个按钮的表单。输入框用于输入种子,或者在生成新种子时,种子显示在那里。
- “生成详细信息”按钮用于显示地址,“生成新种子”用于生成新的唯一种子。当单击 Generate Details 时,我们调用
generate_addresses()
方法,当单击 Generate New Seed 按钮时,我们调用generate_seed()
方法。 - 后来,我们有了一个空的有序列表。在这里,当用户单击 Generate Details 按钮时,我们将动态显示地址、它们的余额以及种子的相关私钥。
- 最后,我们有另一个表单,它接受一个 from 地址和一个 to 地址以及要传输的乙醚量。发件人地址必须是无序列表中当前显示的地址之一。
现在让我们编写 HTML 代码调用的每个函数的实现。首先,让我们编写生成新种子的代码。将此代码放在main.js
文件中:
function generate_seed()
{
var new_seed = lightwallet.keystore.generateRandomSeed();
document.getElementById("seed").value = new_seed;
generate_addresses(new_seed);
}
名称空间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;
}
});
});
}
下面是代码的工作原理:
-
首先,我们有一个名为
totalAddresses
的变量,它保存一个数字,表示用户想要生成的地址总数。 -
然后我们检查是否定义了
seed
参数。如果未定义,我们从输入字段获取种子。我们这样做是为了在生成新种子的同时,以及用户单击 Generate Details 按钮时,可以使用generate_addressess()
方法来显示信息种子。 -
然后,我们使用名称空间
keystore
的isSeedValid()
方法验证种子。 - 然后,我们要求用户输入他们想要生成和显示多少地址。然后我们验证输入。
- 名称空间
keystore
中的私钥总是加密存储的。在生成密钥时,我们需要对它们进行加密,在签署交易时,我们需要对密钥进行解密。用于导出对称加密密钥的密码可以作为来自用户的输入,或者通过提供随机字符串作为密码。为了更好的用户体验,我们生成一个随机字符串,并将其用作密码。对称密钥没有存储在keystore
命名空间中;因此,每当我们进行与私钥相关的操作时,比如生成密钥、访问密钥等等,我们都需要从密码生成密钥。 -
然后我们使用
createVault
方法创建一个keystore
实例。createVault
接受一个对象和一个回调。对象可以有四个属性:password
、seedPharse
、salt
和hdPathString
。password
是必选的,其他都是可选的。如果我们不提供一个seedPharse
,它会生成并使用一个随机种子。salt
连接到密码,以增加对称密钥的安全性,因为攻击者还必须找到密码和盐。如果盐没有被提供,它是随机产生的。keystore
名称空间保存未加密的 salt。hdPathString
用于为keystore
名称空间提供默认的派生路径,即在生成地址、签署交易等的时候。如果我们不提供派生路径,那么就使用这个派生路径。如果我们不提供hdPathString
,那么默认值就是m/0'/0'/0'
。该派生路径的默认用途是sign
。您可以使用keystore
实例的addHdDerivationPath()
方法创建新的派生路径或覆盖现有派生路径的用途。您还可以使用keystore
实例的setDefaultHdDerivationPath()
方法来更改默认的派生路径。最后,一旦创建了keystore
名称空间,就通过回调返回实例。所以在这里,我们只使用密码和种子创建了一个keystore
。 -
现在,我们需要生成用户需要的地址及其相关键的数量。因为我们可以从一个种子生成数百万个地址,所以
keystore
直到我们想要它生成地址时才生成任何地址,因为它不知道我们想要生成多少个地址。在创建了keystore
之后,我们使用keyFromPassword
方法从密码中生成对称密钥。然后我们调用generateNewAddress()
方法来生成地址及其相关的键。 -
generateNewAddress()
接受三个参数:密码派生密钥、要生成的地址数量和派生路径。因为我们没有提供派生路径,所以它使用 keystore 的默认派生路径。如果您多次调用generateNewAddress()
,它将从上次调用时创建的地址恢复。例如,如果您调用此方法两次,每次生成两个地址,您将得到前四个地址。 - 然后我们使用
getAddresses()
来获取存储在keystore
中的所有地址。 - 我们使用
exportPrivateKey
方法解密和检索地址的私钥。 - 我们使用
web3.eth.getBalance()
来获得地址的余额。 - 最后,我们显示无序列表中的所有信息。
现在我们知道了如何从种子中生成地址和它们的私钥。现在让我们编写send_ether()
方法的实现,该方法用于从种子生成的地址之一发送 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;
}
})
}
});
});
}
这里,从种子开始直到生成地址的代码是不言自明的。之后,我们给ks
的passwordProvider
属性分配一个回调。这个回调在事务签名期间被调用,以获取解密私钥的密码。如果我们不提供这个,LightWallet 会提示用户输入密码。然后,我们通过将keystore
作为事务签名者传递来创建一个HookedWeb3Provider
实例。现在,当定制提供者想要签署一个交易时,它调用ks
的hasAddress
和signTransactions
方法。如果要签名的地址不在生成的地址中,ks
将向自定义提供者给出一个错误。最后,我们使用web3.eth.sendTransaction
方法发送一些乙醚。
测试
现在我们已经完成了钱包服务的构建,让我们来测试一下,以确保它能像预期的那样工作。首先,在初始目录中运行node app.js
,然后在您喜欢的浏览器中访问http://localhost:8080
。您将看到以下屏幕:
现在,单击“生成新种子”按钮来生成一个新种子。系统将提示您输入一个数字,指示要生成的地址数量。您可以提供任何数字,但出于测试目的,请提供一个大于 1 的数字。现在屏幕看起来会像这样:
现在要测试发送以太网,您需要从 coinbase 帐户向其中一个生成的地址发送一些以太网。一旦你发送了一些以太到一个生成的地址,点击 Generate Details 按钮来刷新 UI,虽然没有必要测试使用钱包服务发送以太。确保再次生成相同的地址。现在屏幕看起来会像这样:
现在,在 from 地址字段中,输入列表中在from
地址字段中有余额的账户地址。然后在“收件人地址”字段中输入另一个地址。出于测试目的,您可以输入显示的任何其他地址。然后输入小于或等于“起始地址”帐户余额的某个余额。现在,您的屏幕将看起来像这样:
现在单击 Send Ether 按钮,您将在信息框中看到事务散列。等待一段时间让它被开采。同时,您可以通过单击 Generate Details 按钮在很短的时间内检查事务是否被挖掘。一旦挖掘出交易,您的屏幕将类似于这样:
如果一切都按照说明进行,您的钱包服务就准备好了。实际上,您可以将该服务部署到一个定制的域中,并公开使用它。它是完全安全的,用户会信任它。
摘要
在这一章中,你学习了三个重要的以太坊库:Hooked-Web3-Provider、ethereumjs-tx 和 LightWallet。这些库可用于管理帐户和签署以太坊节点之外的交易。在为大多数类型的 DApps 开发客户端时,您会发现这些库非常有用。
最后,我们创建了一个钱包服务,让用户管理他们的帐户,这些帐户与服务的后端共享私钥或任何其他与他们的钱包相关的信息。
在下一章中,我们将构建一个平台来构建和部署智能合约。