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

tualala發表於2018-12-24

使用Node.js和NoSQL開發比特幣加密貨幣應用程式(上)中,我們建立了HD錢包,它可以為給定的種子生成無限量的金鑰,每個金鑰代表一個使用者錢包。我們將根據主種子建立每個包含錢包的使用者帳戶。下面我們接著來看如何進行交易、查詢餘額等重要功能如何實現。

我們要在這裡改變一下。到目前為止,我們已經在NoSQL資料庫中完成了面向帳戶的操作。另一個重要方面是交易。例如,也許使用者X為BTC存入一些美元貨幣,而使用者Y進行提款。我們需要儲存和查詢該交易資訊。

API端點函式將儲存交易資料,但我們仍然可以查詢它。

getAccountBalance(account) {
    var statement = "SELECT SUM(tx.satoshis) AS balance FROM " + this.bucket._name + " AS tx WHERE tx.type = `transaction` AND tx.account = $account";
    var query = Couchbase.N1qlQuery.fromString(statement);
    return new Promise((resolve, reject) => {
        this.bucket.query(query, { "account": account }, (error, result) => {
            if(error) {
                reject({ "code": error.code, "message": error.message });
            }
            resolve({ "balance": result[0].balance });
        });
    });
}

給定一個帳戶,我們希望獲得特定使用者的帳戶餘額。

等一下,讓我們退後一步,因為我們不是已經建立了一些帳戶餘額功能嗎?從技術上講,我們做了,但這些功能用於檢查錢包餘額,而不是帳戶餘額。

這是我的一些經驗變成灰色區域的地方。每次傳送比特幣時,都會收取費用,有時費用相當昂貴。當你存款時,將錢轉入你的錢包並不符合成本效益,因為這將收取礦工費。然後你將被收取撤回甚至轉賬的費用。那時你已經失去了大部分的比特幣。

相反,我認為交易所有一個類似於證券交易所貨幣市場賬戶的持有賬戶。你的帳戶中應該有資金的記錄,但從技術上講,它不在錢包中。如果你想要轉賬,則需要從應用程式地址而不是你的使用者地址進行轉賬。當你退出時,它只是被減去。

再說一次,我不知道這是否真的如何運作,但這就是我為了避免各處收費而採取的方式。

回到我們的getAccountBalance函式。我們正在處理每筆交易的總和。存款具有正值,而轉賬和取款具有負值。將這些資訊彙總在一起可以為你提供準確的數字,不包括你的錢包餘額。稍後我們將獲得一個錢包餘額帳戶。

鑑於我們對帳戶餘額知之甚少,我們可以嘗試從錢包中建立一個交易:

createTransactionFromAccount(account, source, destination, amount) {
    return new Promise((resolve, reject) => {
        this.getAddressBalance(source).then(sourceAddress => {
            if(sourceAddress.balanceSat < amount) {
                return reject({ "message": "Not enough funds in account." });
            }
            this.getPrivateKeyFromAddress(account, source).then(keypair => {
                this.getAddressUtxo(source).then(utxo => {
                    var transaction = new Bitcore.Transaction();
                    for(var i = 0; i < utxo.length; i++) {
                        transaction.from(utxo[i]);
                    }
                    transaction.to(destination, amount);
                    this.addAddress(account).then(change => {
                        transaction.change(change.address);
                        transaction.sign(keypair.secret);
                        resolve(transaction);
                    }, error => reject(error));
                }, error => reject(error));
            }, error => reject(error));
        }, error => reject(error));
    });
}

如果提供了源地址,目的地地址和金額,我們可以建立並簽署一個交易,以便稍後在比特幣網路上廣播。

首先,我們得到有問題的源地址的餘額。我們需要確保它有足夠的UTXO來滿足傳送量預期。請注意,在此示例中,我們正在執行單個地址交易。如果你想變得複雜,可以在單個交易中從多個地址傳送。我們不會在這裡這樣做。如果我們的單個地址有足夠的資金,我們會獲得它的私鑰和UTXO資料。使用UTXO資料,我們可以建立比特幣交易,應用目的地地址和更改地址,然後使用我們的私鑰對交易進行簽名。可以廣播響應。

同樣地,假設我們想從我們的持有賬戶轉賬比特幣:

createTransactionFromMaster(account, destination, amount) {
    return new Promise((resolve, reject) => {
        this.getAccountBalance(account).then(accountBalance => {
            if(accountBalance.balance < amount) {
                reject({ "message": "Not enough funds in account." });
            }
            var mKeyPairs = this.getMasterKeyPairs();
            var masterAddresses = mKeyPairs.map(a => a.address);
            this.getMasterAddressWithMinimum(masterAddresses, amount).then(funds => {
                this.getAddressUtxo(funds.address).then(utxo => {
                    var transaction = new Bitcore.Transaction();
                    for(var i = 0; i < utxo.length; i++) {
                        transaction.from(utxo[i]);
                    }
                    transaction.to(destination, amount);
                    var change = helper.getMasterChangeAddress();
                    transaction.change(change.address);
                    for(var j = 0; j < mKeyPairs.length; j ++) {
                        if(mKeyPairs[j].address == funds.address) {
                            transaction.sign(mKeyPairs[j].secret);
                        }
                    }
                    var tx = {
                        account: account,
                        satoshis: (amount * -1),
                        timestamp: (new Date()).getTime(),
                        status: "transfer",
                        type: "transaction"
                    };
                    this.insert(tx).then(result => {
                        resolve(transaction);
                    }, error => reject(error));
                }, error => reject(error));
            }, error => reject(error));
        }, error => reject(error));
    });
}

我們假設我們的交換地址裝滿了瘋狂的比特幣以滿足需求。

第一步是確保我們的持有賬戶中有資金。我們可以執行總結每個交易的查詢以獲得有效數字。如果我們有足夠的,我們可以獲得所有10個主金鑰對和地址。我們需要檢查哪個地址有足夠的資金髮送。請記住,這裡的單一地址交易可能會有更多。

如果地址有足夠的資金,我們會獲得UTXO資料並開始進行交易。這次代替我們的錢包作為源地址,我們使用交換的錢包。在我們獲得簽名交易之後,我們想在資料庫中建立一個交易來減去我們正在傳輸的值。

在我們進入API端點之前,我想重新嘗試一些事情:

  • 我假設熱門的交易所有一個持有賬戶,以避免對錢包地址徵收費用。
  • 我們在此示例中使用單地址交易,而不是聚合我們擁有的內容。
  • 我應該是在加密帳戶文件中的關鍵資料。
  • 我沒有廣播任何交易,只建立它們。

現在讓我們關注我們的API端點,這是一個簡單的部分。

使用Express Framework設計RESTful API端點

請記住,正如我們在開始時配置的那樣,我們的端點將分為三個檔案,這些檔案充當分組。我們將從最小和最簡單的端點組開始,這些端點比其他任何端點都更實用。

開啟專案的routes/utility.js檔案幷包含以下內容:

const Bitcore = require("bitcore-lib");
const Mnemonic = require("bitcore-mnemonic");

module.exports = (app) => {

    app.get("/mnemonic", (request, response) => {
        response.send({
            "mnemonic": (new Mnemonic(Mnemonic.Words.ENGLISH)).toString()
        });
    });

    app.get("/balance/value", (request, response) => {
        Request("https://api.coinmarketcap.com/v1/ticker/bitcoin/").then(market => {
            response.send({ "value": "$" + (JSON.parse(market)[0].price_usd * request.query.balance).toFixed(2) });
        }, error => {
            response.status(500).send(error);
        });
    });

}

這裡我們有兩個端點,一個用於生成助記符種子,另一個用於獲取比特幣餘額的法定值。這兩者都不是真正必要的,但是在第一次啟動時,生成種子值以便稍後儲存在我們的配置檔案中可能會很好。

現在開啟專案的routes/account.js檔案,以便我們處理帳戶資訊:

const Request = require("request-promise");
const Joi = require("joi");
const helper = require("../app").helper;

module.exports = (app) => {

    app.post("/account", (request, response) => { });

    app.put("/account/address/:id", (request, response) => { });

    app.get("/account/addresses/:id", (request, response) => { });

    app.get("/addresses", (request, response) => { });

    app.get("/account/balance/:id", (request, response) => { });

    app.get("/address/balance/:id", (request, response) => { });

}

請注意,我們正在從尚未啟動的app.js檔案中提取helper程式類。現在就跟它一起使用它以後會有意義,雖然它沒什麼特別的。

在建立帳戶時,我們有以下內容:

app.post("/account", (request, response) => {
    var model = Joi.object().keys({
        firstname: Joi.string().required(),
        lastname: Joi.string().required(),
        type: Joi.string().forbidden().default("account")
    });
    Joi.validate(request.body, model, { stripUnknown: true }, (error, value) => {
        if(error) {
            return response.status(500).send(error);
        }
        helper.createAccount(value).then(result => {
            response.send(value);
        }, error => {
            response.status(500).send(error);
        });
    });
});

使用Joi我們可以驗證請求正文並在錯誤時丟擲錯誤。假設請求正文是正確的,我們可以呼叫createAccount函式在資料庫中儲存一個新帳戶。

建立帳戶後,我們可以新增一些地址:

app.put("/account/address/:id", (request, response) => {
    helper.addAddress(request.params.id).then(result => {
        response.send(result);
    }, error => {
        return response.status(500).send(error);
    });
});

使用傳送的帳戶ID,我們可以呼叫我們的addAddress函式來對我們的文件使用子文件操作。

還不錯吧?

要獲取特定帳戶的所有地址,我們可能會有以下內容:

app.get("/account/addresses/:id", (request, response) => {
    helper.getAddresses(request.params.id).then(result => {
        response.send(result);
    }, error => {
        response.status(500).send(error);
    });
});

或者,如果我們不提供id,我們可以使用以下端點函式從所有帳戶獲取所有地址:

app.get("/addresses", (request, response) => {
    helper.getAddresses().then(result => {
        response.send(result);
    }, error => {
        response.status(500).send(error);
    });
});

現在可能是最棘手的端點功能。假設我們希望獲得帳戶餘額,其中包括持有帳戶以及每個錢包地址。我們可以做到以下幾點:

app.get("/account/balance/:id", (request, response) => {
    helper.getAddresses(request.params.id).then(addresses => helper.getWalletBalance(addresses)).then(balance => {
        helper.getAccountBalance(request.params.id).then(result => {
            response.send({ "balance": balance.balance + result.balance });
        }, error => {
            response.status(500).send({ "code": error.code, "message": error.message });
        });
    }, error => {
        response.status(500).send({ "code": error.code, "message": error.message });
    });
});

以上將呼叫我們的兩個函式來獲得餘額,並將結果加在一起以獲得一個巨大的餘額。

帳戶端點不是特別有趣。建立交易更令人興奮。

開啟專案的routes/transaction.js檔案幷包含以下內容:

const Request = require("request-promise");
const Joi = require("joi");
const Bitcore = require("bitcore-lib");
const helper = require("../app").helper;

module.exports = (app) => {

    app.post("/withdraw", (request, response) => { });

    app.post("/deposit", (request, response) => { });

    app.post("/transfer", (request, response) => { });

}

我們有三種不同型別的交易。我們可以為比特幣存入法定貨幣,為法定貨幣提取比特幣,並將比特幣轉賬到新的錢包地址。

我們來看看存款端點:

app.post("/deposit", (request, response) => {
    var model = Joi.object().keys({
        usd: Joi.number().required(),
        id: Joi.string().required()
    });
    Joi.validate(request.body, model, { stripUnknown: true }, (error, value) => {
        if(error) {
            return response.status(500).send(error);
        }
        Request("https://api.coinmarketcap.com/v1/ticker/bitcoin/").then(market => {
            var btc = value.usd / JSON.parse(market)[0].price_usd;
            var transaction = {
                account: value.id,
                usd: value.usd,
                satoshis: Bitcore.Unit.fromBTC(btc).toSatoshis(),
                timestamp: (new Date()).getTime(),
                status: "deposit",
                type: "transaction"
            };
            helper.insert(transaction).then(result => {
                response.send(result);
            }, error => {
                response.status(500).send(error);
            });
        }, error => {
            response.status(500).send(error);
        });
    });
});

在我們驗證輸入後,我們使用CoinMarketCap檢查美元比特幣的當前值。使用響應中的資料,我們可以根據存入的美元金額計算出應該獲得多少比特幣。

建立資料庫交易後,我們可以儲存它,因為它是一個正數,它將在查詢時返回正餘額。

現在讓我們說我們想從比特幣中提取資金:

app.post("/withdraw", (request, response) => {
    var model = Joi.object().keys({
        satoshis: Joi.number().required(),
        id: Joi.string().required()
    });
    Joi.validate(request.body, model, { stripUnknown: true }, (error, value) => {
        if(error) {
            return response.status(500).send(error);
        }
        helper.getAccountBalance(value.id).then(result => {
            if(result.balance == null || (result.balance - value.satoshis) < 0) {
                return response.status(500).send({ "message": "There are not `" + value.satoshis + "` satoshis available for withdrawal" });
            }
            Request("https://api.coinmarketcap.com/v1/ticker/bitcoin/").then(market => {
                var usd = (Bitcore.Unit.fromSatoshis(value.satoshis).toBTC() * JSON.parse(market)[0].price_usd).toFixed(2);
                var transaction = {
                    account: value.id,
                    satoshis: (value.satoshis * -1),
                    usd: parseFloat(usd),
                    timestamp: (new Date()).getTime(),
                    status: "withdrawal",
                    type: "transaction"
                };
                helper.insert(transaction).then(result => {
                    response.send(result);
                }, error => {
                    response.status(500).send(error);
                });
            }, error => {
                response.status(500).send(error);
            });
        }, error => {
            return response.status(500).send(error);
        });
    });
});

類似的事件正在這裡發生。在驗證請求主體後,我們獲得帳戶餘額並確保我們提取的金額小於或等於我們的餘額。如果是,我們可以根據CoinMarketCap的當前價格進行另一次交易。我們將使用負值建立一個交易並將其儲存到資料庫中。

在這兩種情況下,我們都依賴於CoinMarketCap,它在過去一直存在負面爭議。你可能希望為交易選擇不同的資源。

最後,我們有轉賬:

app.post("/transfer", (request, response) => {
    var model = Joi.object().keys({
        amount: Joi.number().required(),
        sourceaddress: Joi.string().optional(),
        destinationaddress: Joi.string().required(),
        id: Joi.string().required()
    });
    Joi.validate(request.body, model, { stripUnknown: true }, (error, value) => {
        if(error) {
            return response.status(500).send(error);
        }
        if(value.sourceaddress) {
            helper.createTransactionFromAccount(value.id, value.sourceaddress, value.destinationaddress, value.amount).then(result => {
                response.send(result);
            }, error => {
                response.status(500).send(error);
            });
        } else {
            helper.createTransactionFromMaster(value.id, value.destinationaddress, value.amount).then(result => {
                response.send(result);
            }, error => {
                response.status(500).send(error);
            });
        }
    });
});

如果請求包含源地址,我們將從我們自己的錢包轉賬,否則我們將從交換管理的錢包轉賬。

所有這些都基於我們之前建立的功能。

通過端點,我們可以專注於引導我們的應用程式並得出結論。

引導Express Framework應用程式

現在我們有兩個檔案保持不受示例的影響。我們還沒有新增配置或驅動邏輯來引導我們的端點。

開啟專案的config.json檔案,幷包含以下內容:

{
    "mnemonic": "manage inspire agent october potato thought hospital trim shoulder round tired kangaroo",
    "host": "localhost",
    "bucket": "bitbase",
    "username": "bitbase",
    "password": "123456"
}

記住這個檔案非常敏感。考慮將其鎖定或甚至使用不同的方法。如果種子被暴露,則可以毫不費力地獲得所有使用者帳戶和交換帳戶的每個私鑰。

現在開啟專案的app.js檔案幷包含以下內容:

const Express = require("express");
const BodyParser = require("body-parser");
const Bitcore = require("bitcore-lib");
const Mnemonic = require("bitcore-mnemonic");
const Config = require("./config");
const Helper = require("./classes/helper");

var app = Express();

app.use(BodyParser.json());
app.use(BodyParser.urlencoded({ extended: true }));

var mnemonic = new Mnemonic(Config.mnemonic);
var master = new Bitcore.HDPrivateKey(mnemonic.toHDPrivateKey());

module.exports.helper = new Helper(Config.host, Config.bucket, Config.username, Config.password, master);

require("./routes/account.js")(app);
require("./routes/transaction.js")(app);
require("./routes/utility.js")(app);

var server = app.listen(3000, () => {
    console.log("Listening at :" + server.address().port + "...");
});

我們正在做的是初始化Express,載入配置資訊以及連結我們的路由。module.exports.helper變數是我們的單例,將在每個其他JavaScript檔案中使用。

結論

你剛剛瞭解瞭如何使用Node.js和Couchbase作為NoSQL資料庫來構建自己的加密貨幣交易。我們涵蓋了很多,從生成HD錢包到建立具有複雜資料庫邏輯的端點。

我不能強調這一點。我是加密貨幣愛好者,在金融領域沒有真正的經驗。我分享的東西應該有效,但可以做得更好。不要忘記加密金鑰並確保種子安全。測試你的工作,知道自己正在做什麼。

如果你想下載此專案,請在GitHub上檢視。如果你想分享關於該主題的見解,經驗等,請在評論中分享。社群可以努力創造偉大的東西!

如果你是Golang的粉絲,我在之前的教程中建立了一個類似的專案

======================================================================

分享一些以太坊、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開發比特幣加密貨幣應用程式

相關文章