Lanceyan 2019-06-30
我一直在跟踪比特币之类的加密货币相关主题有几个月了,我对所发生的一切都非常着迷。
作为一名Web应用程序开发人员,我一直特别感兴趣的一个主题是加密货币交易以及如何制作它们。从前端看,这些应用程序似乎是用于管理帐户,将比特币转换为美元等法定货币以及将比特币转账给其他人的工具,但它们能做更多吗?
我们将看一些Node.js和NoSQL数据库Couchbase的例子,它们涵盖了以加密货币交易为模型的主题。
免责声明:我不是加密货币专家,也没有参与金融服务或交易所的任何开发。我是这个主题的狂热爱好者,从本文中获得的任何内容都应该经过适当的测试和使用,风险自负。
你将从这篇特定文章中获得那些内容,将无法获得那些内容呢?让我们从你不会从本文中得到的东西开始:
也就是说,以下是你可以期待在本文中学习的一些内容:
我们将在本文中看到许多可以更好地完成的事情。如果你发现了可以改进的内容,请务必在评论中分享。就像我说的那样,我不是这个主题的专家,只是一个粉丝。
为了成功完成这个项目,必须满足一些要求:
重点是我将不会介绍如何启动和运行Couchbase
。这不是一个困难的过程,但是你需要一个Bucket
设置一个应用程序帐户和一个用N1QL
查询索引。
在开始添加任何逻辑之前,我们将创建一个新的Node.js应用程序并下载依赖项。在计算机上的某个位置创建项目目录,并从该目录中的CLI
执行以下命令:
npm init -y npm install couchbase --save npm install express --save npm install body-parser --save npm install joi --save npm install request request-promise --save npm install uuid --save npm install bitcore-lib --save npm install bitcore-mnemonic --save
我知道我可以在一行中完成所有的依赖安装,但我想让它们清楚地阅读。那么我们在上面的命令中做了什么?
首先,我们通过创建package.json
文件来初始化一个新的Node.js项目。然后我们下载我们的依赖项并通过--save
标志将它们添加到package.json
文件中。
对于此示例,我们将使用Express Framework
。express
,body-parser
和joi
包都与接受和验证请求数据相关。因为我们将与公共比特币节点进行通信,所以我们将使用request
和request-promise
包。非常受欢迎的bitcore-lib
软件包将允许我们创建钱包并签署交易,而bitcore-mnemonic
软件包将允许我们生成可用于我们的HD钱包密钥的种子。最后,Couchbase
和uuid
将用于处理我们的数据库。
现在我们可能想要更好地构建我们的项目。在项目目录中添加以下目录和文件(如果它们尚不存在):
package.json config.json app.js routes account.js transaction.js utility.js classes helper.js
我们所有的API端点都将分为几类,并放在每个适当的路由文件中。我们不必这样做,但为了使我们的项目更干净一点。我们去删除大量的比特币和数据库逻辑,我们将把所有非数据验证的内容添加到我们的classes/helper.js
文件中。config.json
文件将包含我们所有的数据库信息以及我们的助记符种子。在一个现实的场景中,这个文件应该被视为黄金般重要,并获得尽可能多的保护。app.js
文件将具有我们所有的配置和引导逻辑,用于连接我们的路由,连接到数据库等。
为方便起见,我们将为项目添加一个依赖项并进行设置:
npm install nodemon --save-dev
nodemon
包将允许我们每次更改文件时热重新加载项目。这不是一个必须的要求,但可以为我们节省一些时间。
打开package.json
文件并添加以下脚本以实现它:
... "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "./node_modules/nodemon/bin/nodemon.js app.js" }, ...
我们可以在此时开始我们的应用程序的开发过程。
在开发我们的应用程序时,在我们开始担心API端点之前,我们想要创建我们的数据库和比特币相关的逻辑。
我们将把时间花在项目的classes/helper.js
文件中。打开它并包含以下内容:
const Couchbase = require("couchbase"); const Request = require("request-promise"); const UUID = require("uuid"); const Bitcore = require("bitcore-lib"); class Helper { constructor(host, bucket, username, password, seed) { this.cluster = new Couchbase.Cluster("couchbase://" + host); this.cluster.authenticate(username, password); this.bucket = this.cluster.openBucket(bucket); this.master = seed; } createKeyPair(account) { } getWalletBalance(addresses) { } getAddressBalance(address) { } getAddressUtxo(address) { } insert(data, id = UUID.v4()) { } createAccount(data) { } addAddress(account) { } getAccountBalance(account) { } getMasterAddresses() { } getMasterKeyPairs() { } getMasterAddressWithMinimum(addresses, amount) { } getMasterChangeAddress() { } getAddresses(account) { } getPrivateKeyFromAddress(account, address) { } createTransactionFromAccount(account, source, destination, amount) { } createTransactionFromMaster(account, destination, amount) { } } module.exports = Helper;
我们将把这个类作为我们应用程序的singleton
来发送。在constructor
方法中,我们建立与数据库集群的连接,打开Bucket
并进行身份验证。打开的Bucket
将在整个helper
类中使用。
让我们在完成数据库逻辑之前跳出比特币逻辑。
如果你不熟悉HD钱包,它们本质上是一个由单个种子衍生而来的钱包。使用种子,你可以得到children,那些children可以再有children,等等。
createKeyPair(account) { var account = this.master.deriveChild(account); var key = account.deriveChild(Math.random() * 10000 + 1); return { "secret": key.privateKey.toWIF().toString(), "address": key.privateKey.toAddress().toString() } }
createKeyPair
函数中的master
变量表示顶级种子密钥。每个用户帐户都是该密钥的直接子项,因此我们根据account
值派生子项。account
值是人员编号,创建的每个帐户都将获得增量编号。但是,我们不会生成帐户密钥并将其称为一天。相反,每个帐户密钥将有10,000个可能的私钥和公钥,以防他们不想多次使用同一个密钥。一旦我们随机生成了一个密钥,我们就会返回它。
同样,我们有一个getMasterChangeAddress
函数,如下所示:
getMasterChangeAddress() { var account = this.master.deriveChild(0); var key = account.deriveChild(Math.random() * 10 + 1); return { "secret": key.privateKey.toWIF().toString(), "address": key.privateKey.toAddress().toString() } }
当我们开始创建帐户时,它们将从一开始,为交易或Web应用程序留下零,或者你想要调用它。我们还为此帐户分配了10个可能的地址。这些地址将做两件事。第一个是他们将比特币发送到其他账户,第二个是他们将收到剩余款项,也就是所谓的变更。请记住,在比特币交易中,必须花费所有未花费的交易输出(UTXO),即使它小于期望的金额。这意味着所需的金额将被发送到目的地,剩余部分将被发送回这10个地址中的一个。
还有其他方法或更好的方法吗?当然,但这个将适用于这个例子。
为了获得我们使用或使用HD种子生成的任何地址的余额,我们可以使用公共比特币资源管理器:
getAddressBalance(address) { return Request("https://insight.bitpay.com/api/addr/" + address); }
上面的函数将采用一个地址并以十进制格式和satoshis
获得余额。展望未来,satoshi
价值是我们唯一的相关价值。如果我们有给定帐户的X个地址,我们可以使用如下函数获得总余额:
getWalletBalance(addresses) { var promises = []; for(var i = 0; i < addresses.length; i++) { promises.push(Request("https://insight.bitpay.com/api/addr/" + addresses[i])); } return Promise.all(promises).then(result => { var balance = result.reduce((a, b) => a + JSON.parse(b).balanceSat, 0); return new Promise((resolve, reject) => { resolve({ "balance": balance }); }); }); }
在上面的getWalletBalance
函数中,我们正在为每个地址发出请求,当它们全部完成时,我们可以添加余额并返回它们。
能够传输加密货币需要的不仅仅是地址余额。相反,我们需要知道给定地址的未花费的交易输出(UTXO)。这可以使用BitPay
中的相同API找到:
getAddressUtxo(address) { return Request("https://insight.bitpay.com/api/addr/" + address + "/utxo").then(utxo => { return new Promise((resolve, reject) => { if(JSON.parse(utxo).length == 0) { reject({ "message": "There are no unspent transactions available." }); } resolve(JSON.parse(utxo)); }); }); }
如果没有未使用的交易输出,则意味着我们无法传输任何内容,而是应该抛出错误。足够的发送代表的是一个不同的意思。
例如,我们可以这样做:
getMasterAddressWithMinimum(addresses, amount) { var promises = []; for(var i = 0; i < addresses.length; i++) { promises.push(Request("https://insight.bitpay.com/api/addr/" + addresses[i])); } return Promise.all(promises).then(result => { for(var i = 0; i < result.length; i++) { if(result[i].balanceSat >= amount) { return resolve({ "address": result[i].addrStr }); } } reject({ "message": "Not enough funds in exchange" }); }); }
在上面的函数中,我们将获取一个地址列表并检查哪个地址的数量大于我们提供的阈值。如果他们都没有足够的余额,我们应该发送这个消息。
最终的实用程序相关功能,我们已经看到了一些:
getMasterKeyPairs() { var keypairs = []; var key; var account = this.master.deriveChild(0); for(var i = 1; i <= 10; i++) { key = account.deriveChild(i); keypairs.push({ "secret": key.privateKey.toWIF().toString(), "address": key.privateKey.toAddress().toString() }); } return keypairs; }
上面的函数将为我们提供所有主密钥,这对于签名和检查值非常有用。
重申一下,我使用有限值来生成多少个键。你可能会也可能不想这样做,这取决于你。
现在让我们深入研究一些用于存储应用程序数据的NoSQL逻辑。
截至目前,我们的数据库中没有数据。第一个逻辑步骤可能是创建一些数据。虽然独立并不是特别困难,但我们可以创建这样的函数:
insert(data, id = UUID.v4()) { return new Promise((resolve, reject) => { this.bucket.insert(id, data, (error, result) => { if(error) { reject({ "code": error.code, "message": error.message }); } data.id = id; resolve(data); }); }); }
基本上,我们接受一个对象和一个id
用作文档密钥。如果未提供文档密钥,我们将自动生成它。完成所有操作后,我们将返回创建的内容,包括响应中的id
。
所以我们假设我们要创建一个用户帐户。我们可以做到以下几点:
createAccount(data) { return new Promise((resolve, reject) => { this.bucket.counter("accounts::total", 1, { "initial": 1 }, (error, result) => { if(error) { reject({ "code": error.code, "message": error.message }); } data.account = result.value; this.insert(data).then(result => { resolve(result); }, error => { reject(error); }); }); }); }
请记住,帐户由此示例的自动递增数值驱动。我们可以使用Couchbase
中的counter
创建递增值。如果计数器不存在,我们将其初始化为1并在每次下一次调用时递增。请记住,0
是为应用程序密钥保留的。
在我们得到计数器值之后,我们将它添加到传递的对象并调用我们的insert
函数,在这种情况下为我们生成一个唯一的id
。
我们还没有看到它,因为我们没有任何端点,但我们假设在创建帐户时,它没有地址信息,只有帐户标识符。我们可能想为用户添加地址:
addAddress(account) { return new Promise((resolve, reject) => { this.bucket.get(account, (error, result) => { if(error) { reject({ "code": error.code, "message": error.message }); } var keypair = this.createKeyPair(result.value.account); this.bucket.mutateIn(account).arrayAppend("addresses", keypair, true).execute((error, result) => { if(error) { reject({ "code": error.code, "message": error.message }); } resolve({ "address": keypair.address }); }); }); }); }
添加地址时,我们首先按文档id
获取用户。检索文档后,我们获取数字帐户值并创建10,000个选项的新密钥对。使用子文档操作,我们可以将密钥对添加到用户文档,而无需下载文档或对其进行操作。
关于我们刚刚做了什么非常严肃的事情。
我将未加密的私钥和公共地址存储在用户文档中。这对生产来说是一个很大的禁忌。还记得你读过的所有关于人们钥匙被盗的地方的故事吗?实际上,我们希望在插入数据之前加密数据。我们可以通过使用Node.js加密库来实现,或者如果我们使用Couchbase Server 5.5,Couchbase的Node.js SDK会提供加密。我们不会在这里探讨它。
好的,我们现在已经在数据库中获得了帐户数据和地址。让我们查询该数据:
getAddresses(account) { var statement, params; if(account) { statement = "SELECT VALUE addresses.address FROM " + this.bucket._name + " AS account USE KEYS $id UNNEST account.addresses as addresses"; params = { "id": account }; } else { statement = "SELECT VALUE addresses.address FROM " + this.bucket._name + " AS account UNNEST account.addresses as addresses WHERE account.type = 'account'"; } var query = Couchbase.N1qlQuery.fromString(statement); return new Promise((resolve, reject) => { this.bucket.query(query, params, (error, result) => { if(error) { reject({ "code": error.code, "message": error.message }); } resolve(result); }); }); }
上面的getAddresses
函数可以做两件事之一。如果提供了帐户,我们将使用N1QL查询来获取该特定帐户的所有地址。如果未提供帐户,我们将获取数据库中每个帐户的所有地址。在这两种情况下,我们只获取公共地址,没有任何敏感信息。使用参数化的N1QL查询,我们可以将数据库结果返回给客户端。
我们的查询中需要注意的事项。
我们将地址存储在用户文档中的数组中。使用UNNEST运算符,我们可以展平这些地址并使响应更具吸引力。
现在假设我们有一个地址,我们想获得相应的私钥。 我们可能会做以下事情:
getPrivateKeyFromAddress(account, address) { var statement = "SELECT VALUE keypairs.secret FROM " + this.bucket._name + " AS account USE KEYS $account UNNEST account.addresses AS keypairs WHERE keypairs.address = $address"; var query = Couchbase.N1qlQuery.fromString(statement); return new Promise((resolve, reject) => { this.bucket.query(query, { "account": account, "address": address }, (error, result) => { if(error) { reject({ "code": error.code, "message": error.message }); } resolve({ "secret": result[0] }); }); }); }
给定一个特定的帐户,我们创建一个类似于我们之前看到的查询。这次,在我们UNNEST
,我们执行WHERE
条件,仅为匹配地址提供结果。如果我们想要我们可以做一个数组操作。使用Couchbase和N1QL,有很多方法可以解决问题。
======================================================================
分享一些以太坊、EOS、比特币等区块链相关的交互式在线编程实战教程:
汇智网原创翻译,转载请标明出处。这里是原文Node.js和NoSQL开发比特币加密货币应用程序