Node.js和NoSQL开发比特币加密货币应用程序(上)

我一直在跟踪比特币之类的加密货币相关主题有几个月了,我对所发生的一切都很是着迷。php

做为一名Web应用程序开发人员,我一直特别感兴趣的一个主题是加密货币交易以及如何制做它们。从前端看,这些应用程序彷佛是用于管理账户,将比特币转换为美圆等法订货币以及将比特币转帐给其余人的工具,但它们能作更多吗?前端

咱们将看一些Node.js和NoSQL数据库Couchbase的例子,它们涵盖了以加密货币交易为模型的主题。java

免责声明:我不是加密货币专家,也没有参与金融服务或交易所的任何开发。我是这个主题的狂热爱好者,从本文中得到的任何内容都应该通过适当的测试和使用,风险自负。node

the take-Away

你将从这篇特定文章中得到那些内容,将没法得到那些内容呢?让咱们从你不会从本文中获得的东西开始:python

  • 咱们不会配置任何银行或信用卡服务来交易美圆等法订货币。
  • 咱们不会将任何已签名的交易广播到比特币网络,最终肯定转帐。

也就是说,如下是你能够期待在本文中学习的一些内容:android

  • 咱们将建立一个分层肯定性(HD,hierarchical deterministic)钱包,它能够为给定的种子生成无限量的密钥,每一个密钥表明一个用户钱包。
  • 咱们将根据主种子建立每一个包含钱包的用户账户。
  • 咱们将建立表明交易所存款,取款和资金转帐的交易,而不实际使用法订货币。
  • 咱们将从比特币网络中查找余额。
  • 咱们将建立在比特币网络上广播的签名交易。

咱们将在本文中看到许多能够更好地完成的事情。若是你发现了能够改进的内容,请务必在评论中分享。就像我说的那样,我不是这个主题的专家,只是一个粉丝。程序员

项目要求

为了成功完成这个项目,必须知足一些要求:web

  • 你必须安装并配置Node.js 6+。
  • 你必须安装Couchbase 5.1+并配置Bucket和RBAC配置文件。

重点是我将不会介绍如何启动和运行Couchbase。这不是一个困难的过程,可是你须要一个Bucket设置一个应用程序账户和一个用N1QL查询索引。mongodb

建立具备依赖关系的Node.js应用程序

在开始添加任何逻辑以前,咱们将建立一个新的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 Frameworkexpressbody-parserjoi包都与接受和验证请求数据相关。由于咱们将与公共比特币节点进行通讯,因此咱们将使用requestrequest-promise包。很是受欢迎的bitcore-lib软件包将容许咱们建立钱包并签署交易,而bitcore-mnemonic软件包将容许咱们生成可用于咱们的HD钱包密钥的种子。最后,couchbaseuuid将用于处理咱们的数据库。

如今咱们可能想要更好地构建咱们的项目。在项目目录中添加如下目录和文件(若是它们尚不存在):

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、比特币等区块链相关的交互式在线编程实战教程:

  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行帐号建立、交易、转帐、代币开发以及过滤器和交易等内容。
  • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括帐户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
  • EOS教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、帐户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如建立地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
  • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如建立地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
  • tendermint区块链开发详解,本课程适合但愿使用tendermint进行区块链开发的工程师,课程内容即包括tendermint应用开发模型中的核心概念,例如ABCI接口、默克尔树、多版本状态库等,也包括代币发行等丰富的实操代码,是go语言工程师快速入门区块链开发的最佳选择。

汇智网原创翻译,转载请标明出处。这里是原文Node.js和NoSQL开发比特币加密货币应用程序

相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息