Node.js和NoSQL開發比特幣加密貨幣應用程式(上)

tualala發表於2018-12-24

我一直在跟蹤比特幣之類的加密貨幣相關主題有幾個月了,我對所發生的一切都非常著迷。

作為一名Web應用程式開發人員,我一直特別感興趣的一個主題是加密貨幣交易以及如何製作它們。從前端看,這些應用程式似乎是用於管理帳戶,將比特幣轉換為美元等法定貨幣以及將比特幣轉賬給其他人的工具,但它們能做更多嗎?

我們將看一些Node.js和NoSQL資料庫Couchbase的例子,它們涵蓋了以加密貨幣交易為模型的主題。

免責宣告:我不是加密貨幣專家,也沒有參與金融服務或交易所的任何開發。我是這個主題的狂熱愛好者,從本文中獲得的任何內容都應該經過適當的測試和使用,風險自負。

the take-Away

你將從這篇特定文章中獲得那些內容,將無法獲得那些內容呢?讓我們從你不會從本文中得到的東西開始:

  • 我們不會配置任何銀行或信用卡服務來交易美元等法定貨幣。
  • 我們不會將任何已簽名的交易廣播到比特幣網路,最終確定轉賬。

也就是說,以下是你可以期待在本文中學習的一些內容:

  • 我們將建立一個分層確定性(HD,hierarchical deterministic)錢包,它可以為給定的種子生成無限量的金鑰,每個金鑰代表一個使用者錢包。
  • 我們將根據主種子建立每個包含錢包的使用者帳戶。
  • 我們將建立代表交易所存款,取款和資金轉賬的交易,而不實際使用法定貨幣。
  • 我們將從比特幣網路中查詢餘額。
  • 我們將建立在比特幣網路上廣播的簽名交易。

我們將在本文中看到許多可以更好地完成的事情。如果你發現了可以改進的內容,請務必在評論中分享。就像我說的那樣,我不是這個主題的專家,只是一個粉絲。

專案要求

為了成功完成這個專案,必須滿足一些要求:

  • 你必須安裝並配置Node.js 6+。
  • 你必須安裝Couchbase 5.1+並配置Bucket和RBAC配置檔案。

重點是我將不會介紹如何啟動和執行Couchbase。這不是一個困難的過程,但是你需要一個Bucket設定一個應用程式帳戶和一個用N1QL查詢索引。

建立具有依賴關係的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開發比特幣加密貨幣應用程式

相關文章