From 6f615644d67043da4e76b5fb3db57fcd55bc27fc Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 31 May 2019 08:42:57 -0700 Subject: [PATCH] feat(v3): Forked the v2 code and tests into v3 --- package.json | 11 +- src/app.js | 32 + src/routes/v3/address.js | 807 +++++++++++++ src/routes/v3/block.js | 280 +++++ src/routes/v3/blockchain.js | 890 +++++++++++++++ src/routes/v3/control.js | 95 ++ src/routes/v3/generating.js | 51 + src/routes/v3/health-check.js | 11 + src/routes/v3/index.js | 15 + src/routes/v3/logging.js | 51 + src/routes/v3/mining.js | 170 +++ src/routes/v3/network.js | 168 +++ src/routes/v3/rawtransactions.js | 609 ++++++++++ src/routes/v3/route-utils.js | 139 +++ src/routes/v3/services/bitbox.js | 11 + src/routes/v3/services/requestUtils.js | 20 + src/routes/v3/slp.js | 1369 ++++++++++++++++++++++ src/routes/v3/transaction.js | 188 ++++ src/routes/v3/util.js | 173 +++ test/v3/address.js | 1373 +++++++++++++++++++++++ test/v3/block.js | 610 ++++++++++ test/v3/blockchain.js | 1148 +++++++++++++++++++ test/v3/control.js | 140 +++ test/v3/e2e/address.js | 60 + test/v3/helpers/panda.js | 29 + test/v3/integration/README.md | 7 + test/v3/integration/rate-limits.js | 94 ++ test/v3/integration/raw-transactions.js | 191 ++++ test/v3/mining.js | 176 +++ test/v3/mocks/address-mock.js | 160 +++ test/v3/mocks/block-mock.js | 42 + test/v3/mocks/blockchain-mock.js | 271 +++++ test/v3/mocks/control-mock.js | 27 + test/v3/mocks/express-mocks.js | 88 ++ test/v3/mocks/mining-mocks.js | 22 + test/v3/mocks/raw-transactions-mocks.js | 157 +++ test/v3/mocks/slp-mocks.js | 327 ++++++ test/v3/mocks/slpjs-mocks.js | 71 ++ test/v3/mocks/transaction-mocks.js | 74 ++ test/v3/mocks/util-mocks.js | 18 + test/v3/rate-limits.js | 166 +++ test/v3/raw-transactions.js | 807 +++++++++++++ test/v3/slp.js | 971 ++++++++++++++++ test/v3/transaction.js | 333 ++++++ test/v3/util.js | 300 +++++ 45 files changed, 12746 insertions(+), 6 deletions(-) create mode 100644 src/routes/v3/address.js create mode 100644 src/routes/v3/block.js create mode 100644 src/routes/v3/blockchain.js create mode 100644 src/routes/v3/control.js create mode 100644 src/routes/v3/generating.js create mode 100644 src/routes/v3/health-check.js create mode 100644 src/routes/v3/index.js create mode 100644 src/routes/v3/logging.js create mode 100644 src/routes/v3/mining.js create mode 100644 src/routes/v3/network.js create mode 100644 src/routes/v3/rawtransactions.js create mode 100644 src/routes/v3/route-utils.js create mode 100644 src/routes/v3/services/bitbox.js create mode 100644 src/routes/v3/services/requestUtils.js create mode 100644 src/routes/v3/slp.js create mode 100644 src/routes/v3/transaction.js create mode 100644 src/routes/v3/util.js create mode 100644 test/v3/address.js create mode 100644 test/v3/block.js create mode 100644 test/v3/blockchain.js create mode 100644 test/v3/control.js create mode 100644 test/v3/e2e/address.js create mode 100644 test/v3/helpers/panda.js create mode 100644 test/v3/integration/README.md create mode 100644 test/v3/integration/rate-limits.js create mode 100644 test/v3/integration/raw-transactions.js create mode 100644 test/v3/mining.js create mode 100644 test/v3/mocks/address-mock.js create mode 100644 test/v3/mocks/block-mock.js create mode 100644 test/v3/mocks/blockchain-mock.js create mode 100644 test/v3/mocks/control-mock.js create mode 100644 test/v3/mocks/express-mocks.js create mode 100644 test/v3/mocks/mining-mocks.js create mode 100644 test/v3/mocks/raw-transactions-mocks.js create mode 100644 test/v3/mocks/slp-mocks.js create mode 100644 test/v3/mocks/slpjs-mocks.js create mode 100644 test/v3/mocks/transaction-mocks.js create mode 100644 test/v3/mocks/util-mocks.js create mode 100644 test/v3/rate-limits.js create mode 100644 test/v3/raw-transactions.js create mode 100644 test/v3/slp.js create mode 100644 test/v3/transaction.js create mode 100644 test/v3/util.js diff --git a/package.json b/package.json index d949671..4c8b124 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,12 @@ "scripts": { "start": "node ./src/app.js", "dev": "nodemon ./dist/app.js", - "test": "export NETWORK=testnet && nyc --reporter=text mocha --timeout 25000 test/v2/", - "test-v2": "export NETWORK=testnet && npm run build && nyc --reporter=text mocha --require babel-core/register --timeout 25000 test/v2/", - "test-v2-no-build": "export NETWORK=testnet && nyc --reporter=text mocha --require babel-core/register --timeout 25000 test/v2/", - "test-all": "TEST=integration nyc --reporter=text mocha --require babel-core/register --timeout 15000 test/v1/ test/v2/", - "test:integration": "mocha test/v2/integration", + "test": "npm run test-v3", + "test-v2": "export NETWORK=testnet && nyc --reporter=text mocha --timeout 25000 test/v2/", + "test-v3": "export NETWORK=testnet && nyc --reporter=text mocha --timeout 25000 test/v3/", + "test:integration": "mocha test/v3/integration", "coverage": "nyc report --reporter=text-lcov | coveralls", - "coverage:report": "export NETWORK=testnet && npm run build && nyc --reporter=html mocha --require babel-core/register test/v2/" + "coverage:report": "export NETWORK=testnet && nyc --reporter=html mocha test/v2/" }, "engines": { "node": ">=10.15.1" diff --git a/src/app.js b/src/app.js index 7b1a02a..d9baa34 100644 --- a/src/app.js +++ b/src/app.js @@ -46,6 +46,21 @@ const transactionV2 = require("./routes/v2/transaction") const utilV2 = require("./routes/v2/util") const slpV2 = require("./routes/v2/slp") +// v3 +const indexV3 = require("./routes/v3/index") +const healthCheckV3 = require("./routes/v3/health-check") +const addressV3 = require("./routes/v3/address") +const blockV3 = require("./routes/v3/block") +const blockchainV3 = require("./routes/v3/blockchain") +const controlV3 = require("./routes/v3/control") +const generatingV3 = require("./routes/v3/generating") +const miningV3 = require("./routes/v3/mining") +const networkV3 = require("./routes/v3/network") +const rawtransactionsV3 = require("./routes/v3/rawtransactions") +const transactionV3 = require("./routes/v3/transaction") +const utilV3 = require("./routes/v3/util") +const slpV3 = require("./routes/v3/slp") + require("dotenv").config() const app = express() @@ -88,10 +103,12 @@ app.use((req, res, next) => { }) const v2prefix = "v2" +const v3prefix = "v3" // Instantiate the authorization middleware, used to implement pro-tier rate limiting. const auth = new AuthMW() app.use(`/${v2prefix}/`, auth.mw()) +app.use(`/${v3prefix}/`, auth.mw()) // Rate limit on all v2 routes app.use(`/${v2prefix}/`, routeRateLimit) @@ -109,6 +126,21 @@ app.use(`/${v2prefix}/` + `transaction`, transactionV2.router) app.use(`/${v2prefix}/` + `util`, utilV2.router) app.use(`/${v2prefix}/` + `slp`, slpV2.router) +// Rate limit on all v2 routes +app.use(`/${v3prefix}/`, routeRateLimit) +app.use(`/${v3prefix}/` + `health-check`, healthCheckV3) +app.use(`/${v3prefix}/` + `address`, addressV3.router) +app.use(`/${v3prefix}/` + `blockchain`, blockchainV3.router) +app.use(`/${v3prefix}/` + `block`, blockV3.router) +app.use(`/${v3prefix}/` + `control`, controlV3.router) +app.use(`/${v3prefix}/` + `generating`, generatingV3) +app.use(`/${v3prefix}/` + `mining`, miningV3.router) +app.use(`/${v3prefix}/` + `network`, networkV3) +app.use(`/${v3prefix}/` + `rawtransactions`, rawtransactionsV3.router) +app.use(`/${v3prefix}/` + `transaction`, transactionV3.router) +app.use(`/${v3prefix}/` + `util`, utilV3.router) +app.use(`/${v3prefix}/` + `slp`, slpV3.router) + // catch 404 and forward to error handler app.use((req, res, next) => { const err = { diff --git a/src/routes/v3/address.js b/src/routes/v3/address.js new file mode 100644 index 0000000..efce84d --- /dev/null +++ b/src/routes/v3/address.js @@ -0,0 +1,807 @@ +/* + Address route +*/ + +"use strict" + +const express = require("express") +const requestUtils = require("./services/requestUtils") +const axios = require("axios") +const logger = require("./logging.js") +const routeUtils = require("./route-utils") +const wlogger = require("../../util/winston-logging") + +//const router = express.Router() +const router = express.Router() + +// Used for processing error messages before sending them to the user. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +const BITBOXJS = require("@chris.troutner/bitbox-js") +const BITBOX = new BITBOXJS() + +// Use the default (and max) page size of 1000 +// https://github.com/bitpay/insight-api#notes-on-upgrading-from-v03 +const PAGE_SIZE = 1000 + +// Connect the route endpoints to their handler functions. +router.get("/", root) +router.get("/details/:address", detailsSingle) +router.post("/details", detailsBulk) +router.post("/utxo", utxoBulk) +router.get("/utxo/:address", utxoSingle) +router.post("/unconfirmed", unconfirmedBulk) +router.get("/unconfirmed/:address", unconfirmedSingle) +router.get("/transactions/:address", transactionsSingle) +router.post("/transactions", transactionsBulk) +router.get("/fromXPub/:xpub", fromXPubSingle) + +// Root API endpoint. Simply acknowledges that it exists. +function root(req, res, next) { + return res.json({ status: "address" }) +} + +// Query the Insight API for details on a single BCH address. +// Returns a Promise. +async function detailsFromInsight(thisAddress, currentPage = 0) { + try { + let addr + if ( + process.env.BITCOINCOM_BASEURL === "https://bch-insight.bitpay.com/api/" + ) + addr = BITBOX.Address.toCashAddress(thisAddress) + else addr = BITBOX.Address.toLegacyAddress(thisAddress) + + let path = `${process.env.BITCOINCOM_BASEURL}addr/${addr}` + + // Set from and to params based on currentPage and pageSize + // https://github.com/bitpay/insight-api/blob/master/README.md#notes-on-upgrading-from-v02 + const from = currentPage * PAGE_SIZE + const to = from + PAGE_SIZE + path = `${path}?from=${from}&to=${to}` + + // Query the Insight server. + const axiosResponse = await axios.get(path) + const retData = axiosResponse.data + //console.log(`retData: ${util.inspect(retData)}`) + + // Calculate pagesTotal from response + const pagesTotal = Math.ceil(retData.txApperances / PAGE_SIZE) + + // Append different address formats to the return data. + retData.legacyAddress = BITBOX.Address.toLegacyAddress(retData.addrStr) + retData.cashAddress = BITBOX.Address.toCashAddress(retData.addrStr) + delete retData.addrStr + + // Append pagination information to the return data. + retData.currentPage = currentPage + retData.pagesTotal = pagesTotal + + return retData + } catch (err) { + // Dev Note: Do not log error messages here. Throw them instead and let the + // parent function handle it. + throw err + } +} + +// POST handler for bulk queries on address details +// curl -d '{"addresses": ["bchtest:qzjtnzcvzxx7s0na88yrg3zl28wwvfp97538sgrrmr", "bchtest:qp6hgvevf4gzz6l7pgcte3gaaud9km0l459fa23dul"]}' -H "Content-Type: application/json" http://localhost:3000/v2/address/details +// curl -d '{"addresses": ["bchtest:qzjtnzcvzxx7s0na88yrg3zl28wwvfp97538sgrrmr", "bchtest:qp6hgvevf4gzz6l7pgcte3gaaud9km0l459fa23dul"], "from": 1, "to": 5}' -H "Content-Type: application/json" http://localhost:3000/v2/address/details +async function detailsBulk(req, res, next) { + try { + let addresses = req.body.addresses + const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0 + + // Reject if addresses is not an array. + if (!Array.isArray(addresses)) { + res.status(400) + return res.json({ + error: "addresses needs to be an array. Use GET for single address." + }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, addresses)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + logger.debug(`Executing address/details with these addresses: `, addresses) + wlogger.debug(`Executing address/details with these addresses: `, addresses) + + // Validate each element in the address array. + for (let i = 0; i < addresses.length; i++) { + const thisAddress = addresses[i] + + // Ensure the input is a valid BCH address. + try { + BITBOX.Address.toLegacyAddress(thisAddress) + } catch (err) { + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${thisAddress}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const networkIsValid = routeUtils.validateNetwork(thisAddress) + if (!networkIsValid) { + res.status(400) + return res.json({ + error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.` + }) + } + } + + // Loops through each address and creates an array of Promises, querying + // Insight API in parallel. + addresses = addresses.map(async (address, index) => + detailsFromInsight(address, currentPage) + ) + + // Wait for all parallel Insight requests to return. + const result = await axios.all(addresses) + + // Return the array of retrieved address information. + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + //logger.error(`Error in detailsBulk(): `, err) + wlogger.error(`Error in address.ts/detailsBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// GET handler for single address details +async function detailsSingle(req, res, next) { + try { + const address = req.params.address + const currentPage = req.query.page ? parseInt(req.query.page, 10) : 0 + + if (!address || address === "") { + res.status(400) + return res.json({ error: "address can not be empty" }) + } + + // Reject if address is an array. + if (Array.isArray(address)) { + res.status(400) + return res.json({ + error: "address can not be an array. Use POST for bulk upload." + }) + } + + logger.debug(`Executing address/detailsSingle with this address: `, address) + wlogger.debug( + `Executing address/detailsSingle with this address: `, + address + ) + + // Ensure the input is a valid BCH address. + try { + var legacyAddr = BITBOX.Address.toLegacyAddress(address) + } catch (err) { + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${address}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const networkIsValid = routeUtils.validateNetwork(address) + if (!networkIsValid) { + res.status(400) + return res.json({ + error: `Invalid network. Trying to use a testnet address on mainnet, or vice versa.` + }) + } + + // Query the Insight API. + const retData = await detailsFromInsight(address, currentPage) + + // Return the retrieved address information. + res.status(200) + return res.json(retData) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in address.ts/detailsSingle: `, err) + wlogger.error(`Error in address.ts/detailsSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Retrieve UTXO data from the Insight API +async function utxoFromInsight(thisAddress) { + try { + let addr + if ( + process.env.BITCOINCOM_BASEURL === "https://bch-insight.bitpay.com/api/" + ) + addr = BITBOX.Address.toCashAddress(thisAddress) + else addr = BITBOX.Address.toLegacyAddress(thisAddress) + + const path = `${process.env.BITCOINCOM_BASEURL}addr/${addr}/utxo` + + // Query the Insight server. + const response = await axios.get(path) + + // Append different address formats to the return data. + const retData = { + utxos: Array, + legacyAddress: String, + cashAddress: String, + scriptPubKey: String + } + if (response.data.length && response.data[0].scriptPubKey) { + const spk = response.data[0].scriptPubKey + retData.scriptPubKey = spk + } + retData.legacyAddress = BITBOX.Address.toLegacyAddress(thisAddress) + retData.cashAddress = BITBOX.Address.toCashAddress(thisAddress) + retData.utxos = response.data.map(utxo => { + delete utxo.address + delete utxo.scriptPubKey + return utxo + }) + //console.log(`utxoFromInsight retData: ${util.inspect(retData)}`) + + return retData + } catch (err) { + // Dev Note: Do not log error messages here. Throw them instead and let the + // parent function handle it. + throw err + } +} + +// Retrieve UTXO information for an address. +async function utxoBulk(req, res, next) { + try { + let addresses = req.body.addresses + + // Reject if address is not an array. + if (!Array.isArray(addresses)) { + res.status(400) + return res.json({ error: "addresses needs to be an array" }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, addresses)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + // Validate each element in the address array. + for (let i = 0; i < addresses.length; i++) { + const thisAddress = addresses[i] + + // Ensure the input is a valid BCH address. + try { + BITBOX.Address.toLegacyAddress(thisAddress) + } catch (er) { + //if (er.message.includes("Unsupported address format")) + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${thisAddress}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const networkIsValid = routeUtils.validateNetwork(thisAddress) + if (!networkIsValid) { + res.status(400) + return res.json({ + error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.` + }) + } + } + + logger.debug(`Executing address/utxoBulk with these addresses: `, addresses) + wlogger.debug( + `Executing address/utxoBulk with these addresses: `, + addresses + ) + + // Loops through each address and creates an array of Promises, querying + // Insight API in parallel. + addresses = addresses.map(async (address, index) => + utxoFromInsight(address) + ) + + // Wait for all parallel Insight requests to return. + const result = await axios.all(addresses) + + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in address.ts/utxoBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// GET handler for single address details +async function utxoSingle(req, res, next) { + try { + const address = req.params.address + if (!address || address === "") { + res.status(400) + return res.json({ error: "address can not be empty" }) + } + + // Reject if address is an array. + if (Array.isArray(address)) { + res.status(400) + return res.json({ + error: "address can not be an array. Use POST for bulk upload." + }) + } + + logger.debug(`Executing address/utxoSingle with this address: `, address) + wlogger.debug(`Executing address/utxoSingle with this address: `, address) + + // Ensure the input is a valid BCH address. + try { + var legacyAddr = BITBOX.Address.toLegacyAddress(address) + } catch (err) { + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${address}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const networkIsValid = routeUtils.validateNetwork(address) + if (!networkIsValid) { + res.status(400) + return res.json({ + error: `Invalid network. Trying to use a testnet address on mainnet, or vice versa.` + }) + } + + // Query the Insight API. + const retData = await utxoFromInsight(address) + + // Return the array of retrieved address information. + res.status(200) + return res.json(retData) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in address.ts/utxoSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Retrieve any unconfirmed TX information for a given address. +async function unconfirmedBulk(req, res, next) { + try { + const addresses = req.body.addresses + + // Reject if address is not an array. + if (!Array.isArray(addresses)) { + res.status(400) + return res.json({ error: "addresses needs to be an array" }) + } + + logger.debug(`Executing address/utxo with these addresses: `, addresses) + wlogger.debug(`Executing address/utxo with these addresses: `, addresses) + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, addresses)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + // Validate each element in the address array. + for (let i = 0; i < addresses.length; i++) { + const thisAddress = addresses[i] + + // Ensure the input is a valid BCH address. + try { + BITBOX.Address.toLegacyAddress(thisAddress) + } catch (err) { + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${thisAddress}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const networkIsValid = routeUtils.validateNetwork(thisAddress) + if (!networkIsValid) { + res.status(400) + return res.json({ + error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.` + }) + } + } + + // Collect an array of promises. + const promises = addresses.map(address => utxoFromInsight(address)) + + // Wait for all parallel Insight requests to return. + const result = await axios.all(promises) + + // Loop through each result + const finalResult = result.map(elem => { + //console.log(`elem: ${util.inspect(elem)}`) + + // Filter out confirmed transactions. + const unconfirmedUtxos = elem.utxos.filter( + utxo => utxo.confirmations === 0 + ) + + elem.utxos = unconfirmedUtxos + + return elem + }) + + // Return the array of retrieved address information. + res.status(200) + return res.json(finalResult) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in address.ts/unconfirmedBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// GET handler. Retrieve any unconfirmed TX information for a given address. +async function unconfirmedSingle(req, res, next) { + try { + const address = req.params.address + if (!address || address === "") { + res.status(400) + return res.json({ error: "address can not be empty" }) + } + + // Reject if address is an array. + if (Array.isArray(address)) { + res.status(400) + return res.json({ + error: "address can not be an array. Use POST for bulk upload." + }) + } + + logger.debug(`Executing address/utxoSingle with this address: `, address) + wlogger.debug(`Executing address/utxoSingle with this address: `, address) + + // Ensure the input is a valid BCH address. + try { + var legacyAddr = BITBOX.Address.toLegacyAddress(address) + } catch (err) { + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${address}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const networkIsValid = routeUtils.validateNetwork(address) + if (!networkIsValid) { + res.status(400) + return res.json({ + error: `Invalid network. Trying to use a testnet address on mainnet, or vice versa.` + }) + } + + // Query the Insight API. + const retData = await utxoFromInsight(address) + //console.log(`retData: ${JSON.stringify(retData,null,2)}`) + + // Loop through each returned UTXO. + const unconfirmedUTXOs = [] + for (let j = 0; j < retData.utxos.length; j++) { + const thisUtxo = retData.utxos[j] + + // Only interested in UTXOs with no confirmations. + if (thisUtxo.confirmations === 0) unconfirmedUTXOs.push(thisUtxo) + } + + retData.utxos = unconfirmedUTXOs + + // Return the array of retrieved address information. + res.status(200) + return res.json(retData) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in address.ts/unconfirmedSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Retrieve transaction data from the Insight API +async function transactionsFromInsight(thisAddress, currentPage = 0) { + try { + const path = `${ + process.env.BITCOINCOM_BASEURL + }txs/?address=${thisAddress}&pageNum=${currentPage}` + + // Query the Insight server. + const response = await axios.get(path) + + // Append different address formats to the return data. + const retData = response.data + retData.legacyAddress = BITBOX.Address.toLegacyAddress(thisAddress) + retData.cashAddress = BITBOX.Address.toCashAddress(thisAddress) + retData.currentPage = currentPage + + return retData + } catch (err) { + // Dev Note: Do not log error messages here. Throw them instead and let the + // parent function handle it. + throw err + } +} + +// Get an array of TX information for a given address. +async function transactionsBulk(req, res, next) { + try { + let addresses = req.body.addresses + const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0 + + // Reject if address is not an array. + if (!Array.isArray(addresses)) { + res.status(400) + return res.json({ error: "addresses needs to be an array" }) + } + + logger.debug(`Executing address/utxo with these addresses: `, addresses) + wlogger.debug(`Executing address/utxo with these addresses: `, addresses) + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, addresses)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + // Validate each element in the address array. + for (let i = 0; i < addresses.length; i++) { + const thisAddress = addresses[i] + + // Ensure the input is a valid BCH address. + try { + BITBOX.Address.toLegacyAddress(thisAddress) + } catch (err) { + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${thisAddress}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const networkIsValid = routeUtils.validateNetwork(thisAddress) + if (!networkIsValid) { + res.status(400) + return res.json({ + error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.` + }) + } + } + + // Loop through each address and collect an array of promises. + addresses = addresses.map(async (address, index) => + transactionsFromInsight(address, currentPage) + ) + + // Wait for all parallel Insight requests to return. + const result = await axios.all(addresses) + + // Return the array of retrieved address information. + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in address.ts/transactionsBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// GET handler. Retrieve any unconfirmed TX information for a given address. +async function transactionsSingle(req, res, next) { + try { + const address = req.params.address + const currentPage = req.query.page ? parseInt(req.query.page, 10) : 0 + + if (!address || address === "") { + res.status(400) + return res.json({ error: "address can not be empty" }) + } + + // Reject if address is an array. + if (Array.isArray(address)) { + res.status(400) + return res.json({ + error: "address can not be an array. Use POST for bulk upload." + }) + } + + logger.debug( + `Executing address/transactionsSingle with this address: `, + address + ) + wlogger.debug( + `Executing address/transactionsSingle with this address: `, + address + ) + + // Ensure the input is a valid BCH address. + try { + var legacyAddr = BITBOX.Address.toLegacyAddress(address) + } catch (err) { + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${address}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const networkIsValid = routeUtils.validateNetwork(address) + if (!networkIsValid) { + res.status(400) + return res.json({ + error: `Invalid network. Trying to use a testnet address on mainnet, or vice versa.` + }) + } + + // Query the Insight API. + const retData = await transactionsFromInsight(address, currentPage) + //console.log(`retData: ${JSON.stringify(retData,null,2)}`) + + // Return the array of retrieved address information. + res.status(200) + return res.json(retData) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in address.ts/transactionsSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function fromXPubSingle(req, res, next) { + try { + const xpub = req.params.xpub + const hdPath = req.query.hdPath ? req.query.hdPath : "0" + + if (!xpub || xpub === "") { + res.status(400) + return res.json({ error: "xpub can not be empty" }) + } + + // Reject if xpub is an array. + if (Array.isArray(xpub)) { + res.status(400) + return res.json({ + error: "xpub can not be an array. Use POST for bulk upload." + }) + } + + logger.debug(`Executing address/fromXPub with this xpub: `, xpub) + wlogger.debug(`Executing address/fromXPub with this xpub: `, xpub) + + const cashAddr = BITBOX.Address.fromXPub(xpub, hdPath) + const legacyAddr = BITBOX.Address.toLegacyAddress(cashAddr) + res.status(200) + return res.json({ + cashAddress: cashAddr, + legacyAddress: legacyAddr + }) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in address.ts/fromXPubSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +module.exports = { + router, + testableComponents: { + root, + detailsBulk, + detailsSingle, + utxoBulk, + utxoSingle, + unconfirmedBulk, + unconfirmedSingle, + transactionsBulk, + transactionsSingle, + fromXPubSingle + } +} diff --git a/src/routes/v3/block.js b/src/routes/v3/block.js new file mode 100644 index 0000000..500687f --- /dev/null +++ b/src/routes/v3/block.js @@ -0,0 +1,280 @@ +"use strict" + +const express = require("express") +const requestUtils = require("./services/requestUtils") +const axios = require("axios") +const bitbox = require("./services/bitbox") +const logger = require("./logging.js") +const wlogger = require("../../util/winston-logging") + +const routeUtils = require("./route-utils") + +// Used for processing error messages before sending them to the user. +const util = require("util") +util.inspect.defaultOptions = { depth: 3 } + +const router = express.Router() +//const BitboxHTTP = bitbox.getInstance() + +router.get("/", root) +router.get("/detailsByHash/:hash", detailsByHashSingle) +router.post("/detailsByHash", detailsByHashBulk) +router.get("/detailsByHeight/:height", detailsByHeightSingle) +router.post("/detailsByHeight", detailsByHeightBulk) + +function root(req, res, next) { + return res.json({ status: "block" }) +} + +// Call the insight server to get block details based on the hash. +async function detailsByHashSingle(req, res, next) { + try { + const hash = req.params.hash + + // Reject if hash is empty + if (!hash || hash === "") { + res.status(400) + return res.json({ error: "hash must not be empty" }) + } + + const response = await axios.get( + `${process.env.BITCOINCOM_BASEURL}block/${hash}` + ) + //console.log(`response.data: ${JSON.stringify(response.data,null,2)}`) + + const parsed = response.data + return res.json(parsed) + } catch (error) { + //console.log(`error object: ${util.inspect(error)}`) + + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(error) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + if (error.response && error.response.status === 404) { + res.status(404) + return res.json({ error: "Not Found" }) + } + + // Write out error to error log. + //logger.error(`Error in block/detailsByHash: `, error) + wlogger.error(`Error in block.ts/detailsByHashSingle().`, error) + + res.status(500) + return res.json({ error: util.inspect(error) }) + } +} + +async function detailsByHashBulk(req, res, next) { + try { + const hashes = req.body.hashes + + // Reject if hashes is not an array. + if (!Array.isArray(hashes)) { + res.status(400) + return res.json({ + error: "hashes needs to be an array. Use GET for single address." + }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, hashes)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + // Validate each hash in the array. + for (let i = 0; i < hashes.length; i++) { + const thisHash = hashes[i] + + if (thisHash.length !== 64) { + res.status(400) + return res.json({ + error: `Invalid hash. Double check your hash is valid: ${thisHash}` + }) + } + } + + // Loop through each hash and creates an array of promises + const axiosPromises = hashes.map(async hash => + axios.get(`${process.env.BITCOINCOM_BASEURL}block/${hash}`) + ) + + // Wait for all parallel promises to return. + const axiosResult = await axios.all(axiosPromises) + + // Extract the data component from the axios response. + const result = axiosResult.map(x => x.data) + //console.log(`result: ${util.inspect(result)}`) + + res.status(200) + return res.json(result) + } catch (error) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(error) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + if (error.response && error.response.status === 404) { + res.status(404) + return res.json({ error: "Not Found" }) + } + + // Write out error to error log. + //logger.error(`Error in block/detailsByHash: `, error) + wlogger.error(`Error in block.ts/detailsByHashBulk().`, error) + + res.status(500) + return res.json({ error: util.inspect(error) }) + } +} + +// Call the Full Node to get block hash based on height, then call the Insight +// server to get details from that hash. +async function detailsByHeightSingle(req, res, next) { + try { + const height = req.params.height + + // Reject if id is empty + if (!height || height === "") { + res.status(400) + return res.json({ error: "height must not be empty" }) + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "getblockhash" + requestConfig.data.method = "getblockhash" + requestConfig.data.params = [parseInt(height)] + + const response = await BitboxHTTP(requestConfig) + + const hash = response.data.result + //console.log(`response.data: ${util.inspect(response.data)}`) + + // Call detailsByHashSingle now that the hash has been retrieved. + req.params.hash = hash + return detailsByHashSingle(req, res, next) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in control/getInfo: `, error) + wlogger.error(`Error in block.ts/detailsByHeightSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function detailsByHeightBulk(req, res, next) { + try { + const heights = req.body.heights + + // Reject if heights is not an array. + if (!Array.isArray(heights)) { + res.status(400) + return res.json({ + error: "heights needs to be an array. Use GET for single height." + }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, heights)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + logger.debug(`Executing detailsByHeight with these heights: `, heights) + + // Validate each element in the address array. + for (let i = 0; i < heights.length; i++) { + const thisHeight = heights[i] + + // Reject if id is empty + if (!thisHeight || thisHeight === "") { + res.status(400) + return res.json({ error: "height must not be empty" }) + } + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + // Loop through each height and creates an array of requests to call in parallel + const promises = heights.map(async height => { + requestConfig.data.id = "getblockhash" + requestConfig.data.method = "getblockhash" + requestConfig.data.params = [parseInt(height)] + + const response = await BitboxHTTP(requestConfig) + + const hash = response.data.result + + const axiosResult = await axios.get( + `${process.env.BITCOINCOM_BASEURL}block/${hash}` + ) + + return axiosResult.data + }) + + // Wait for all parallel Insight requests to return. + const result = await axios.all(promises) + + res.status(200) + return res.json(result) + } catch (error) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(error) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + if (error.response && error.response.status === 404) { + res.status(404) + return res.json({ error: "Not Found" }) + } + + // Write out error to error log. + //logger.error(`Error in block/detailsByHash: `, error) + wlogger.error(`Error in block.ts/detailsByHeightBulk().`, error) + + res.status(500) + return res.json({ error: util.inspect(error) }) + } +} + +module.exports = { + router, + testableComponents: { + root, + detailsByHashSingle, + detailsByHashBulk, + detailsByHeightSingle, + detailsByHeightBulk + } +} diff --git a/src/routes/v3/blockchain.js b/src/routes/v3/blockchain.js new file mode 100644 index 0000000..65d772d --- /dev/null +++ b/src/routes/v3/blockchain.js @@ -0,0 +1,890 @@ +/* + TODO + - Add blockhash functionality back into getTxOutProof +*/ + +"use strict" + +const express = require("express") +const router = express.Router() +const axios = require("axios") + +const routeUtils = require("./route-utils") +const logger = require("./logging.js") +const wlogger = require("../../util/winston-logging") + +// Used to convert error messages to strings, to safely pass to users. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +// Define routes. +router.get("/", root) +router.get("/getBestBlockHash", getBestBlockHash) +// Dev Note: getBlock/:hash ommited because its the same as block/detailsByHash +//router.get("/getBlock/:hash", getBlock) +router.get("/getBlockchainInfo", getBlockchainInfo) +router.get("/getBlockCount", getBlockCount) +router.get("/getBlockHeader/:hash", getBlockHeaderSingle) +router.post("/getBlockHeader", getBlockHeaderBulk) + +router.get("/getChainTips", getChainTips) +router.get("/getDifficulty", getDifficulty) +router.get("/getMempoolEntry/:txid", getMempoolEntrySingle) +router.post("/getMempoolEntry", getMempoolEntryBulk) +router.get("/getMempoolInfo", getMempoolInfo) +router.get("/getRawMempool", getRawMempool) +router.get("/getTxOut/:txid/:n", getTxOut) +router.get("/getTxOutProof/:txid", getTxOutProofSingle) +router.post("/getTxOutProof", getTxOutProofBulk) +router.get("/verifyTxOutProof/:proof", verifyTxOutProofSingle) +router.post("/verifyTxOutProof", verifyTxOutProofBulk) + +function root(req, res, next) { + return res.json({ status: "blockchain" }) +} + +// Returns the hash of the best (tip) block in the longest block chain. +async function getBestBlockHash(req, res, next) { + try { + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "getbestblockhash" + requestConfig.data.method = "getbestblockhash" + requestConfig.data.params = [] + + const response = await BitboxHTTP(requestConfig) + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getBestBlockHash().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function getBlockchainInfo(req, res, next) { + try { + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "getblockchaininfo" + requestConfig.data.method = "getblockchaininfo" + requestConfig.data.params = [] + + const response = await BitboxHTTP(requestConfig) + + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getBlockchainInfo().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function getBlockCount(req, res, next) { + try { + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "getblockcount" + requestConfig.data.method = "getblockcount" + requestConfig.data.params = [] + + const response = await BitboxHTTP(requestConfig) + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getBlockCount().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function getBlockHeaderSingle(req, res, next) { + try { + let verbose = false + if (req.query.verbose && req.query.verbose.toString() === "true") + verbose = true + + const hash = req.params.hash + if (!hash || hash === "") { + res.status(400) + return res.json({ error: "hash can not be empty" }) + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "getblockheader" + requestConfig.data.method = "getblockheader" + requestConfig.data.params = [hash, verbose] + + const response = await BitboxHTTP(requestConfig) + + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getBlockHeaderSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function getBlockHeaderBulk(req, res, next) { + try { + const hashes = req.body.hashes + const verbose = req.body.verbose ? req.body.verbose : false + + if (!Array.isArray(hashes)) { + res.status(400) + return res.json({ + error: "hashes needs to be an array. Use GET for single hash." + }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, hashes)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + logger.debug( + `Executing blockchain/getBlockHeaderBulk with these hashes: `, + hashes + ) + + // Validate each hash in the array. + for (let i = 0; i < hashes.length; i++) { + const hash = hashes[i] + + if (hash.length !== 64) { + res.status(400) + return res.json({ error: `This is not a hash: ${hash}` }) + } + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + // Loop through each hash and creates an array of requests to call in parallel + const promises = hashes.map(async hash => { + requestConfig.data.id = "getblockheader" + requestConfig.data.method = "getblockheader" + requestConfig.data.params = [hash, verbose] + + return await BitboxHTTP(requestConfig) + }) + + const axiosResult = await axios.all(promises) + + // Extract the data component from the axios response. + const result = axiosResult.map(x => x.data.result) + + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getBlockHeaderBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function getChainTips(req, res, next) { + try { + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "getchaintips" + requestConfig.data.method = "getchaintips" + requestConfig.data.params = [] + + const response = await BitboxHTTP(requestConfig) + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getChainTips().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Get the current difficulty value, used to regulate mining power on the network. +async function getDifficulty(req, res, next) { + try { + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "getdifficulty" + requestConfig.data.method = "getdifficulty" + requestConfig.data.params = [] + + const response = await BitboxHTTP(requestConfig) + + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getDifficulty().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Returns mempool data for given transaction. TXID must be in mempool (unconfirmed) +async function getMempoolEntrySingle(req, res, next) { + try { + // Validate input parameter + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "getmempoolentry" + requestConfig.data.method = "getmempoolentry" + requestConfig.data.params = [txid] + + const response = await BitboxHTTP(requestConfig) + + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getMempoolEntrySingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function getMempoolEntryBulk(req, res, next) { + try { + const txids = req.body.txids + + if (!Array.isArray(txids)) { + res.status(400) + return res.json({ + error: "txids needs to be an array. Use GET for single txid." + }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, txids)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + logger.debug( + `Executing blockchain/getMempoolEntry with these txids: `, + txids + ) + + // Validate each element in the array + for (let i = 0; i < txids.length; i++) { + const txid = txids[i] + + if (txid.length !== 64) { + res.status(400) + return res.json({ error: "This is not a txid" }) + } + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + // Loop through each txid and creates an array of requests to call in parallel + const promises = txids.map(async txid => { + requestConfig.data.id = "getmempoolentry" + requestConfig.data.method = "getmempoolentry" + requestConfig.data.params = [txid] + + return await BitboxHTTP(requestConfig) + }) + + const axiosResult = await axios.all(promises) + + // Extract the data component from the axios response. + const result = axiosResult.map(x => x.data.result) + + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getMempoolEntryBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function getMempoolInfo(req, res, next) { + try { + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "getmempoolinfo" + requestConfig.data.method = "getmempoolinfo" + requestConfig.data.params = [] + + const response = await BitboxHTTP(requestConfig) + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getMempoolInfo().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function getRawMempool(req, res, next) { + try { + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + let verbose = false + if (req.query.verbose && req.query.verbose === "true") verbose = true + + requestConfig.data.id = "getrawmempool" + requestConfig.data.method = "getrawmempool" + requestConfig.data.params = [verbose] + + const response = await BitboxHTTP(requestConfig) + + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getRawMempool().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Returns details about an unspent transaction output. +async function getTxOut(req, res, next) { + try { + // Validate input parameter + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + let n = req.params.n + if (n === undefined || n === "") { + res.status(400) + return res.json({ error: "n can not be empty" }) + } + n = parseInt(n) + + let include_mempool = false + if (req.query.include_mempool && req.query.include_mempool === "true") + include_mempool = true + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "gettxout" + requestConfig.data.method = "gettxout" + requestConfig.data.params = [txid, n, include_mempool] + + const response = await BitboxHTTP(requestConfig) + + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getTxOut().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Returns a hex-encoded proof that 'txid' was included in a block. +async function getTxOutProofSingle(req, res, next) { + try { + // Validate input parameter + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "gettxoutproof" + requestConfig.data.method = "gettxoutproof" + requestConfig.data.params = [[txid]] + + const response = await BitboxHTTP(requestConfig) + + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getTxOutProofSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Returns a hex-encoded proof that 'txid' was included in a block. +async function getTxOutProofBulk(req, res, next) { + try { + const txids = req.body.txids + + // Reject if txids is not an array. + if (!Array.isArray(txids)) { + res.status(400) + return res.json({ + error: "txids needs to be an array. Use GET for single txid." + }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, txids)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + // Validate each element in the array. + for (let i = 0; i < txids.length; i++) { + const txid = txids[i] + + if (txid.length !== 64) { + res.status(400) + return res.json({ + error: `Invalid txid. Double check your txid is valid: ${txid}` + }) + } + } + + logger.debug(`Executing blockchain/getTxOutProof with these txids: `, txids) + + // Loop through each txid and creates an array of requests to call in parallel + const promises = txids.map(async txid => { + requestConfig.data.id = "gettxoutproof" + requestConfig.data.method = "gettxoutproof" + requestConfig.data.params = [[txid]] + + return await BitboxHTTP(requestConfig) + }) + + // Wait for all parallel promisses to resolve. + const axiosResult = await axios.all(promises) + + // Extract the data component from the axios response. + const result = axiosResult.map(x => x.data.result) + + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getTxOutProofBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +/* +// +// router.get('/preciousBlock/:hash', async (req, res, next) => { +// BitboxHTTP({ +// method: 'post', +// auth: { +// username: username, +// password: password +// }, +// data: { +// jsonrpc: "1.0", +// id:"preciousblock", +// method: "preciousblock", +// params: [ +// req.params.hash +// ] +// } +// }) +// .then((response) => { +// res.json(JSON.stringify(response.data.result)); +// }) +// .catch((error) => { +// res.send(error.response.data.error.message); +// }); +// }); +// +// router.post('/pruneBlockchain/:height', async (req, res, next) => { +// BitboxHTTP({ +// method: 'post', +// auth: { +// username: username, +// password: password +// }, +// data: { +// jsonrpc: "1.0", +// id:"pruneblockchain", +// method: "pruneblockchain", +// params: [ +// req.params.height +// ] +// } +// }) +// .then((response) => { +// res.json(response.data.result); +// }) +// .catch((error) => { +// res.send(error.response.data.error.message); +// }); +// }); +// +// router.get('/verifyChain', async (req, res, next) => { +// BitboxHTTP({ +// method: 'post', +// auth: { +// username: username, +// password: password +// }, +// data: { +// jsonrpc: "1.0", +// id:"verifychain", +// method: "verifychain" +// } +// }) +// .then((response) => { +// res.json(response.data.result); +// }) +// .catch((error) => { +// res.send(error.response.data.error.message); +// }); +// }); +*/ + +async function verifyTxOutProofSingle(req, res, next) { + try { + // Validate input parameter + const proof = req.params.proof + if (!proof || proof === "") { + res.status(400) + return res.json({ error: "proof can not be empty" }) + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "verifytxoutproof" + requestConfig.data.method = "verifytxoutproof" + requestConfig.data.params = [req.params.proof] + + const response = await BitboxHTTP(requestConfig) + + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/verifyTxOutProofSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function verifyTxOutProofBulk(req, res, next) { + try { + const proofs = req.body.proofs + + // Reject if proofs is not an array. + if (!Array.isArray(proofs)) { + res.status(400) + return res.json({ + error: "proofs needs to be an array. Use GET for single proof." + }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, proofs)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + // Validate each element in the array. + for (let i = 0; i < proofs.length; i++) { + const proof = proofs[i] + + if (!proof || proof === "") { + res.status(400) + return res.json({ error: `proof can not be empty: ${proof}` }) + } + } + + logger.debug( + `Executing blockchain/verifyTxOutProof with these proofs: `, + proofs + ) + + // Loop through each proof and creates an array of requests to call in parallel + const promises = proofs.map(async proof => { + requestConfig.data.id = "verifytxoutproof" + requestConfig.data.method = "verifytxoutproof" + requestConfig.data.params = [proof] + + return await BitboxHTTP(requestConfig) + }) + + // Wait for all parallel promisses to resolve. + const axiosResult = await axios.all(promises) + + // Extract the data component from the axios response. + const result = axiosResult.map(x => x.data.result[0]) + + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/verifyTxOutProofBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +module.exports = { + router, + testableComponents: { + root, + getBestBlockHash, + //getBlock, + getBlockchainInfo, + getBlockCount, + getBlockHeaderSingle, + getBlockHeaderBulk, + getChainTips, + getDifficulty, + getMempoolInfo, + getRawMempool, + getMempoolEntrySingle, + getMempoolEntryBulk, + getTxOut, + getTxOutProofSingle, + getTxOutProofBulk, + verifyTxOutProofSingle, + verifyTxOutProofBulk + } +} diff --git a/src/routes/v3/control.js b/src/routes/v3/control.js new file mode 100644 index 0000000..fabc233 --- /dev/null +++ b/src/routes/v3/control.js @@ -0,0 +1,95 @@ +"use strict" + +const express = require("express") +const router = express.Router() +const axios = require("axios") + +const logger = require("./logging.js") +const routeUtils = require("./route-utils") +const wlogger = require("../../util/winston-logging") + +// Used for processing error messages before sending them to the user. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +router.get("/", root) +router.get("/getInfo", getInfo) + +function root(req, res, next) { + return res.json({ status: "control" }) +} + +// Execute the RPC getinfo call. +async function getInfo(req, res, next) { + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "getinfo" + requestConfig.data.method = "getinfo" + requestConfig.data.params = [] + + try { + const response = await BitboxHTTP(requestConfig) + + return res.json(response.data.result) + } catch (error) { + wlogger.error(`Error in control.ts/getInfo().`, error) + + // Write out error to error log. + //logger.error(`Error in control/getInfo: `, error) + + res.status(500) + if (error.response && error.response.data && error.response.data.error) + return res.json({ error: error.response.data.error }) + return res.json({ error: util.inspect(error) }) + } +} + +// router.get('/getMemoryInfo', (req, res, next) => { +// BitboxHTTP({ +// method: 'post', +// auth: { +// username: username, +// password: password +// }, +// data: { +// jsonrpc: "1.0", +// id:"getmemoryinfo", +// method: "getmemoryinfo" +// } +// }) +// .then((response) => { +// res.json(response.data.result); +// }) +// .catch((error) => { +// res.send(error.response.data.error.message); +// }); +// }); +// +// router.get('/help', (req, res, next) => { +// BITBOX.Control.help() +// .then((result) => { +// res.json(result); +// }, (err) => { console.log(err); +// }); +// }); +// +// router.post('/stop', (req, res, next) => { +// BITBOX.Control.stop() +// .then((result) => { +// res.json(result); +// }, (err) => { console.log(err); +// }); +// }); + +module.exports = { + router, + testableComponents: { + root, + getInfo + } +} diff --git a/src/routes/v3/generating.js b/src/routes/v3/generating.js new file mode 100644 index 0000000..ec0c601 --- /dev/null +++ b/src/routes/v3/generating.js @@ -0,0 +1,51 @@ +"use strict" + +const express = require("express") +const router = express.Router() +//const axios = require("axios"); + +//const BITBOXCli = require("bitbox-sdk/lib/bitbox-sdk").default; +//const BITBOX = new BITBOXCli(); + +//const BitboxHTTP = axios.create({ +// baseURL: process.env.RPC_BASEURL, +//}); +//const username = process.env.RPC_USERNAME; +//const password = process.env.RPC_PASSWORD; + +router.get("/", (req, res, next) => { + res.json({ status: "generating" }) +}) +// +// router.post('/generateToAddress/:nblocks/:address', (req, res, next) => { +// let maxtries = 1000000; +// if(req.query.maxtries) { +// maxtries = parseInt(req.query.maxtries); +// } +// +// BitboxHTTP({ +// method: 'post', +// auth: { +// username: username, +// password: password +// }, +// data: { +// jsonrpc: "1.0", +// id:"generatetoaddress", +// method: "generatetoaddress", +// params: [ +// req.params.nblocks, +// req.params.address, +// maxtries +// ] +// } +// }) +// .then((response) => { +// res.json(response.data.result); +// }) +// .catch((error) => { +// res.send(error.response.data.error.message); +// }); +// }); + +module.exports = router diff --git a/src/routes/v3/health-check.js b/src/routes/v3/health-check.js new file mode 100644 index 0000000..8c05030 --- /dev/null +++ b/src/routes/v3/health-check.js @@ -0,0 +1,11 @@ +"use strict" + +const express = require("express") +const router = express.Router() + +/* GET home page. */ +router.get("/", (req, res, next) => { + res.json({ status: "winning v2" }) +}) + +module.exports = router diff --git a/src/routes/v3/index.js b/src/routes/v3/index.js new file mode 100644 index 0000000..33c175f --- /dev/null +++ b/src/routes/v3/index.js @@ -0,0 +1,15 @@ +"use strict" + +const express = require("express") +const router = express.Router() + +/* GET home page. */ +router.get("/", (req, res, next) => { + res.render("swagger-v2") +}) + +router.get("/v2", (req, res, next) => { + res.render("swagger-v2") +}) + +module.exports = router diff --git a/src/routes/v3/logging.js b/src/routes/v3/logging.js new file mode 100644 index 0000000..07fd9be --- /dev/null +++ b/src/routes/v3/logging.js @@ -0,0 +1,51 @@ +/* + A utility library for setting up greylog2 logging. +*/ + +"use strict" + +// This will be uncommented and correct once we have our logging server functioning. +/* +var graylog2 = require("graylog2"); +var logger = new graylog2.graylog({ + servers: [ + { 'host': '127.0.0.1', port: 12201 }, + { 'host': '127.0.0.2', port: 12201 } + ], + hostname: 'server.name', // the name of this host + // (optional, default: os.hostname()) + facility: 'Node.js', // the facility for these log messages + // (optional, default: "Node.js") + bufferSize: 1350 // max UDP packet size, should never exceed the + // MTU of your system (optional, default: 1400) +}); + +logger.on('error', function (error) { + console.error('Error while trying to write to graylog2:', error); +}); +*/ + +// This is just a placeholder function that will be replaced once we get the +// greylog server working. +function log(msg, obj) { + //console.log(msg, obj) +} + +// This is just a placeholder function that will be replaced once we get the +// greylog server working. +function error(msg, obj) { + if (!obj) console.error(msg) + else console.error(msg, obj) +} + +// This is just a placeholder function that will be replaced once we get the +// greylog server working. +function debug(msg, obj) { + //console.log(msg, obj) +} + +module.exports = { + log, + error, + debug +} diff --git a/src/routes/v3/mining.js b/src/routes/v3/mining.js new file mode 100644 index 0000000..243b75a --- /dev/null +++ b/src/routes/v3/mining.js @@ -0,0 +1,170 @@ +"use strict" + +const express = require("express") +const router = express.Router() +const axios = require("axios") + +const routeUtils = require("./route-utils") +const logger = require("./logging.js") +const wlogger = require("../../util/winston-logging") + +// Used to convert error messages to strings, to safely pass to users. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +const BitboxHTTP = axios.create({ + baseURL: process.env.RPC_BASEURL +}) +const username = process.env.RPC_USERNAME +const password = process.env.RPC_PASSWORD + +const requestConfig = { + method: "post", + auth: { + username: username, + password: password + }, + data: { + jsonrpc: "1.0" + } +} + +router.get("/", root) +router.get("/getMiningInfo", getMiningInfo) +router.get("/getNetworkHashps", getNetworkHashPS) + +function root(req, res, next) { + return res.json({ status: "mining" }) +} + +// +// router.get('/getBlockTemplate/:templateRequest', (req, res, next) => { +// BitboxHTTP({ +// method: 'post', +// auth: { +// username: username, +// password: password +// }, +// data: { +// jsonrpc: "1.0", +// id:"getblocktemplate", +// method: "getblocktemplate", +// params: [ +// req.params.templateRequest +// ] +// } +// }) +// .then((response) => { +// res.json(response.data.result); +// }) +// .catch((error) => { +// res.send(error.response.data.error.message); +// }); +// }); + +async function getMiningInfo(req, res, next) { + try { + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "getmininginfo" + requestConfig.data.method = "getmininginfo" + requestConfig.data.params = [] + + const response = await BitboxHTTP(requestConfig) + + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + wlogger.error(`Error in mining.ts/getMiningInfo().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function getNetworkHashPS(req, res, next) { + try { + let nblocks = 120 // Default + let height = -1 // Default + if (req.query.nblocks) nblocks = parseInt(req.query.nblocks) + if (req.query.height) height = parseInt(req.query.height) + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "getnetworkhashps" + requestConfig.data.method = "getnetworkhashps" + requestConfig.data.params = [nblocks, height] + + const response = await BitboxHTTP(requestConfig) + + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + wlogger.error(`Error in mining.ts/getNetworkHashPS().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// +// router.post('/submitBlock/:hex', (req, res, next) => { +// let parameters = ''; +// if(req.query.parameters && req.query.parameters !== '') { +// parameters = true; +// } +// +// BitboxHTTP({ +// method: 'post', +// auth: { +// username: username, +// password: password +// }, +// data: { +// jsonrpc: "1.0", +// id:"submitblock", +// method: "submitblock", +// params: [ +// req.params.hex, +// parameters +// ] +// } +// }) +// .then((response) => { +// res.json(response.data.result); +// }) +// .catch((error) => { +// res.send(error.response.data.error.message); +// }); +// }); + +module.exports = { + router, + testableComponents: { + root, + getMiningInfo, + getNetworkHashPS + } +} diff --git a/src/routes/v3/network.js b/src/routes/v3/network.js new file mode 100644 index 0000000..0af2366 --- /dev/null +++ b/src/routes/v3/network.js @@ -0,0 +1,168 @@ +"use strict" + +const express = require("express") +const router = express.Router() + +router.get("/", async (req, res, next) => { + res.json({ status: "network" }) +}) + +// router.post('/addNode/:node/:command', (req, res, next) => { +// BITBOX.Network.addNode(req.params.node, req.params.command) +// .then((result) => { +// res.json(result); +// }, (err) => { console.log(err); +// }); +// }); +// +// router.post('/clearBanned', (req, res, next) => { +// BITBOX.Network.clearBanned() +// .then((result) => { +// res.json(result); +// }, (err) => { console.log(err); +// }); +// }); +// +// router.post('/disconnectNode/:address/:nodeid', (req, res, next) => { +// BITBOX.Network.disconnectNode(req.params.address, req.params.nodeid) +// .then((result) => { +// res.json(result); +// }, (err) => { console.log(err); +// }); +// }); +// +// router.get('/getAddedNodeInfo/:node', (req, res, next) => { +// BITBOX.Network.getAddedNodeInfo(req.params.node) +// .then((result) => { +// res.json(result); +// }, (err) => { console.log(err); +// }); +// }); +// +// router.get('/getConnectionCount', (req, res, next) => { +// BitboxHTTP({ +// method: 'post', +// auth: { +// username: username, +// password: password +// }, +// data: { +// jsonrpc: "1.0", +// id:"getconnectioncount", +// method: "getconnectioncount" +// } +// }) +// .then((response) => { +// res.json(response.data.result); +// }) +// .catch((error) => { +// res.send(error.response.data.error.message); +// }); +// }); +// +// router.get('/getNetTotals', (req, res, next) => { +// BitboxHTTP({ +// method: 'post', +// auth: { +// username: username, +// password: password +// }, +// data: { +// jsonrpc: "1.0", +// id:"getnettotals", +// method: "getnettotals" +// } +// }) +// .then((response) => { +// res.json(response.data.result); +// }) +// .catch((error) => { +// res.send(error.response.data.error.message); +// }); +// }); +// +// router.get('/getNetworkInfo', (req, res, next) => { +// BitboxHTTP({ +// method: 'post', +// auth: { +// username: username, +// password: password +// }, +// data: { +// jsonrpc: "1.0", +// id:"getnetworkinfo", +// method: "getnetworkinfo" +// } +// }) +// .then((response) => { +// res.json(response.data.result); +// }) +// .catch((error) => { +// res.send(error.response.data.error.message); +// }); +// }); +// +// router.get('/getPeerInfo', (req, res, next) => { +// BitboxHTTP({ +// method: 'post', +// auth: { +// username: username, +// password: password +// }, +// data: { +// jsonrpc: "1.0", +// id:"getpeerinfo", +// method: "getpeerinfo" +// } +// }) +// .then((response) => { +// res.json(response.data.result); +// }) +// .catch((error) => { +// res.send(error.response.data.error.message); +// }); +// }); +// +// router.get('/ping', (req, res, next) => { +// BitboxHTTP({ +// method: 'post', +// auth: { +// username: username, +// password: password +// }, +// data: { +// jsonrpc: "1.0", +// id:"ping", +// method: "ping" +// } +// }) +// .then((response) => { +// res.json(JSON.stringify(response.data.result)); +// }) +// .catch((error) => { +// res.send(error.response.data.error.message); +// }); +// }); +// +// router.post('/setBan/:subnet/:command', (req, res, next) => { +// // TODO finish this +// BITBOX.Network.getConnectionCount(req.params.subnet, req.params.command) +// .then((result) => { +// res.json(result); +// }, (err) => { console.log(err); +// }); +// }); +// +// router.post('/setNetworkActive/:state', (req, res, next) => { +// let state = true; +// if(req.params.state && req.params.state === 'false') { +// state = false; +// } +// BITBOX.Network.getConnectionCount(state) +// .then((result) => { +// res.json(result); +// }, (err) => { console.log(err); +// }); +// }); + +module.exports = router diff --git a/src/routes/v3/rawtransactions.js b/src/routes/v3/rawtransactions.js new file mode 100644 index 0000000..99104c7 --- /dev/null +++ b/src/routes/v3/rawtransactions.js @@ -0,0 +1,609 @@ +"use strict" + +const express = require("express") +const router = express.Router() +const axios = require("axios") + +const routeUtils = require("./route-utils") +const logger = require("./logging.js") +const wlogger = require("../../util/winston-logging") + +// Used to convert error messages to strings, to safely pass to users. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +const BitboxHTTP = axios.create({ + baseURL: process.env.RPC_BASEURL +}) +const username = process.env.RPC_USERNAME +const password = process.env.RPC_PASSWORD + +const requestConfig = { + method: "post", + auth: { + username: username, + password: password + }, + data: { + jsonrpc: "1.0" + } +} + +router.get("/", root) +router.get("/decodeRawTransaction/:hex", decodeRawTransactionSingle) +router.post("/decodeRawTransaction", decodeRawTransactionBulk) +router.get("/decodeScript/:hex", decodeScriptSingle) +router.post("/decodeScript", decodeScriptBulk) +router.post("/getRawTransaction", getRawTransactionBulk) +router.get("/getRawTransaction/:txid", getRawTransactionSingle) +router.post("/sendRawTransaction", sendRawTransactionBulk) +router.get("/sendRawTransaction/:hex", sendRawTransactionSingle) + +function root(req, res, next) { + return res.json({ status: "rawtransactions" }) +} + +// Decode transaction hex into a JSON object. +// GET +async function decodeRawTransactionSingle(req, res, next) { + try { + const hex = req.params.hex + + // Throw an error if hex is empty. + if (!hex || hex === "") { + res.status(400) + return res.json({ error: "hex can not be empty" }) + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "decoderawtransaction" + requestConfig.data.method = "decoderawtransaction" + requestConfig.data.params = [hex] + + const response = await BitboxHTTP(requestConfig) + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error( + `Error in rawtransactions.ts/decodeRawTransactionSingle().`, + err + ) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function decodeRawTransactionBulk(req, res, next) { + try { + const hexes = req.body.hexes + + if (!Array.isArray(hexes)) { + res.status(400) + return res.json({ error: "hexes must be an array" }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, hexes)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + const results = [] + + // Validate each element in the address array. + for (let i = 0; i < hexes.length; i++) { + const thisHex = hexes[i] + + // Reject if id is empty + if (!thisHex || thisHex === "") { + res.status(400) + return res.json({ error: "Encountered empty hex" }) + } + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + // Loop through each height and creates an array of requests to call in parallel + const promises = hexes.map(async hex => { + requestConfig.data.id = "decoderawtransaction" + requestConfig.data.method = "decoderawtransaction" + requestConfig.data.params = [hex] + + return await BitboxHTTP(requestConfig) + }) + + // Wait for all parallel Insight requests to return. + const axiosResult = await axios.all(promises) + + // Retrieve the data part of the result. + const result = axiosResult.map(x => x.data.result) + + res.status(200) + return res.json(result) + + /* + // Loop through each hex and creates an array of requests to call in parallel + hexes = hexes.map(async (hex: any) => { + if (!hex || hex === "") { + res.status(400) + return res.json({ error: "Encountered empty hex" }) + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "decoderawtransaction" + requestConfig.data.method = "decoderawtransaction" + requestConfig.data.params = [hex] + + return await BitboxHTTP(requestConfig) + }) + + const result: Array = [] + return axios.all(hexes).then( + axios.spread((...args) => { + args.forEach((arg: any) => { + if (arg) { + result.push(arg.data.result) + } + }) + res.status(200) + return res.json(result) + }) + ) +*/ + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/getRawTransaction: `, err) + wlogger.error( + `Error in rawtransactions.ts/decodeRawTransactionBulk().`, + err + ) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Decode a raw transaction from hex to assembly. +// GET single +async function decodeScriptSingle(req, res, next) { + try { + const hex = req.params.hex + + // Throw an error if hex is empty. + if (!hex || hex === "") { + res.status(400) + return res.json({ error: "hex can not be empty" }) + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "decodescript" + requestConfig.data.method = "decodescript" + requestConfig.data.params = [hex] + + const response = await BitboxHTTP(requestConfig) + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeScript: `, err) + wlogger.error(`Error in rawtransactions.ts/decodeScriptSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Decode a raw transaction from hex to assembly. +// POST bulk +async function decodeScriptBulk(req, res, next) { + try { + const hexes = req.body.hexes + + // Validation + if (!Array.isArray(hexes)) { + res.status(400) + return res.json({ error: "hexes must be an array" }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, hexes)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + // Validate each hex in the array + for (let i = 0; i < hexes.length; i++) { + const hex = hexes[i] + + // Throw an error if hex is empty. + if (!hex || hex === "") { + res.status(400) + return res.json({ error: "Encountered empty hex" }) + } + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + // Loop through each hex and create an array of promises + const promises = hexes.map(async hex => { + requestConfig.data.id = "decodescript" + requestConfig.data.method = "decodescript" + requestConfig.data.params = [hex] + + const response = await BitboxHTTP(requestConfig) + return response + }) + + // Wait for all parallel promises to return. + const resolved = await Promise.all(promises) + + // Retrieve the data from each resolved promise. + const result = resolved.map(x => x.data.result) + + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeScript: `, err) + wlogger.error(`Error in rawtransactions.ts/decodeScriptBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Retrieve raw transactions details from the full node. +async function getRawTransactionsFromNode(txid, verbose) { + try { + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "getrawtransaction" + requestConfig.data.method = "getrawtransaction" + requestConfig.data.params = [txid, verbose] + + const response = await BitboxHTTP(requestConfig) + + return response.data.result + } catch (err) { + wlogger.error(`Error in rawtransactions.ts/getRawTransactionsFromNode().`) + throw err + } +} + +// Get a JSON object breakdown of transaction details. +// POST +async function getRawTransactionBulk(req, res, next) { + try { + let verbose = 0 + if (req.body.verbose) verbose = 1 + + const txids = req.body.txids + if (!Array.isArray(txids)) { + res.status(400) + return res.json({ error: "txids must be an array" }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, txids)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + // stub response object + const returnResponse = { + status: 100, + json: { + error: "" + } + } + + // Validate each txid in the array. + for (let i = 0; i < txids.length; i++) { + const txid = txids[i] + + if (!txid || txid === "") { + res.status(400) + return res.json({ error: `Encountered empty TXID` }) + } + + if (txid.length !== 64) { + res.status(400) + return res.json({ + error: `parameter 1 must be of length 64 (not ${txid.length})` + }) + } + } + + // Loop through each txid and create an array of promises + const promises = txids.map(async txid => + getRawTransactionsFromNode(txid, verbose) + ) + + // Wait for all parallel promises to return. + const axiosResult = await axios.all(promises) + + res.status(200) + return res.json(axiosResult) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/getRawTransaction: `, err) + wlogger.error(`Error in rawtransactions.ts/getRawTransactionBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Get a JSON object breakdown of transaction details. +// GET +async function getRawTransactionSingle(req, res, next) { + try { + let verbose = 0 + if (req.query.verbose === "true") verbose = 1 + + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + const data = await getRawTransactionsFromNode(txid, verbose) + + return res.json(data) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/getRawTransaction: `, err) + wlogger.error(`Error in rawtransactions.ts/getRawTransactionSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Transmit a raw transaction to the BCH network. +async function sendRawTransactionBulk(req, res, next) { + try { + // Validation + const hexes = req.body.hexes + + // Reject if input is not an array + if (!Array.isArray(hexes)) { + res.status(400) + return res.json({ error: "hex must be an array" }) + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, hexes)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + // Validate each element + for (let i = 0; i < hexes.length; i++) { + const hex = hexes[i] + + if (hex === "") { + res.status(400) + return res.json({ + error: `Encountered empty hex` + }) + } + } + + // Dev Note CT 1/31/2019: + // Sending the 'sendrawtrnasaction' RPC call to a full node in parallel will + // not work. Testing showed that the full node will return the same TXID for + // different TX hexes. I believe this is by design, to prevent double spends. + // In parallel, we are essentially asking the node to broadcast a new TX before + // it's finished broadcast the previous one. Serial execution is required. + + // How to send TX hexes in parallel the WRONG WAY: + /* + // Collect an array of promises. + const promises = hexes.map(async (hex: any) => { + requestConfig.data.id = "sendrawtransaction" + requestConfig.data.method = "sendrawtransaction" + requestConfig.data.params = [hex] + + return await BitboxHTTP(requestConfig) + }) + + // Wait for all parallel Insight requests to return. + const axiosResult: Array = await axios.all(promises) + + // Retrieve the data part of the result. + const result = axiosResult.map(x => x.data.result) + */ + + // Sending them serially. + const result = [] + for (let i = 0; i < hexes.length; i++) { + const hex = hexes[i] + + requestConfig.data.id = "sendrawtransaction" + requestConfig.data.method = "sendrawtransaction" + requestConfig.data.params = [hex] + + const rpcResult = await BitboxHTTP(requestConfig) + + result.push(rpcResult.data.result) + } + + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + wlogger.error(`Error in rawtransactions.ts/sendRawTransactionBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Transmit a raw transaction to the BCH network. +async function sendRawTransactionSingle(req, res, next) { + try { + const hex = req.params.hex // URL parameter + + // Reject if input is not an array or a string + if (typeof hex !== "string") { + res.status(400) + return res.json({ error: "hex must be a string" }) + } + + // Validation + if (hex === "") { + res.status(400) + return res.json({ + error: `Encountered empty hex` + }) + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + // RPC call + requestConfig.data.id = "sendrawtransaction" + requestConfig.data.method = "sendrawtransaction" + requestConfig.data.params = [hex] + + const rpcResult = await BitboxHTTP(requestConfig) + + const result = rpcResult.data.result + + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + wlogger.error( + `Error in rawtransactions.ts/sendRawTransactionSingle().`, + err + ) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +module.exports = { + router, + getRawTransactionsFromNode, + testableComponents: { + root, + decodeRawTransactionSingle, + decodeRawTransactionBulk, + decodeScriptSingle, + decodeScriptBulk, + getRawTransactionBulk, + getRawTransactionSingle, + sendRawTransactionBulk, + sendRawTransactionSingle + } +} diff --git a/src/routes/v3/route-utils.js b/src/routes/v3/route-utils.js new file mode 100644 index 0000000..93e6e9b --- /dev/null +++ b/src/routes/v3/route-utils.js @@ -0,0 +1,139 @@ +/* + A private library of utility functions used by several different routes. +*/ + +"use strict" + +const axios = require("axios") +const wlogger = require("../../util/winston-logging") + +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +const BITBOXJS = require("@chris.troutner/bitbox-js") +const BITBOX = new BITBOXJS() + +module.exports = { + validateNetwork, // Prevents a common user error + setEnvVars, // Allows RPC variables to be set dynamically based on changing env vars. + decodeError, // Extract and interpret error messages. + validateArraySize // Ensure the passed array meets rate limiting requirements. +} + +// This function expects the Request Express.js object and an array as input. +// The array is then validated against freemium and pro-tier rate limiting +// requirements. A boolean is returned to indicate if the array size if valid +// or not. +function validateArraySize(req, array) { + const FREEMIUM_INPUT_SIZE = 20 + const PRO_INPUT_SIZE = 100 + + if (req.locals && req.locals.proLimit) { + if (array.length <= PRO_INPUT_SIZE) return true + } else if (array.length <= FREEMIUM_INPUT_SIZE) { + return true + } + + return false +} + +// Returns true if user-provided cash address matches the correct network, +// mainnet or testnet. If NETWORK env var is not defined, it returns false. +// This prevent a common user-error issue that is easy to make: passing a +// testnet address into rest.bitcoin.com or passing a mainnet address into +// trest.bitcoin.com. +function validateNetwork(addr) { + try { + const network = process.env.NETWORK + + // Return false if NETWORK is not defined. + if (!network || network === "") { + console.log(`Warning: NETWORK environment variable is not defined!`) + return false + } + + // Convert the user-provided address to a cashaddress, for easy detection + // of the intended network. + const cashAddr = BITBOX.Address.toCashAddress(addr) + + // Return true if the network and address both match testnet + const addrIsTest = BITBOX.Address.isTestnetAddress(cashAddr) + if (network === "testnet" && addrIsTest) return true + + // Return true if the network and address both match mainnet + const addrIsMain = BITBOX.Address.isMainnetAddress(cashAddr) + if (network === "mainnet" && addrIsMain) return true + + return false + } catch (err) { + logger.error(`Error in validateNetwork()`) + return false + } +} + +// Dynamically set these based on env vars. Allows unit testing. +function setEnvVars() { + const BitboxHTTP = axios.create({ + baseURL: process.env.RPC_BASEURL + }) + const username = process.env.RPC_USERNAME + const password = process.env.RPC_PASSWORD + + const requestConfig = { + method: "post", + auth: { + username: username, + password: password + }, + data: { + jsonrpc: "1.0" + } + } + + return { BitboxHTTP, username, password, requestConfig } +} + +// Error messages returned by a full node can be burried pretty deep inside the +// error object returned by Axios. This function attempts to extract and interpret +// error messages. +// Returns an object. If successful, obj.msg is a string. +// If there is a failure, obj.msg is false. +function decodeError(err) { + try { + // Attempt to extract the full node error message. + if ( + err.response && + err.response.data && + err.response.data.error && + err.response.data.error.message + ) + return { msg: err.response.data.error.message, status: 400 } + + // Attempt to extract the Insight error message + if (err.response && err.response.data) + return { msg: err.response.data, status: err.response.status } + + // Attempt to detect a network connection error. + if (err.message && err.message.indexOf("ENOTFOUND") > -1) { + return { + msg: + "Network error: Could not communicate with full node or other external service.", + status: 503 + } + } + + // Different kind of network error + if (err.message && err.message.indexOf("ENETUNREACH") > -1) { + return { + msg: + "Network error: Could not communicate with full node or other external service.", + status: 503 + } + } + + return { msg: false, status: 500 } + } catch (err) { + wlogger.error(`unhandled error in route-utils.js/decodeError(): `, err) + return { msg: false, status: 500 } + } +} diff --git a/src/routes/v3/services/bitbox.js b/src/routes/v3/services/bitbox.js new file mode 100644 index 0000000..8fb0414 --- /dev/null +++ b/src/routes/v3/services/bitbox.js @@ -0,0 +1,11 @@ +"use strict" + +const axios = require("axios") + +const BitboxHTTP = axios.create({ + baseURL: process.env.RPC_BASEURL +}) + +const getInstance = () => BitboxHTTP + +module.exports = getInstance diff --git a/src/routes/v3/services/requestUtils.js b/src/routes/v3/services/requestUtils.js new file mode 100644 index 0000000..7f64965 --- /dev/null +++ b/src/routes/v3/services/requestUtils.js @@ -0,0 +1,20 @@ +"use strict" + +const username = process.env.RPC_USERNAME +const password = process.env.RPC_PASSWORD + +const getRequestConfig = (method, params) => ({ + method: "post", + auth: { + username, + password + }, + data: { + jsonrpc: "1.0", + id: method, + method, + params + } +}) + +module.exports = getRequestConfig diff --git a/src/routes/v3/slp.js b/src/routes/v3/slp.js new file mode 100644 index 0000000..af9cdb0 --- /dev/null +++ b/src/routes/v3/slp.js @@ -0,0 +1,1369 @@ +"use strict" + +const express = require("express") +const router = express.Router() +const axios = require("axios") + +const routeUtils = require("./route-utils") +const logger = require("./logging.js") +const strftime = require("strftime") +const wlogger = require("../../util/winston-logging") + +// Used to convert error messages to strings, to safely pass to users. +const util = require("util") +util.inspect.defaultOptions = { depth: 5 } + +const SLPSDK = require("slp-sdk") +const SLP = new SLPSDK() + +// Instantiate SLPJS. +const slp = require("slpjs") +const slpjs = new slp.Slp(SLP) +const utils = slp.Utils + +// SLP tx db (LevelDB for caching) +const level = require("level") +const slpTxDb = level("./slp-tx-db") + +// Setup JSON RPC +const BitboxHTTP = axios.create({ + baseURL: process.env.RPC_BASEURL +}) +const username = process.env.RPC_USERNAME +const password = process.env.RPC_PASSWORD + +// Setup REST and TREST URLs used by slpjs +// Dev note: this allows for unit tests to mock the URL. +if (!process.env.REST_URL) process.env.REST_URL = `https://rest.bitcoin.com/v2/` +if (!process.env.TREST_URL) + process.env.TREST_URL = `https://trest.bitcoin.com/v2/` + +router.get("/", root) +router.get("/list", list) +router.get("/list/:tokenId", listSingleToken) +router.post("/list", listBulkToken) +router.get("/balancesForAddress/:address", balancesForAddress) +router.get("/balancesForToken/:tokenId", balancesForTokenSingle) +router.get("/balance/:address/:tokenId", balancesForAddressByTokenID) +router.get("/convert/:address", convertAddressSingle) +router.post("/convert", convertAddressBulk) +router.post("/validateTxid", validateBulk) +router.get("/validateTxid/:txid", validateSingle) +router.get("/txDetails/:txid", txDetails) +router.get("/tokenStats/:tokenId", tokenStats) +router.get("/transactions/:tokenId/:address", txsTokenIdAddressSingle) + +if (process.env.NON_JS_FRAMEWORK && process.env.NON_JS_FRAMEWORK === "true") { + router.get( + "/createTokenType1/:fundingAddress/:fundingWif/:tokenReceiverAddress/:batonReceiverAddress/:bchChangeReceiverAddress/:decimals/:name/:symbol/:documentUri/:documentHash/:initialTokenQty", + createTokenType1 + ) + router.get( + "/mintTokenType1/:fundingAddress/:fundingWif/:tokenReceiverAddress/:batonReceiverAddress/:bchChangeReceiverAddress/:tokenId/:additionalTokenQty", + mintTokenType1 + ) + router.get( + "/sendTokenType1/:fundingAddress/:fundingWif/:tokenReceiverAddress/:bchChangeReceiverAddress/:tokenId/:amount", + sendTokenType1 + ) + router.get( + "/burnTokenType1/:fundingAddress/:fundingWif/:bchChangeReceiverAddress/:tokenId/:amount", + burnTokenType1 + ) + router.get( + "/burnAllTokenType1/:fundingAddress/:fundingWif/:bchChangeReceiverAddress/:tokenId", + burnAllTokenType1 + ) +} + +// Retrieve raw transactions details from the full node. +// TODO: move this function to a separate support library. +// TODO: Add unit tests for this function. +async function getRawTransactionsFromNode(txids) { + try { + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + const txPromises = txids.map(async txid => { + // Check slpTxDb + try { + if (slpTxDb.isOpen()) { + const rawTx = await slpTxDb.get(txid) + return rawTx + } + } catch (err) {} + + requestConfig.data.id = "getrawtransaction" + requestConfig.data.method = "getrawtransaction" + requestConfig.data.params = [txid, 0] + + const response = await BitboxHTTP(requestConfig) + const result = response.data.result + + // Insert to slpTxDb + try { + if (slpTxDb.isOpen()) await slpTxDb.put(txid, result) + } catch (err) { + // console.log("Error inserting to slpTxDb", err) + } + + return result + }) + + const results = await axios.all(txPromises) + return results + } catch (err) { + wlogger.error(`Error in slp.ts/getRawTransactionsFromNode().`, err) + throw err + } +} + +// Create a validator for validating SLP transactions. +function createValidator(network, getRawTransactions = null) { + let tmpSLP + + if (network === "mainnet") + tmpSLP = new SLPSDK({ restURL: process.env.REST_URL }) + else tmpSLP = new SLPSDK({ restURL: process.env.TREST_URL }) + + const slpValidator = new slp.LocalValidator( + tmpSLP, + getRawTransactions + ? getRawTransactions + : tmpSLP.RawTransactions.getRawTransaction.bind(this) + ) + + return slpValidator +} + +// Instantiate the local SLP validator. +const slpValidator = createValidator( + process.env.NETWORK, + getRawTransactionsFromNode +) + +// Instantiate the bitboxproxy class in SLPJS. +const bitboxproxy = new slp.BitboxNetwork(SLP, slpValidator) + +const requestConfig = { + method: "post", + auth: { + username: username, + password: password + }, + data: { + jsonrpc: "1.0" + } +} + +function formatTokenOutput(token) { + token.tokenDetails.id = token.tokenDetails.tokenIdHex + delete token.tokenDetails.tokenIdHex + token.tokenDetails.documentHash = token.tokenDetails.documentSha256Hex + delete token.tokenDetails.documentSha256Hex + token.tokenDetails.initialTokenQty = parseFloat( + token.tokenDetails.genesisOrMintQuantity + ) + delete token.tokenDetails.genesisOrMintQuantity + delete token.tokenDetails.transactionType + delete token.tokenDetails.batonVout + delete token.tokenDetails.sendOutputs + + token.tokenDetails.blockCreated = token.tokenStats.block_created + token.tokenDetails.blockLastActiveSend = + token.tokenStats.block_last_active_send + token.tokenDetails.blockLastActiveMint = + token.tokenStats.block_last_active_mint + token.tokenDetails.txnsSinceGenesis = + token.tokenStats.qty_valid_txns_since_genesis + token.tokenDetails.validAddresses = token.tokenStats.qty_valid_token_addresses + token.tokenDetails.totalMinted = parseFloat(token.tokenStats.qty_token_minted) + token.tokenDetails.totalBurned = parseFloat(token.tokenStats.qty_token_burned) + token.tokenDetails.circulatingSupply = parseFloat( + token.tokenStats.qty_token_circulating_supply + ) + token.tokenDetails.mintingBatonStatus = token.tokenStats.minting_baton_status + + delete token.tokenStats.block_last_active_send + delete token.tokenStats.block_last_active_mint + delete token.tokenStats.qty_valid_txns_since_genesis + delete token.tokenStats.qty_valid_token_addresses + return token +} + +function root(req, res, next) { + return res.json({ status: "slp" }) +} + +async function list(req, res, next) { + try { + const query = { + v: 3, + q: { + db: ["t"], + find: { + $query: {} + }, + project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, + sort: { "tokenStats.block_created": -1 }, + limit: 10000 + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString("base64") + const url = `${process.env.SLPDB_URL}q/${b64}` + + // Get data from SLPDB. + const tokenRes = await axios.get(url) + + const formattedTokens = [] + + if (tokenRes.data.t.length) { + tokenRes.data.t.forEach(token => { + token = formatTokenOutput(token) + formattedTokens.push(token.tokenDetails) + }) + } + + res.status(200) + return res.json(formattedTokens) + } catch (err) { + wlogger.error(`Error in slp.ts/list().`, err) + + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + res.status(500) + return res.json({ error: `Error in /list: ${err.message}` }) + } +} + +async function listSingleToken(req, res, next) { + try { + const tokenId = req.params.tokenId + + if (!tokenId || tokenId === "") { + res.status(400) + return res.json({ error: "tokenId can not be empty" }) + } + + const t = await lookupToken(tokenId) + + res.status(200) + return res.json(t) + } catch (err) { + wlogger.error(`Error in slp.ts/listSingleToken().`, err) + + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + res.status(500) + return res.json({ error: `Error in /list/:tokenId: ${err.message}` }) + } +} + +async function listBulkToken(req, res, next) { + try { + const tokenIds = req.body.tokenIds + + // Reject if tokenIds is not an array. + if (!Array.isArray(tokenIds)) { + res.status(400) + return res.json({ + error: "tokenIds needs to be an array. Use GET for single tokenId." + }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, tokenIds)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + const query = { + v: 3, + q: { + db: ["t"], + find: { + "tokenDetails.tokenIdHex": { + $in: tokenIds + } + }, + project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, + sort: { "tokenStats.block_created": -1 }, + limit: 10000 + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString("base64") + const url = `${process.env.SLPDB_URL}q/${b64}` + + const tokenRes = await axios.get(url) + + const formattedTokens = [] + const txids = [] + + if (tokenRes.data.t.length) { + tokenRes.data.t.forEach(token => { + txids.push(token.tokenDetails.tokenIdHex) + token = formatTokenOutput(token) + formattedTokens.push(token.tokenDetails) + }) + } + + tokenIds.forEach(tokenId => { + if (!txids.includes(tokenId)) { + formattedTokens.push({ + id: tokenId, + valid: false + }) + } + }) + + res.status(200) + return res.json(formattedTokens) + } catch (err) { + wlogger.error(`Error in slp.ts/listBulkToken().`, err) + + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + res.status(500) + return res.json({ error: `Error in /list/:tokenId: ${err.message}` }) + } +} + +async function lookupToken(tokenId) { + try { + const query = { + v: 3, + q: { + db: ["t"], + find: { + $query: { + "tokenDetails.tokenIdHex": tokenId + } + }, + project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, + limit: 1000 + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString("base64") + const url = `${process.env.SLPDB_URL}q/${b64}` + + const tokenRes = await axios.get(url) + //console.log(`tokenRes.data: ${util.inspect(tokenRes.data,null,2)}`) + //console.log(`tokenRes.data.t[0]: ${util.inspect(tokenRes.data.t[0],null,2)}`) + + const formattedTokens = [] + + if (tokenRes.data.t.length) { + tokenRes.data.t.forEach(token => { + token = formatTokenOutput(token) + formattedTokens.push(token.tokenDetails) + }) + } + + let t + formattedTokens.forEach(token => { + if (token.id === tokenId) t = token + }) + + // If token could not be found. + if (t === undefined) { + t = { + id: "not found" + } + } + + return t + } catch (err) { + wlogger.error(`Error in slp.ts/lookupToken().`, err) + //console.log(`Error in slp.ts/lookupToken()`) + throw err + } +} + +// Retrieve token balances for all tokens for a single address. +async function balancesForAddress(req, res, next) { + try { + // Validate the input data. + const address = req.params.address + if (!address || address === "") { + res.status(400) + return res.json({ error: "address can not be empty" }) + } + + // Ensure the input is a valid BCH address. + try { + const cash = utils.toCashAddress(address) + } catch (err) { + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${address}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const cashAddr = utils.toCashAddress(address) + const networkIsValid = routeUtils.validateNetwork(cashAddr) + if (!networkIsValid) { + res.status(400) + return res.json({ + error: `Invalid network. Trying to use a testnet address on mainnet, or vice versa.` + }) + } + + const query = { + v: 3, + q: { + db: ["a"], + find: { + address: SLP.Address.toSLPAddress(address), + token_balance: { $gte: 0 } + }, + limit: 10000 + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString("base64") + const url = `${process.env.SLPDB_URL}q/${b64}` + + const tokenRes = await axios.get(url) + + const tokenIds = [] + if (tokenRes.data.a.length > 0) { + tokenRes.data.a = tokenRes.data.a.map(token => { + token.tokenId = token.tokenDetails.tokenIdHex + tokenIds.push(token.tokenId) + token.balance = parseFloat(token.token_balance) + token.slpAddress = token.address + delete token.tokenDetails + delete token.satoshis_balance + delete token.token_balance + delete token._id + delete token.address + return token + }) + + const promises = tokenIds.map(async tokenId => { + try { + const query2 = { + v: 3, + q: { + db: ["t"], + find: { + $query: { + "tokenDetails.tokenIdHex": tokenId + } + }, + project: { + "tokenDetails.decimals": 1, + "tokenDetails.tokenIdHex": 1, + _id: 0 + }, + limit: 1000 + } + } + + const s2 = JSON.stringify(query2) + const b642 = Buffer.from(s2).toString("base64") + const url2 = `${process.env.SLPDB_URL}q/${b642}` + + const tokenRes2 = await axios.get(url2) + return tokenRes2.data + } catch (err) { + throw err + } + }) + + const details = await axios.all(promises) + tokenRes.data.a = tokenRes.data.a.map(token => { + details.forEach(detail => { + if (detail.t[0].tokenDetails.tokenIdHex === token.tokenId) + token.decimalCount = detail.t[0].tokenDetails.decimals + }) + return token + }) + + return res.json(tokenRes.data.a) + } + return res.json("No balance for this address") + } catch (err) { + wlogger.error(`Error in slp.ts/balancesForAddress().`, err) + + // Decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + res.status(500) + return res.json({ + error: `Error in /ddress/:address: ${err.message}` + }) + } +} + +// Retrieve token balances for all addresses by single tokenId. +async function balancesForTokenSingle(req, res, next) { + try { + // Validate the input data. + const tokenId = req.params.tokenId + if (!tokenId || tokenId === "") { + res.status(400) + return res.json({ error: "tokenId can not be empty" }) + } + + const query = { + v: 3, + q: { + find: { + "tokenDetails.tokenIdHex": tokenId, + token_balance: { $gte: 0 } + }, + limit: 10000, + project: { address: 1, satoshis_balance: 1, token_balance: 1, _id: 0 } + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString("base64") + const url = `${process.env.SLPDB_URL}q/${b64}` + + // Get data from SLPDB. + const tokenRes = await axios.get(url) + const resBalances = tokenRes.data.a.map((addy, index) => { + delete addy.satoshis_balance + addy.tokenBalance = parseFloat(addy.token_balance) + addy.slpAddress = addy.address + addy.tokenId = tokenId + delete addy.address + delete addy.token_balance + return addy + }) + return res.json(resBalances) + } catch (err) { + wlogger.error(`Error in slp.ts/balancesForTokenSingle().`, err) + + // Decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + res.status(500) + return res.json({ + error: `Error in /balancesForToken/:tokenId: ${err.message}` + }) + } +} + +// Retrieve token balances for a single token class, for a single address. +async function balancesForAddressByTokenID(req, res, next) { + try { + // Validate input data. + const address = req.params.address + if (!address || address === "") { + res.status(400) + return res.json({ error: "address can not be empty" }) + } + + const tokenId = req.params.tokenId + if (!tokenId || tokenId === "") { + res.status(400) + return res.json({ error: "tokenId can not be empty" }) + } + + // Ensure the input is a valid BCH address. + try { + const cash = utils.toCashAddress(address) + } catch (err) { + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${address}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const cashAddr = utils.toCashAddress(address) + const networkIsValid = routeUtils.validateNetwork(cashAddr) + if (!networkIsValid) { + res.status(400) + return res.json({ + error: `Invalid network. Trying to use a testnet address on mainnet, or vice versa.` + }) + } + + // Convert input to an simpleledger: address. + const slpAddr = utils.toSlpAddress(req.params.address) + + const query = { + v: 3, + q: { + db: ["a"], + find: { + address: slpAddr, + token_balance: { $gte: 0 } + }, + limit: 10 + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString("base64") + const url = `${process.env.SLPDB_URL}q/${b64}` + + // Get data from SLPDB. + const tokenRes = await axios.get(url) + let resVal + res.status(200) + if (tokenRes.data.a.length > 0) { + tokenRes.data.a.forEach(async token => { + if (token.tokenDetails.tokenIdHex === tokenId) { + resVal = { + tokenId: tokenRes.data.a[0].tokenDetails.tokenIdHex, + balance: parseFloat(tokenRes.data.a[0].token_balance) + } + // const query2 = { + // v: 3, + // q: { + // db: ["t"], + // find: { + // $query: { + // "tokenDetails.tokenIdHex": tokenId + // } + // }, + // project: { + // "tokenDetails.decimals": 1, + // "tokenDetails.tokenIdHex": 1, + // _id: 0 + // }, + // limit: 1000 + // } + // } + // + // const s2 = JSON.stringify(query2) + // const b642 = Buffer.from(s2).toString("base64") + // const url2 = `${process.env.SLPDB_URL}q/${b642}` + // + // const tokenRes2 = await axios.get(url2) + // console.log("hello world", tokenRes2.data.t) + // resVal = { + // tokenId: token.tokenDetails.tokenIdHex, + // balance: parseFloat(token.token_balance), + // decimalCount: tokenRes2.data.t[0].tokenDetails.decimals + // } + // console.log("resVal", resVal) + // return res.json(resVal) + } else { + resVal = { + tokenId: tokenId, + balance: 0 + } + } + }) + } else { + resVal = { + tokenId: tokenId, + balance: 0 + } + } + return res.json(resVal) + } catch (err) { + wlogger.error(`Error in slp.ts/balancesForAddressByTokenID().`, err) + + // Decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + res.status(500) + return res.json({ + error: `Error in /balance/:address/:tokenId: ${err.message}` + }) + } +} + +async function convertAddressSingle(req, res, next) { + try { + const address = req.params.address + + // Validate input + if (!address || address === "") { + res.status(400) + return res.json({ error: "address can not be empty" }) + } + + const slpAddr = SLP.Address.toSLPAddress(address) + + const obj = { + slpAddress: "", + cashAddress: "", + legacyAddress: "" + } + obj.slpAddress = slpAddr + obj.cashAddress = SLP.Address.toCashAddress(slpAddr) + obj.legacyAddress = SLP.Address.toLegacyAddress(obj.cashAddress) + + res.status(200) + return res.json(obj) + } catch (err) { + wlogger.error(`Error in slp.ts/convertAddressSingle().`, err) + + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + res.status(500) + return res.json({ + error: `Error in /address/convert/:address: ${err.message}` + }) + } +} + +async function convertAddressBulk(req, res, next) { + const addresses = req.body.addresses + + // Reject if hashes is not an array. + if (!Array.isArray(addresses)) { + res.status(400) + return res.json({ + error: "addresses needs to be an array. Use GET for single address." + }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, addresses)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + // Convert each address in the array. + const convertedAddresses = [] + for (let i = 0; i < addresses.length; i++) { + const address = addresses[i] + + // Validate input + if (!address || address === "") { + res.status(400) + return res.json({ error: "address can not be empty" }) + } + + const slpAddr = SLP.Address.toSLPAddress(address) + + const obj = { + slpAddress: "", + cashAddress: "", + legacyAddress: "" + } + obj.slpAddress = slpAddr + obj.cashAddress = SLP.Address.toCashAddress(slpAddr) + obj.legacyAddress = SLP.Address.toLegacyAddress(obj.cashAddress) + + convertedAddresses.push(obj) + } + + res.status(200) + return res.json(convertedAddresses) +} + +async function validateBulk(req, res, next) { + try { + const txids = req.body.txids + + // Reject if txids is not an array. + if (!Array.isArray(txids)) { + res.status(400) + return res.json({ error: "txids needs to be an array" }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, txids)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + logger.debug(`Executing slp/validate with these txids: `, txids) + + // Validate each txid + const validatePromises = txids.map(async txid => { + try { + // Dev note: must call module.exports to allow stubs in unit tests. + const isValid = await module.exports.testableComponents.isValidSlpTxid( + txid + ) + + const tmp = { + txid: txid, + valid: isValid ? true : false + } + return tmp + } catch (err) { + //console.log(`err obj: ${util.inspect(err)}`) + //console.log(`err.response.data: ${util.inspect(err.response.data)}`) + throw err + } + }) + + // Filter array to only valid txid results + const validateResults = await axios.all(validatePromises) + const validTxids = validateResults.filter(result => result) + + res.status(200) + return res.json(validTxids) + } catch (err) { + wlogger.error(`Error in slp.ts/validateBulk().`, err) + + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function validateSingle(req, res, next) { + try { + const txid = req.params.txid + + // Validate input + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + logger.debug(`Executing slp/validate/:txid with this txid: `, txid) + + // Validate txid + // Dev note: must call module.exports to allow stubs in unit tests. + const isValid = await module.exports.testableComponents.isValidSlpTxid(txid) + + const tmp = { + txid: txid, + valid: isValid ? true : false + } + + res.status(200) + return res.json(tmp) + } catch (err) { + wlogger.error(`Error in slp.ts/validateSingle().`, err) + + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Returns a Boolean if the input TXID is a valid SLP TXID. +async function isValidSlpTxid(txid) { + const isValid = await slpValidator.isValidSlpTxid(txid) + return isValid +} + +// Below are functions which are enabled for teams not using our javascript SDKs which still need to create txs +// These should never be enabled on our public REST API + +async function createTokenType1(req, res, next) { + const fundingAddress = req.params.fundingAddress + if (!fundingAddress || fundingAddress === "") { + res.status(400) + return res.json({ error: "fundingAddress can not be empty" }) + } + + const fundingWif = req.params.fundingWif + if (!fundingWif || fundingWif === "") { + res.status(400) + return res.json({ error: "fundingWif can not be empty" }) + } + + const tokenReceiverAddress = req.params.tokenReceiverAddress + if (!tokenReceiverAddress || tokenReceiverAddress === "") { + res.status(400) + return res.json({ error: "tokenReceiverAddress can not be empty" }) + } + + const batonReceiverAddress = req.params.batonReceiverAddress + if (!batonReceiverAddress || batonReceiverAddress === "") { + res.status(400) + return res.json({ error: "batonReceiverAddress can not be empty" }) + } + + const bchChangeReceiverAddress = req.params.bchChangeReceiverAddress + if (!bchChangeReceiverAddress || bchChangeReceiverAddress === "") { + res.status(400) + return res.json({ error: "bchChangeReceiverAddress can not be empty" }) + } + + const decimals = req.params.decimals + if (!decimals || decimals === "") { + res.status(400) + return res.json({ error: "decimals can not be empty" }) + } + + const name = req.params.name + if (!name || name === "") { + res.status(400) + return res.json({ error: "name can not be empty" }) + } + + const symbol = req.params.symbol + if (!symbol || symbol === "") { + res.status(400) + return res.json({ error: "symbol can not be empty" }) + } + + const documentUri = req.params.documentUri + if (!documentUri || documentUri === "") { + res.status(400) + return res.json({ error: "documentUri can not be empty" }) + } + + const documentHash = req.params.documentHash + if (!documentHash || documentHash === "") { + res.status(400) + return res.json({ error: "documentHash can not be empty" }) + } + + const initialTokenQty = req.params.initialTokenQty + if (!initialTokenQty || initialTokenQty === "") { + res.status(400) + return res.json({ error: "initialTokenQty can not be empty" }) + } + + const token = await SLP.TokenType1.create({ + fundingAddress: fundingAddress, + fundingWif: fundingWif, + tokenReceiverAddress: tokenReceiverAddress, + batonReceiverAddress: batonReceiverAddress, + bchChangeReceiverAddress: bchChangeReceiverAddress, + decimals: decimals, + name: name, + symbol: symbol, + documentUri: documentUri, + documentHash: documentHash, + initialTokenQty: initialTokenQty + }) + + res.status(200) + return res.json(token) +} + +async function mintTokenType1(req, res, next) { + const fundingAddress = req.params.fundingAddress + if (!fundingAddress || fundingAddress === "") { + res.status(400) + return res.json({ error: "fundingAddress can not be empty" }) + } + + const fundingWif = req.params.fundingWif + if (!fundingWif || fundingWif === "") { + res.status(400) + return res.json({ error: "fundingWif can not be empty" }) + } + + const tokenReceiverAddress = req.params.tokenReceiverAddress + if (!tokenReceiverAddress || tokenReceiverAddress === "") { + res.status(400) + return res.json({ error: "tokenReceiverAddress can not be empty" }) + } + + const batonReceiverAddress = req.params.batonReceiverAddress + if (!batonReceiverAddress || batonReceiverAddress === "") { + res.status(400) + return res.json({ error: "batonReceiverAddress can not be empty" }) + } + + const bchChangeReceiverAddress = req.params.bchChangeReceiverAddress + if (!bchChangeReceiverAddress || bchChangeReceiverAddress === "") { + res.status(400) + return res.json({ error: "bchChangeReceiverAddress can not be empty" }) + } + + const tokenId = req.params.tokenId + if (!tokenId || tokenId === "") { + res.status(400) + return res.json({ error: "tokenId can not be empty" }) + } + + const additionalTokenQty = req.params.additionalTokenQty + if (!additionalTokenQty || additionalTokenQty === "") { + res.status(400) + return res.json({ error: "additionalTokenQty can not be empty" }) + } + + const mint = await SLP.TokenType1.mint({ + fundingAddress: fundingAddress, + fundingWif: fundingWif, + tokenReceiverAddress: tokenReceiverAddress, + batonReceiverAddress: batonReceiverAddress, + bchChangeReceiverAddress: bchChangeReceiverAddress, + tokenId: tokenId, + additionalTokenQty: additionalTokenQty + }) + + res.status(200) + return res.json(mint) +} + +async function sendTokenType1(req, res, next) { + const fundingAddress = req.params.fundingAddress + if (!fundingAddress || fundingAddress === "") { + res.status(400) + return res.json({ error: "fundingAddress can not be empty" }) + } + + const fundingWif = req.params.fundingWif + if (!fundingWif || fundingWif === "") { + res.status(400) + return res.json({ error: "fundingWif can not be empty" }) + } + + const tokenReceiverAddress = req.params.tokenReceiverAddress + if (!tokenReceiverAddress || tokenReceiverAddress === "") { + res.status(400) + return res.json({ error: "tokenReceiverAddress can not be empty" }) + } + + const bchChangeReceiverAddress = req.params.bchChangeReceiverAddress + if (!bchChangeReceiverAddress || bchChangeReceiverAddress === "") { + res.status(400) + return res.json({ error: "bchChangeReceiverAddress can not be empty" }) + } + + const tokenId = req.params.tokenId + if (!tokenId || tokenId === "") { + res.status(400) + return res.json({ error: "tokenId can not be empty" }) + } + + const amount = req.params.amount + if (!amount || amount === "") { + res.status(400) + return res.json({ error: "amount can not be empty" }) + } + const send = await SLP.TokenType1.send({ + fundingAddress: fundingAddress, + fundingWif: fundingWif, + tokenReceiverAddress: tokenReceiverAddress, + bchChangeReceiverAddress: bchChangeReceiverAddress, + tokenId: tokenId, + amount: amount + }) + + res.status(200) + return res.json(send) +} + +async function burnTokenType1(req, res, next) { + const fundingAddress = req.params.fundingAddress + if (!fundingAddress || fundingAddress === "") { + res.status(400) + return res.json({ error: "fundingAddress can not be empty" }) + } + + const fundingWif = req.params.fundingWif + if (!fundingWif || fundingWif === "") { + res.status(400) + return res.json({ error: "fundingWif can not be empty" }) + } + + const bchChangeReceiverAddress = req.params.bchChangeReceiverAddress + if (!bchChangeReceiverAddress || bchChangeReceiverAddress === "") { + res.status(400) + return res.json({ error: "bchChangeReceiverAddress can not be empty" }) + } + + const tokenId = req.params.tokenId + if (!tokenId || tokenId === "") { + res.status(400) + return res.json({ error: "tokenId can not be empty" }) + } + + const amount = req.params.amount + if (!amount || amount === "") { + res.status(400) + return res.json({ error: "amount can not be empty" }) + } + + const burn = await SLP.TokenType1.burn({ + fundingAddress: fundingAddress, + fundingWif: fundingWif, + tokenId: tokenId, + amount: amount, + bchChangeReceiverAddress: bchChangeReceiverAddress + }) + + res.status(200) + return res.json(burn) +} + +async function burnAllTokenType1(req, res, next) { + const fundingAddress = req.params.fundingAddress + if (!fundingAddress || fundingAddress === "") { + res.status(400) + return res.json({ error: "fundingAddress can not be empty" }) + } + + const fundingWif = req.params.fundingWif + if (!fundingWif || fundingWif === "") { + res.status(400) + return res.json({ error: "fundingWif can not be empty" }) + } + + const bchChangeReceiverAddress = req.params.bchChangeReceiverAddress + if (!bchChangeReceiverAddress || bchChangeReceiverAddress === "") { + res.status(400) + return res.json({ error: "bchChangeReceiverAddress can not be empty" }) + } + + const tokenId = req.params.tokenId + if (!tokenId || tokenId === "") { + res.status(400) + return res.json({ error: "tokenId can not be empty" }) + } + + const burnAll = await SLP.TokenType1.burnAll({ + fundingAddress: fundingAddress, + fundingWif: fundingWif, + tokenId: tokenId, + bchChangeReceiverAddress: bchChangeReceiverAddress + }) + + res.status(200) + return res.json(burnAll) +} + +async function txDetails(req, res, next) { + try { + // Validate input parameter + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + if (txid.length !== 64) { + res.status(400) + return res.json({ error: "This is not a txid" }) + } + + let tmpSLP + if (process.env.NETWORK === "testnet") + tmpSLP = new SLPSDK({ restURL: process.env.TREST_URL }) + else tmpSLP = new SLPSDK({ restURL: process.env.REST_URL }) + + const tmpbitboxNetwork = new slp.BitboxNetwork(tmpSLP, slpValidator) + //console.log( + // `tmpbitboxNetwork: ${JSON.stringify(tmpbitboxNetwork, null, 2)}` + //) + + // Get TX info + token info + const result = await tmpbitboxNetwork.getTransactionDetails(txid) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + res.status(200) + return res.json(result) + } catch (err) { + wlogger.error(`Error in slp.ts/txDetails().`, err) + + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Handle corner case of mis-typted txid + if (err.error.indexOf("Not found") > -1) { + res.status(400) + return res.json({ error: "TXID not found" }) + } + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function tokenStats(req, res, next) { + const tokenId = req.params.tokenId + if (!tokenId || tokenId === "") { + res.status(400) + return res.json({ error: "tokenId can not be empty" }) + } + + try { + const query = { + v: 3, + q: { + db: ["t"], + find: { + $query: { + "tokenDetails.tokenIdHex": tokenId + } + }, + project: { tokenDetails: 1, tokenStats: 1, _id: 0 }, + limit: 10 + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString("base64") + const url = `${process.env.SLPDB_URL}q/${b64}` + + // Get data from BitDB. + const tokenRes = await axios.get(url) + + const formattedTokens = [] + + if (tokenRes.data.t.length) { + tokenRes.data.t.forEach(token => { + token = formatTokenOutput(token) + formattedTokens.push(token.tokenDetails) + }) + } + + res.status(200) + return res.json(formattedTokens[0]) + } catch (err) { + wlogger.error(`Error in slp.ts/tokenStats().`, err) + + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + res.status(500) + return res.json({ error: `Error in /tokenStats: ${err.message}` }) + } +} + +// Retrieve transactions by tokenId and address. +async function txsTokenIdAddressSingle(req, res, next) { + try { + // Validate the input data. + const tokenId = req.params.tokenId + if (!tokenId || tokenId === "") { + res.status(400) + return res.json({ error: "tokenId can not be empty" }) + } + + const address = req.params.address + if (!address || address === "") { + res.status(400) + return res.json({ error: "address can not be empty" }) + } + + const query = { + v: 3, + q: { + find: { + db: ["c", "u"], + $query: { + $or: [ + { + "in.e.a": address + }, + { + "out.e.a": address + } + ], + "slp.detail.tokenIdHex": tokenId + }, + $orderby: { + "blk.i": -1 + } + }, + limit: 100 + }, + r: { + f: "[.[] | { txid: .tx.h, tokenDetails: .slp } ]" + } + } + + const s = JSON.stringify(query) + const b64 = Buffer.from(s).toString("base64") + const url = `${process.env.SLPDB_URL}q/${b64}` + + // Get data from SLPDB. + const tokenRes = await axios.get(url) + //console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data,null,2)}`) + + return res.json(tokenRes.data.c) + } catch (err) { + wlogger.error(`Error in slp.ts/txsTokenIdAddressSingle().`, err) + + // Decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + res.status(500) + return res.json({ + error: `Error in /transactions/:tokenId/:address: ${err.message}` + }) + } +} + +module.exports = { + router, + testableComponents: { + root, + list, + listSingleToken, + listBulkToken, + balancesForAddress, + balancesForAddressByTokenID, + convertAddressSingle, + convertAddressBulk, + validateBulk, + isValidSlpTxid, + createTokenType1, + mintTokenType1, + sendTokenType1, + burnTokenType1, + burnAllTokenType1, + txDetails, + tokenStats, + balancesForTokenSingle, + txsTokenIdAddressSingle + } +} diff --git a/src/routes/v3/transaction.js b/src/routes/v3/transaction.js new file mode 100644 index 0000000..b7205a0 --- /dev/null +++ b/src/routes/v3/transaction.js @@ -0,0 +1,188 @@ +"use strict" + +const express = require("express") +const router = express.Router() +const axios = require("axios") + +const routeUtils = require("./route-utils") +const logger = require("./logging.js") +const wlogger = require("../../util/winston-logging") + +const BITBOXJS = require("@chris.troutner/bitbox-js") +const BITBOX = new BITBOXJS() + +// Used to convert error messages to strings, to safely pass to users. +const util = require("util") +util.inspect.defaultOptions = { depth: 3 } + +// Manipulates and formats the raw data comming from Insight API. +const processInputs = tx => { + // Add legacy and cashaddr to tx vin + if (tx.vin) { + tx.vin.forEach(vin => { + if (!vin.coinbase) { + vin.value = vin.valueSat + const address = vin.addr + if (address) { + vin.legacyAddress = BITBOX.Address.toLegacyAddress(address) + vin.cashAddress = BITBOX.Address.toCashAddress(address) + delete vin.addr + } + delete vin.valueSat + delete vin.doubleSpentTxID + } + }) + } + + // Add legacy and cashaddr to tx vout + if (tx.vout) { + tx.vout.forEach(vout => { + // Overwrite value string with value in satoshis + //vout.value = parseFloat(vout.value) * 100000000 + + if (vout.scriptPubKey) { + if (vout.scriptPubKey.addresses) { + const cashAddrs = [] + vout.scriptPubKey.addresses.forEach(addr => { + const cashAddr = BITBOX.Address.toCashAddress(addr) + cashAddrs.push(cashAddr) + }) + vout.scriptPubKey.cashAddrs = cashAddrs + } + } + }) + } +} + +router.get("/", root) +router.post("/details", detailsBulk) +router.get("/details/:txid", detailsSingle) + +function root(req, res, next) { + return res.json({ status: "transaction" }) +} + +// Retrieve transaction data from the Insight API +// This function is also used by the SLP route library. +async function transactionsFromInsight(txid) { + try { + const path = `${process.env.BITCOINCOM_BASEURL}tx/${txid}` + + // Query the Insight server. + const response = await axios.get(path) + //console.log(`Insight output: ${JSON.stringify(response.data, null, 2)}`) + + // Parse the data. + const parsed = response.data + if (parsed) processInputs(parsed) + + return parsed + } catch (err) { + // Dev Note: Do not log error messages here. Throw them instead and let the + // parent function handle it. + throw err + } +} + +async function detailsBulk(req, res, next) { + try { + const txids = req.body.txids + + // Reject if address is not an array. + if (!Array.isArray(txids)) { + res.status(400) + return res.json({ error: "txids needs to be an array" }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, txids)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + logger.debug(`Executing transaction/details with these txids: `, txids) + + // Collect an array of promises + const promises = txids.map( + async txid => await transactionsFromInsight(txid) + ) + + // Wait for all parallel promises to return. + const result = await Promise.all(promises) + + // Return the array of retrieved transaction information. + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + wlogger.error(`Error in transactions.ts/detailsBulk().`, err) + + //console.log(`Error in transaction details: `, err) + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// GET handler. Retrieve any unconfirmed TX information for a given address. +async function detailsSingle(req, res, next) { + try { + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + // Reject if address is an array. + if (Array.isArray(txid)) { + res.status(400) + return res.json({ + error: "txid can not be an array. Use POST for bulk upload." + }) + } + + logger.debug( + `Executing transaction.ts/detailsSingle with this txid: `, + txid + ) + + // Query the Insight API. + const retData = await transactionsFromInsight(txid) + //console.log(`retData: ${JSON.stringify(retData,null,2)}`) + + // Return the array of retrieved address information. + res.status(200) + return res.json(retData) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in transactions.ts/detailsSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +module.exports = { + router, + transactionsFromInsight, + testableComponents: { + root, + detailsBulk, + detailsSingle + } +} diff --git a/src/routes/v3/util.js b/src/routes/v3/util.js new file mode 100644 index 0000000..e222d24 --- /dev/null +++ b/src/routes/v3/util.js @@ -0,0 +1,173 @@ +"use strict" + +const express = require("express") +const router = express.Router() +const axios = require("axios") + +const routeUtils = require("./route-utils") +const logger = require("./logging.js") +const wlogger = require("../../util/winston-logging") + +const BITBOXJS = require("@chris.troutner/bitbox-js") +const BITBOX = new BITBOXJS() + +// Used to convert error messages to strings, to safely pass to users. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +const BitboxHTTP = axios.create({ + baseURL: process.env.RPC_BASEURL +}) + +const username = process.env.RPC_USERNAME +const password = process.env.RPC_PASSWORD + +const requestConfig = { + method: "post", + auth: { + username: username, + password: password + }, + data: { + jsonrpc: "1.0" + } +} + +router.get("/", root) +router.get("/validateAddress/:address", validateAddressSingle) +router.post("/validateAddress", validateAddressBulk) + +function root(req, res, next) { + return res.json({ status: "util" }) +} + +async function validateAddressSingle(req, res, next) { + try { + const address = req.params.address + if (!address || address === "") { + res.status(400) + return res.json({ error: "address can not be empty" }) + } + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + requestConfig.data.id = "validateaddress" + requestConfig.data.method = "validateaddress" + requestConfig.data.params = [address] + + const response = await BitboxHTTP(requestConfig) + + return res.json(response.data.result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + wlogger.error(`Error in util.ts/validateAddressSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +async function validateAddressBulk(req, res, next) { + try { + const addresses = req.body.addresses + + // Reject if addresses is not an array. + if (!Array.isArray(addresses)) { + res.status(400) + return res.json({ + error: "addresses needs to be an array. Use GET for single address." + }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, addresses)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + // Validate each element in the array. + for (let i = 0; i < addresses.length; i++) { + const address = addresses[i] + + // Ensure the input is a valid BCH address. + try { + var legacyAddr = BITBOX.Address.toLegacyAddress(address) + } catch (err) { + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${address}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const networkIsValid = routeUtils.validateNetwork(address) + if (!networkIsValid) { + res.status(400) + return res.json({ + error: `Invalid network. Trying to use a testnet address on mainnet, or vice versa.` + }) + } + } + + logger.debug(`Executing util/validate with these addresses: `, addresses) + + const { + BitboxHTTP, + username, + password, + requestConfig + } = routeUtils.setEnvVars() + + // Loop through each address and creates an array of requests to call in parallel + const promises = addresses.map(async address => { + requestConfig.data.id = "validateaddress" + requestConfig.data.method = "validateaddress" + requestConfig.data.params = [address] + + return await BitboxHTTP(requestConfig) + }) + + // Wait for all parallel Insight requests to return. + const axiosResult = await axios.all(promises) + + // Retrieve the data part of the result. + const result = axiosResult.map(x => x.data.result) + + res.status(200) + return res.json(result) + } catch (err) { + // Attempt to decode the error message. + const { msg, status } = routeUtils.decodeError(err) + if (msg) { + res.status(status) + return res.json({ error: msg }) + } + + wlogger.error(`Error in util.ts/validateAddressSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +module.exports = { + router, + testableComponents: { + root, + validateAddressSingle, + validateAddressBulk + } +} diff --git a/test/v3/address.js b/test/v3/address.js new file mode 100644 index 0000000..41f6de5 --- /dev/null +++ b/test/v3/address.js @@ -0,0 +1,1373 @@ +/* + TESTS FOR THE ADDRESS.JS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. + + To-Do: + -/details/:address + --Verify to/from query options work correctly. + -GET /unconfirmed/:address & POST /unconfirmed + --Should initiate a transfer of BCH to verify unconfirmed TX. + ---This would be more of an e2e test. +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const addressRoute = require("../../src/routes/v3/address") +const nock = require("nock") // HTTP mocking + +let originalUrl // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/address-mock") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#AddressRouter", () => { + let req, res + + before(() => { + originalUrl = process.env.BITCOINCOM_BASEURL + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + // console.log(`Testing type is: ${process.env.TEST}`) + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + process.env.BITCOINCOM_BASEURL = originalUrl + }) + + describe("#root", () => { + // root route handler. + const root = addressRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + + assert.equal(result.status, "address", "Returns static string") + }) + }) + + describe("#AddressDetailsBulk", () => { + // details route handler. + const detailsBulk = addressRoute.testableComponents.detailsBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + address: `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + } + + const result = await detailsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.body = { + addresses: [`02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + const result = await detailsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should detect a network mismatch", async () => { + req.body = { + addresses: [`bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3`] + } + + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await detailsBulk(req, res) + //console.log(`network issue result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.isAbove(res.statusCode, 499, "HTTP status code 500 expected.") + //assert.include(result.error, "ENOTFOUND", "Error message expected") + assert.include( + result.error, + "Network error: Could not communicate", + "Error message expected" + ) + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should default to page 0", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert current page defaults to 0 + assert.equal(result[0].currentPage, 0) + }) + + it("should process the requested page", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`], + page: 5 + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=5000&to=6000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert current page is same as requested + assert.equal(result[0].currentPage, 5) + }) + + it("should calculate the total number of pages", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result[0].pagesTotal, 1) + }) + + it("should get details for a single address", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.equal(result.length, 1, "Array with one entry") + assert.hasAllKeys(result[0], [ + "balance", + "balanceSat", + "totalReceived", + "totalReceivedSat", + "totalSent", + "totalSentSat", + "unconfirmedBalance", + "unconfirmedBalanceSat", + "unconfirmedTxApperances", + "txApperances", + "transactions", + "legacyAddress", + "cashAddress", + "currentPage", + "pagesTotal" + ]) + }) + + it("should get details for multiple addresses", async () => { + req.body = { + addresses: [ + `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`, + `bchtest:qzknfggae0av6yvxk77gmyq7syc67yux6sk80haqyr` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mwJnEzXzKkveF2q5Af9jxi9j1zrtWAnPU8?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "2 outputs for 2 inputs") + }) + }) + + describe("#AddressDetailsSingle", () => { + // details route handler. + const detailsSingle = addressRoute.testableComponents.detailsSingle + + it("should throw 400 if address is empty", async () => { + const result = await detailsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should error on an array", async () => { + req.params.address = [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + + const result = await detailsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "address can not be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.params.address = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + const result = await detailsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.params.address = `bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3` + + const result = await detailsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.params.address = `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await detailsSingle(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should default to page 0", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert current page defaults to 0 + assert.equal(result.currentPage, 0) + }) + + it("should process the requested page", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + req.query.page = 5 + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=5000&to=6000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsSingle(req, res) + + // Assert current page is same as requested + assert.equal(result.currentPage, 5) + }) + + it("should calculate the total number of pages", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsSingle(req, res) + + assert.equal(result.pagesTotal, 1) + }) + + it("should get details for a single address", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.hasAllKeys(result, [ + "balance", + "balanceSat", + "totalReceived", + "totalReceivedSat", + "totalSent", + "totalSentSat", + "unconfirmedBalance", + "unconfirmedBalanceSat", + "unconfirmedTxApperances", + "txApperances", + "transactions", + "legacyAddress", + "cashAddress", + "currentPage", + "pagesTotal" + ]) + }) + }) + + describe("#AddressUtxoBulk", () => { + // utxo route handler. + const utxoBulk = addressRoute.testableComponents.utxoBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await utxoBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + address: `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + } + + const result = await utxoBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.body = { + addresses: [`02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + const result = await utxoBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.body = { + addresses: [`bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3`] + } + + const result = await utxoBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body = { + addresses: [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api" + + const result = await utxoBulk(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get utxos for a single address", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz/utxo`) + .reply(200, mockData.mockUtxoDetails) + } + + // Call the details API. + const result = await utxoBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result, "result should be an array") + + // Each element should have these primary properties. + assert.hasAllKeys(result[0], [ + "utxos", + "legacyAddress", + "cashAddress", + "scriptPubKey" + ]) + + // Validate the UTXO data structure. + assert.hasAnyKeys(result[0].utxos[0], [ + "txid", + "vout", + "amount", + "satoshis", + "height", + "confirmations" + ]) + }) + + it("should get utxos for mulitple addresses", async () => { + req.body = { + addresses: [ + `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`, + `bchtest:qzknfggae0av6yvxk77gmyq7syc67yux6sk80haqyr` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz/utxo`) + .reply(200, mockData.mockUtxoDetails) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mwJnEzXzKkveF2q5Af9jxi9j1zrtWAnPU8/utxo`) + .reply(200, mockData.mockUtxoDetails) + } + + // Call the details API. + const result = await utxoBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "2 outputs for 2 inputs") + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await utxoBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + }) + + describe("#AddressUtxoSingle", () => { + // details route handler. + const utxoSingle = addressRoute.testableComponents.utxoSingle + + it("should throw 400 if address is empty", async () => { + const result = await utxoSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should error on an array", async () => { + req.params.address = [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + + const result = await utxoSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "address can not be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.params.address = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + const result = await utxoSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.params.address = `bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3` + + const result = await utxoSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await utxoSingle(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get details for a single address", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz/utxo`) + .reply(200, mockData.mockUtxoDetails) + } + + // Call the details API. + const result = await utxoSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Each element should have these primary properties. + assert.hasAllKeys(result, [ + "utxos", + "legacyAddress", + "cashAddress", + "scriptPubKey" + ]) + + // Validate the UTXO data structure. + assert.hasAnyKeys(result.utxos[0], [ + "txid", + "vout", + "amount", + "satoshis", + "height", + "confirmations" + ]) + }) + }) + + describe("#AddressUnconfirmedBulk", () => { + // unconfirmed route handler. + const unconfirmedBulk = addressRoute.testableComponents.unconfirmedBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await unconfirmedBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + address: `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + } + + const result = await unconfirmedBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await unconfirmedBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw an error for an invalid address", async () => { + req.body = { + addresses: [`02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + const result = await unconfirmedBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.body = { + addresses: [`bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3`] + } + + const result = await unconfirmedBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body = { + addresses: [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api" + + const result = await unconfirmedBulk(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get unconfirmed data for a single address", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz/utxo`) + .reply(200, mockData.mockUtxoDetails) + } + + // Call the details API. + const result = await unconfirmedBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + //console.log(`result[0].utxos: ${util.inspect(result[0].utxos)}`) + + assert.isArray(result, "result should be an array") + + // Dev note: Unconfirmed TXs are hard to test in an integration test because + // the nature of an unconfirmed transation is transient. It quickly becomes + // confirmed and thus should not show up. + }) + + it("should get unconfirmed data for an array of addresses", async () => { + req.body = { + addresses: [ + `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`, + `bchtest:qzknfggae0av6yvxk77gmyq7syc67yux6sk80haqyr` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz/utxo`) + .reply(200, mockData.mockUtxoDetails) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mwJnEzXzKkveF2q5Af9jxi9j1zrtWAnPU8/utxo`) + .reply(200, mockData.mockUtxoDetails) + } + + // Call the details API. + const result = await unconfirmedBulk(req, res) + + assert.isArray(result) + }) + }) + + describe("#AddressUnconfirmedSingle", () => { + // details route handler. + const unconfirmedSingle = addressRoute.testableComponents.unconfirmedSingle + + it("should throw 400 if address is empty", async () => { + const result = await unconfirmedSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should error on an array", async () => { + req.params.address = [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + + const result = await unconfirmedSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "address can not be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.params.address = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + const result = await unconfirmedSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.params.address = `bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3` + + const result = await unconfirmedSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.params.address = `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await unconfirmedSingle(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get details for a single address", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz/utxo`) + .reply(200, mockData.mockUtxoDetails) + } + + // Call the details API. + const result = await unconfirmedSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Each element should have these primary properties. + assert.hasAllKeys(result, [ + "utxos", + "legacyAddress", + "cashAddress", + "scriptPubKey" + ]) + + assert.isArray(result.utxos) + }) + }) + + describe("#AddressTransactionsBulk", () => { + // unconfirmed route handler. + const transactionsBulk = addressRoute.testableComponents.transactionsBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await transactionsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + address: `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + } + + const result = await transactionsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await transactionsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw an error for an invalid address", async () => { + req.body = { + addresses: [`02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + const result = await transactionsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.body = { + addresses: [`bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3`] + } + + const result = await transactionsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body = { + addresses: [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api" + + const result = await transactionsBulk(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should default to page 0", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the endpoint + const result = await transactionsBulk(req, res) + + // Assert current page defaults to 0 + assert.equal(result[0].currentPage, 0) + }) + + it("should process the requested page", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`], + page: 5 + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=5` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the endpoint + const result = await transactionsBulk(req, res) + + // Assert current page is same as requested + assert.equal(result[0].currentPage, 5) + }) + + it("should get transactions for a single address", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the details API. + const result = await transactionsBulk(req, res) + + assert.isArray(result, "result should be an array") + + assert.exists(result[0].pagesTotal) + assert.exists(result[0].currentPage) + assert.exists(result[0].txs) + assert.isArray(result[0].txs) + assert.exists(result[0].legacyAddress) + assert.exists(result[0].cashAddress) + }) + + it("should get transactions for an array of addresses", async () => { + req.body = { + addresses: [ + `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`, + `bchtest:qzknfggae0av6yvxk77gmyq7syc67yux6sk80haqyr` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0` + ) + .reply(200, mockData.mockTransactions) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qzknfggae0av6yvxk77gmyq7syc67yux6sk80haqyr&pageNum=0` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the details API. + const result = await transactionsBulk(req, res) + + assert.isArray(result, "result should be an array") + + assert.equal(result.length, 2, "Array should have 2 elements") + }) + }) + + describe("#AddressTransactionsSingle", () => { + // details route handler. + const transactionsSingle = + addressRoute.testableComponents.transactionsSingle + + it("should throw 400 if address is empty", async () => { + const result = await transactionsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should error on an array", async () => { + req.params.address = [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + + const result = await transactionsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "address can not be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.params.address = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + const result = await transactionsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.params.address = `bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3` + + const result = await transactionsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.params.address = `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await transactionsSingle(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should default to page 0", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the endpoint + const result = await transactionsSingle(req, res) + + // Assert current page defaults to 0 + assert.equal(result.currentPage, 0) + }) + + it("should process the requested page", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + req.query.page = 5 + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=5` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the endpoint + const result = await transactionsSingle(req, res) + + // Assert current page is same as requested + assert.equal(result.currentPage, 5) + }) + + it("should get details for a single address", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the details API. + const result = await transactionsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.exists(result.pagesTotal) + assert.exists(result.currentPage) + assert.exists(result.txs) + assert.isArray(result.txs) + assert.exists(result.legacyAddress) + assert.exists(result.cashAddress) + }) + }) + + describe("#AddressFromXPubSingle", () => { + // details route handler. + const fromXPubSingle = addressRoute.testableComponents.fromXPubSingle + + it("should throw 400 if xpub is empty", async () => { + const result = await fromXPubSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "xpub can not be empty") + }) + + it("should error on an array", async () => { + req.params.xpub = [ + `tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM` + ] + + const result = await fromXPubSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "xpub can not be an array", + "Proper error message" + ) + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.params.xpub = `tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM` + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await fromXPubSingle(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should create an address from xpub", async () => { + req.params.xpub = `tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM` + + // Mock the Insight URL for unit tests. + // TODO add unit test + // if (process.env.TEST === "unit") { + // nock(`${process.env.BITCOINCOM_BASEURL}`) + // .get( + // `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0` + // ) + // .reply(200, mockData.mockTransactions) + // } + + // Call the details API. + const result = await fromXPubSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.exists(result.legacyAddress) + assert.exists(result.cashAddress) + }) + }) +}) diff --git a/test/v3/block.js b/test/v3/block.js new file mode 100644 index 0000000..754376f --- /dev/null +++ b/test/v3/block.js @@ -0,0 +1,610 @@ +"use strict" + +const blockRoute = require("../../src/routes/v3/block") +const chai = require("chai") +const assert = chai.assert +const nock = require("nock") // HTTP mocking + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/block-mock") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#Block", () => { + let req, res + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", () => { + // root route handler. + const root = blockRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + + assert.equal(result.status, "block", "Returns static string") + }) + }) + + describe("#detailsByHashSingle", () => { + const detailsByHash = blockRoute.testableComponents.detailsByHashSingle + + it("should throw an error for an empty hash", async () => { + req.params.hash = "" + + const result = await detailsByHash(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hash must not be empty", + "Proper error message" + ) + }) + + it("should throw 50X when network issues", async () => { + // Save the existing RPC URL. + const savedUrl = process.env.BITCOINCOM_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + req.params.hash = "abc123" + const result = await detailsByHash(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.isAbove(res.statusCode, 499, "HTTP status code 50X expected.") + //assert.include(result.error, "ENOTFOUND", "Error message expected") + }) + + it("should throw an error for invalid hash", async () => { + req.params.hash = "abc123" + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${req.params.hash}`) + .reply(404, "Not found") + } + + const result = await detailsByHash(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.include(result.error, "Not found", "Proper error message") + }) + + it("should GET /detailsByHash/:hash", async () => { + req.params.hash = + "00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79" + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${req.params.hash}`) + .reply(200, mockData.mockBlockDetails) + } + + const result = await detailsByHash(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "hash", + "size", + "height", + "version", + "merkleroot", + "tx", + "time", + "nonce", + "bits", + "difficulty", + "chainwork", + "confirmations", + "previousblockhash", + "nextblockhash", + "reward", + "isMainChain", + "poolInfo" + ]) + assert.isArray(result.tx) + }) + }) + + describe("#detailsByHashBulk", () => { + // details route handler. + const detailsByHashBulk = blockRoute.testableComponents.detailsByHashBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await detailsByHashBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hashes needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + hashes: + "00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79" + } + + const result = await detailsByHashBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hashes needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.hashes = testArray + + const result = await detailsByHashBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw an error for an invalid hash", async () => { + req.body = { + hashes: [`abc123`] + } + + const result = await detailsByHashBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid hash", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body = { + hashes: [ + "00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79" + ] + } + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await detailsByHashBulk(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get details for a single hash", async () => { + req.body = { + hashes: [ + "00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79" + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${req.body.hashes[0]}`) + .reply(200, mockData.mockBlockDetails) + } + + // Call the details API. + const result = await detailsByHashBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.equal(result.length, 1, "Array with one entry") + assert.hasAllKeys(result[0], [ + "bits", + "chainwork", + "confirmations", + "difficulty", + "hash", + "height", + "isMainChain", + "merkleroot", + "nextblockhash", + "nonce", + "poolInfo", + "previousblockhash", + "reward", + "size", + "time", + "tx", + "version" + ]) + }) + + it("should get details for multiple hashes", async () => { + req.body = { + hashes: [ + `00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79`, + `00000000c2b2c19cf499f57d5b0f724c6df753330d7acc7d4a8ebe412d427bd0` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${req.body.hashes[0]}`) + .reply(200, mockData.mockBlockDetails) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${req.body.hashes[1]}`) + .reply(200, mockData.mockBlockDetails) + } + + // Call the details API. + const result = await detailsByHashBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "2 outputs for 2 inputs") + }) + + it("should throw an error if hash not found", async () => { + req.body = { + hashes: [ + `00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44abcdef` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${req.body.hashes[0]}`) + //.reply(404, { error: { message: "Not Found" } }) + .reply(404, "Not found") + } + + const result = await detailsByHashBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 404, "HTTP status code 404 expected.") + assert.include(result.error, "Not found", "Proper error message") + }) + }) + + describe("Block Details By Height", () => { + // block route handler. + const detailsByHeight = blockRoute.testableComponents.detailsByHeightSingle + + it("should throw an error for an empty height", async () => { + req.params.height = "" + + const result = await detailsByHeight(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "height must not be empty", + "Proper error message" + ) + }) + + it("should throw 500 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl = process.env.BITCOINCOM_BASEURL + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.height = "abc123" + const result = await detailsByHeight(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or great expected." + ) + //assert.include(result.error, "ENOTFOUND", "Error message expected") + }) + + it("should throw an error for invalid height", async () => { + req.params.height = "abc123" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(500, { + error: { + code: -1, + message: "JSON value is not an integer as expected" + } + }) + } + + const result = await detailsByHeight(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "JSON value is not an integer as expected", + "Proper error message" + ) + }) + + it("should GET /detailsByHeight/:height", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockHash }) + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/block/00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79` + ) + .reply(200, mockData.mockBlockDetails) + } + + req.params.height = 500000 + + const result = await detailsByHeight(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "hash", + "size", + "height", + "version", + "merkleroot", + "tx", + "time", + "nonce", + "bits", + "difficulty", + "chainwork", + "confirmations", + "previousblockhash", + "nextblockhash", + "reward", + "isMainChain", + "poolInfo" + ]) + assert.isArray(result.tx) + }) + }) + + describe("#detailsByHeightBulk", () => { + // details route handler. + const detailsByHeightBulk = + blockRoute.testableComponents.detailsByHeightBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await detailsByHeightBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "heights needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single height", async () => { + req.body = { + heights: 500000 + } + + const result = await detailsByHeightBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "heights needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.heights = testArray + + const result = await detailsByHeightBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw error for an invalid height", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(500, { + error: { + code: -1, + message: "JSON value is not an integer as expected" + } + }) + } + + req.body.heights = [`abc123`] + + const result = await detailsByHeightBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body.heights = [`500000`] + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await detailsByHeightBulk(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get details for a single height", async () => { + req.body.heights = [`500000`] + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockHash }) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${mockData.mockBlockHash}`) + .reply(200, mockData.mockBlockDetails) + } + + // Call the details API. + const result = await detailsByHeightBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.equal(result.length, 1, "Array with one entry") + assert.hasAllKeys(result[0], [ + "bits", + "chainwork", + "confirmations", + "difficulty", + "hash", + "height", + "isMainChain", + "merkleroot", + "nextblockhash", + "nonce", + "poolInfo", + "previousblockhash", + "reward", + "size", + "time", + "tx", + "version" + ]) + }) + + it("should get details for multiple block heights", async () => { + req.body = { + heights: [`500000`, `500001`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: mockData.mockBlockHash }) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${mockData.mockBlockHash}`) + .times(2) + .reply(200, mockData.mockBlockDetails) + } + + // Call the details API. + const result = await detailsByHeightBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "2 outputs for 2 inputs") + }) + }) +}) diff --git a/test/v3/blockchain.js b/test/v3/blockchain.js new file mode 100644 index 0000000..b883f67 --- /dev/null +++ b/test/v3/blockchain.js @@ -0,0 +1,1148 @@ +/* + TODO: + -getRawMempool + --Add tests for 'verbose' input values + -getMempoolEntry & getMempoolEntryBulk + --Needs e2e test to create unconfirmed tx, for real-world test. +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const nock = require("nock") // HTTP mocking +const blockchainRoute = require("../../src/routes/v3/blockchain") + +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +if (!process.env.TEST) process.env.TEST = "unit" + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/blockchain-mock") + +let originalEnvVars // Used during transition from integration to unit tests. + +describe("#BlockchainRouter", () => { + let req, res + + // local node will be started in regtest mode on the port 48332 + //before(panda.runLocalNode) + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // otherwise the panda will run forever + //process.exit() + + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", () => { + // root route handler. + const root = blockchainRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + + assert.equal(result.status, "blockchain", "Returns static string") + }) + }) + + describe("getBestBlockHash()", () => { + // block route handler. + const getBestBlockHash = blockchainRoute.testableComponents.getBestBlockHash + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getBestBlockHash(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getBestBlockHash", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockHash }) + } + + const result = await getBestBlockHash(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isString(result) + assert.equal(result.length, 64, "Hash string is fixed length") + }) + }) + + describe("getBlockchainInfo()", () => { + // block route handler. + const getBlockchainInfo = + blockchainRoute.testableComponents.getBlockchainInfo + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getBlockchainInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getBlockchainInfo", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockchainInfo }) + } + + const result = await getBlockchainInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "chain", + "blocks", + "headers", + "bestblockhash", + "difficulty", + "mediantime", + "verificationprogress", + "chainwork", + "pruned", + "softforks", + "bip9_softforks" + ]) + }) + }) + + describe("getBlockCount()", () => { + // block route handler. + const getBlockCount = blockchainRoute.testableComponents.getBlockCount + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getBlockCount(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getBlockCount", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: 126769 }) + } + + const result = await getBlockCount(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isNumber(result) + }) + }) + + describe("getBlockHeaderSingle()", async () => { + const getBlockHeader = + blockchainRoute.testableComponents.getBlockHeaderSingle + + it("should throw 400 error if hash is missing", async () => { + const result = await getBlockHeader(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "hash can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.hash = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await getBlockHeader(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Network error: Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET block header", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { + result: + "0000ff7f7d217c9b7845ea8b50d620c59a1bf7c276566406e9b7bc7e463e0000000000006d70322c0b697c1c81d2744f87f09f1e9780ba5d30338952e2cdc64e60456f8423bb0a5ceafa091a3e843526" + }) + } + + req.params.hash = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await getBlockHeader(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isString(result) + assert.equal( + result, + "0000ff7f7d217c9b7845ea8b50d620c59a1bf7c276566406e9b7bc7e463e0000000000006d70322c0b697c1c81d2744f87f09f1e9780ba5d30338952e2cdc64e60456f8423bb0a5ceafa091a3e843526" + ) + }) + + it("should GET verbose block header", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockHeader }) + } + + req.query.verbose = true + req.params.hash = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await getBlockHeader(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, [ + "hash", + "confirmations", + "height", + "version", + "versionHex", + "merkleroot", + "time", + "mediantime", + "nonce", + "bits", + "difficulty", + "chainwork", + "previousblockhash", + "nextblockhash" + ]) + }) + }) + + describe("#getBlockHeaderBulk", () => { + // route handler. + const getBlockHeaderBulk = + blockchainRoute.testableComponents.getBlockHeaderBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await getBlockHeaderBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hashes needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single hash", async () => { + req.body.hashes = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await getBlockHeaderBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hashes needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.hashes = testArray + + const result = await getBlockHeaderBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw a 400 error for an invalid hash", async () => { + req.body.hashes = ["badHash"] + + await getBlockHeaderBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body.hashes = [ + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + ] + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await getBlockHeaderBulk(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get concise block header for a single hash", async () => { + req.body.hashes = [ + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + ] + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockHeaderConcise }) + } + + // Call the details API. + const result = await getBlockHeaderBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.isArray(result) + assert.equal( + result[0], + "0000ff7f7d217c9b7845ea8b50d620c59a1bf7c276566406e9b7bc7e463e0000000000006d70322c0b697c1c81d2744f87f09f1e9780ba5d30338952e2cdc64e60456f8423bb0a5ceafa091a3e843526" + ) + }) + + it("should get verbose block header for a single hash", async () => { + req.body = { + hashes: [ + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + ], + verbose: true + } + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockHeader }) + } + + // Call the details API. + const result = await getBlockHeaderBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "hash", + "confirmations", + "height", + "version", + "versionHex", + "merkleroot", + "time", + "mediantime", + "nonce", + "bits", + "difficulty", + "chainwork", + "previousblockhash", + "nextblockhash" + ]) + }) + + it("should get details for multiple block heights", async () => { + req.body.hashes = [ + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900", + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + ] + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: mockData.mockBlockHeaderConcise }) + } + + // Call the details API. + const result = await getBlockHeaderBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "2 outputs for 2 inputs") + }) + }) + + describe("getChainTips()", () => { + // block route handler. + const getChainTips = blockchainRoute.testableComponents.getChainTips + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getChainTips(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getChainTips", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockChainTips }) + } + + const result = await getChainTips(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], ["height", "hash", "branchlen", "status"]) + }) + }) + + describe("getDifficulty()", () => { + // block route handler. + const getDifficulty = blockchainRoute.testableComponents.getDifficulty + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getDifficulty(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getDifficulty", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: 4049809.205246544 }) + } + + const result = await getDifficulty(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isNumber(result) + }) + }) + + describe("getMempoolInfo()", () => { + // block route handler. + const getMempoolInfo = blockchainRoute.testableComponents.getMempoolInfo + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getMempoolInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getMempoolInfo", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockMempoolInfo }) + } + + const result = await getMempoolInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "result", + "bytes", + "usage", + "maxmempool", + "mempoolminfree" + ]) + }) + }) + + describe("getRawMempool()", () => { + // block route handler. + const getRawMempool = blockchainRoute.testableComponents.getRawMempool + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getRawMempool(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getMempoolInfo", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockRawMempool }) + } + + const result = await getRawMempool(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + // Not sure what other assertions should be made here. + }) + }) + + describe("getMempoolEntry()", () => { + // block route handler. + const getMempoolEntry = + blockchainRoute.testableComponents.getMempoolEntrySingle + + it("should throw 400 if txid is empty", async () => { + const result = await getMempoolEntry(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await getMempoolEntry(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getMempoolEntry", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: { error: "Transaction not in mempool" } }) + } + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await getMempoolEntry(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.isString(result.error) + assert.equal(result.error, "Transaction not in mempool") + }) + }) + + describe("#getMempoolEntryBulk", () => { + // route handler. + const getMempoolEntryBulk = + blockchainRoute.testableComponents.getMempoolEntryBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await getMempoolEntryBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txids needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single txid", async () => { + req.body.txids = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await getMempoolEntryBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txids needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.txids = testArray + + const result = await getMempoolEntryBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + // Only execute on integration tests. + if (process.env.TEST !== "unit") { + // Dev-note: This test passes because it expects an error. TXIDs do not + // stay in the mempool for long, so it does not work well for a unit or + // integration test. + it("should retrieve single mempool entry", async () => { + req.body.txids = [ + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + ] + + const result = await getMempoolEntryBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.isString(result.error) + assert.equal(result.error, "Transaction not in mempool") + }) + + // Dev-note: This test passes because it expects an error. TXIDs do not + // stay in the mempool for long, so it does not work well for a unit or + // integration test. + it("should retrieve multiple mempool entries", async () => { + req.body.txids = [ + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde`, + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + ] + + const result = await getMempoolEntryBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.isString(result.error) + assert.equal(result.error, "Transaction not in mempool") + }) + } + }) + + describe("getTxOut()", () => { + // block route handler. + const getTxOut = blockchainRoute.testableComponents.getTxOut + + it("should throw 400 if txid is empty", async () => { + const result = await getTxOut(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should throw 400 if n is empty", async () => { + req.params.txid = `sometxid` + const result = await getTxOut(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "n can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.n = 0 + + const result = await getTxOut(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + // This test can only run for unit tests. See TODO at the top of this file. + it("should GET /getTxOut", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockTxOut }) + } + + req.params.txid = `5747e6462e2c452a5d583fd6a5f82866cd8e4a86826c86d9a1842b7d023e0c0c` + req.params.n = 1 + + const result = await getTxOut(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, [ + "bestblock", + "confirmations", + "value", + "scriptPubKey", + "coinbase" + ]) + assert.hasAllKeys(result.scriptPubKey, [ + "asm", + "hex", + "reqSigs", + "type", + "addresses" + ]) + assert.isArray(result.scriptPubKey.addresses) + }) + }) + + describe("getTxOutProof()", () => { + const getTxOutProof = blockchainRoute.testableComponents.getTxOutProofSingle + + it("should throw 400 if txid is empty", async () => { + const result = await getTxOutProof(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await getTxOutProof(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getTxOutProof", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockTxOutProof }) + } + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await getTxOutProof(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isString(result) + }) + }) + + describe("#getTxOutProofBulk", () => { + // route handler. + const getTxOutProofBulk = + blockchainRoute.testableComponents.getTxOutProofBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await getTxOutProofBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txids needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single txid", async () => { + req.body.txids = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await getTxOutProofBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txids needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.txids = testArray + + const result = await getTxOutProofBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should GET proof for single txid", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockTxOutProof }) + } + + req.body.txids = [ + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + ] + + const result = await getTxOutProofBulk(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isString(result[0]) + }) + + it("should GET proof for multiple txids", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: mockData.mockTxOutProof }) + } + + req.body.txids = [ + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde`, + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + ] + + const result = await getTxOutProofBulk(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "Correct length of returned array") + }) + }) + + describe("verifyTxOutProof()", () => { + const verifyTxOutProof = + blockchainRoute.testableComponents.verifyTxOutProofSingle + + it("should throw 400 if proof is empty", async () => { + const result = await verifyTxOutProof(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "proof can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.proof = mockData.mockTxOutProof + + const result = await verifyTxOutProof(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /verifyTxOutProof", async () => { + const expected = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: [expected] }) + } + + req.params.proof = mockData.mockTxOutProof + + const result = await verifyTxOutProof(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isString(result[0]) + assert.equal(result[0], expected) + }) + }) + + describe("#verifyTxOutProofBulk", () => { + // route handler. + const verifyTxOutProofBulk = + blockchainRoute.testableComponents.verifyTxOutProofBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await verifyTxOutProofBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "proofs needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single txid", async () => { + req.body.proofs = mockData.mockTxOutProof + + const result = await verifyTxOutProofBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "proofs needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.proofs = testArray + + const result = await verifyTxOutProofBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should get single proof", async () => { + const expected = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: [expected] }) + } + + req.body.proofs = [mockData.mockTxOutProof] + + const result = await verifyTxOutProofBulk(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isString(result[0]) + assert.equal(result[0], expected) + }) + + it("should get multiple proofs", async () => { + const expected = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: [expected] }) + } + + req.body.proofs = [mockData.mockTxOutProof, mockData.mockTxOutProof] + + const result = await verifyTxOutProofBulk(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isString(result[0]) + assert.equal(result[0], expected) + assert.equal(result.length, 2) + }) + }) +}) diff --git a/test/v3/control.js b/test/v3/control.js new file mode 100644 index 0000000..fd63e48 --- /dev/null +++ b/test/v3/control.js @@ -0,0 +1,140 @@ +/* + TESTS FOR THE CONTROL.JS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. + +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const controlRoute = require("../../src/routes/v3/control") +const nock = require("nock") // HTTP mocking + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/control-mock") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#ControlRouter", () => { + let req, res + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", async () => { + // root route handler. + const root = controlRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result.status, "control", "Returns static string") + }) + }) + + describe("#GetInfo", () => { + const getInfo = controlRoute.testableComponents.getInfo + + it("should throw 500 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error, "ENOTFOUND", "Error message expected") + }) + + it("should get info on the full node", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockGetInfo }) + } + + const result = await getInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "version", + "protocolversion", + "blocks", + "timeoffset", + "connections", + "proxy", + "difficulty", + "testnet", + "paytxfee", + "relayfee", + "errors" + ]) + }) + }) +}) diff --git a/test/v3/e2e/address.js b/test/v3/e2e/address.js new file mode 100644 index 0000000..878ad9e --- /dev/null +++ b/test/v3/e2e/address.js @@ -0,0 +1,60 @@ +/* + End-to-end tests for the Address endpoint. + These tests assume that the repo is running locally and pointed at TESTNET +*/ + +"use strict" + +const rp = require("request-promise") + +const rawtransactions = require("../../../dist/routes/v2/address") + +/* + This unconfirmed utxo test needs to be expanded in the future to automatically + create a transaction. For now, that is done manually and the address provided + as a constant. + + 1. Start rest running locally, pointed at TESTNET. + 2. Fund each address with a small amount of tBCH + 3. Run this program with `node address.js` + 4. If successful, it will return the unconfirmed UTXOs. +*/ +const addr1 = "bchtest:qpdcp5pv7qphu5tsjfgwezld9n9aq9ue6swgqpf45c" +const addr2 = "bchtest:qzpvx999fagau0xqmvu3xvll9evapge7u5hcgeknzv" + +async function testSingleUnconfirmed() { + try { + const options = { + method: "GET", + uri: `http://localhost:3000/v2/address/unconfirmed/${addr1}`, + resolveWithFullResponse: true, + json: true + } + + const result = await rp(options) + // console.log(`result.body: ${JSON.stringify(result.body, null, 2)}`) + } catch (err) { + // console.log(`Error: `, err) + } +} +testSingleUnconfirmed() + +async function testDoubleUnconfirmed() { + try { + const options = { + method: "POST", + uri: `http://localhost:3000/v2/address/unconfirmed`, + resolveWithFullResponse: true, + json: true, + body: { + addresses: [addr1, addr2] + } + } + + const result = await rp(options) + // console.log(`result.body: ${JSON.stringify(result.body, null, 2)}`) + } catch (err) { + // console.log(`Error: `, err) + } +} +testDoubleUnconfirmed() diff --git a/test/v3/helpers/panda.js b/test/v3/helpers/panda.js new file mode 100644 index 0000000..48a7d7f --- /dev/null +++ b/test/v3/helpers/panda.js @@ -0,0 +1,29 @@ +"use strict" + +/** + * Read more about panda here: https://panda-suite.github.io/ + */ +const panda = require("pandacash-core") + +const runLocalNode = done => { + const server = panda.server({ + // always the same mnemonic + // mnemonic: "cigar magnet ocean purchase travel damp snack alone theme budget wagon wrong", + seedAccounts: true, + enableLogs: false, + debug: false + }) + + server.listen({ + port: 48332, + walletPort: 48333, + }, (err, pandaCashCore) => { + if (err) return console.error(err) + + done() + }) +} + +module.exports = { + runLocalNode +} diff --git a/test/v3/integration/README.md b/test/v3/integration/README.md new file mode 100644 index 0000000..3bd22f8 --- /dev/null +++ b/test/v3/integration/README.md @@ -0,0 +1,7 @@ +This directory contains integration tests. To run them requires that +a local copy of rest.bitcoin.com is running. The tests in this directory are +exactly the same calls that end users would use for their app to interact +directly with rest.bitcoin.com. + +At present, this directory focuses on raw-transations. But more integration tests +will be added. diff --git a/test/v3/integration/rate-limits.js b/test/v3/integration/rate-limits.js new file mode 100644 index 0000000..b61c25e --- /dev/null +++ b/test/v3/integration/rate-limits.js @@ -0,0 +1,94 @@ +/* + */ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const axios = require("axios") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +const SERVER = `https://rest.btctest.net/v2/` +//const SERVER = `http://localhost:3000/v2/` + +describe("#rate limits", () => { + it("should get control/getInfo() with no auth", async () => { + const options = { + method: "GET", + url: `${SERVER}control/getInfo` + } + + const result = await axios(options) + //console.log(`result.status: ${result.status}`) + //console.log(`result.data: ${util.inspect(result.data)}`) + + assert.equal(result.status, 200) + assert.hasAnyKeys(result.data, ["version"]) + }) + + it("should trigger rate-limit handler if rate limits exceeds 60 request per minute", async () => { + try { + // Actual rate limit is 60 per minute X 4 nodes = 240 rpm. + const options = { + method: "GET", + url: `${SERVER}control/getInfo` + } + + const promises = [] + for (let i = 0; i < 250; i++) { + const promise = axios(options) + promises.push(promise) + } + + await Promise.all(promises) + + assert.equal(true, false, "Unexpected result!") + } catch (err) { + //console.log(`err.response: ${util.inspect(err.response)}`) + + assert.equal(err.response.status, 429) + assert.include(err.response.data.error, "Too many requests") + } + }) + + it("should not trigger rate-limit handler if correct pro-tier password is used", async () => { + try { + const username = "BITBOX" + + // Pro-tier is accessed by using the right password. + const password = "BITBOX" + //const password = "something" + + const combined = `${username}:${password}` + const base64Credential = Buffer.from(combined).toString("base64") + const readyCredential = `Basic ${base64Credential}` + + const options = { + method: "GET", + url: `${SERVER}control/getInfo`, + headers: { Authorization: readyCredential } + } + + const promises = [] + for (let i = 0; i < 250; i++) { + const promise = axios(options) + promises.push(promise) + } + + await Promise.all(promises) + + assert.equal(true, true, "Not throwing an error is a pass!") + } catch (err) { + // console.log(`err.response: ${util.inspect(err.response)}`) + + assert.equal( + true, + false, + "This error handler should not have been triggered. Is the password correct?" + ) + } + }) +}) diff --git a/test/v3/integration/raw-transactions.js b/test/v3/integration/raw-transactions.js new file mode 100644 index 0000000..7c17347 --- /dev/null +++ b/test/v3/integration/raw-transactions.js @@ -0,0 +1,191 @@ +/* + These integration tests are intended to be run against a live local copy of + rest.bitcoin.com. They exercise the endpoints in the same way the SDK or + end-user application would. These tests were created to replace the parts + removed from the swagger UI, that otherwise would have excersiced these endpoints. + + TODO: + -/rawtransactions/sendRawTransaction is more appropropriate for an e2e test, + so it is omitted here. +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const rawtransactions = require("../../../dist/routes/v2/rawtransactions") +const rp = require("request-promise") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +const mockData = require("../mocks/raw-transactions-mocks") + +describe("#Raw-Transactions", () => { + describe("#root", () => { + it(`should return root`, async () => { + const options = { + method: "GET", + uri: `http://localhost:3000/v2/rawtransactions/`, + resolveWithFullResponse: true, + json: true + } + + const result = await rp(options) + //console.log(`result: ${JSON.stringify(result, null, 0)}`) + + assert.equal(result.body.status, "rawtransactions") + }) + }) + + describe("#whCreateTx", () => { + it(`should return tx hex`, async () => { + const minIn = { + txid: + "f7ed9cf23dee85910f6269c9a101a75fcfd2f3c6fc81f17fad824ff7aaf99ab2", + vout: 1 + } + + const options = { + method: "PUT", + uri: `http://localhost:3000/v2/rawtransactions/create`, + resolveWithFullResponse: true, + json: true, + body: { + //inputs: [mockData.mockWHCreateInput], + inputs: [minIn], + outputs: {} + } + } + + const result = await rp(options) + //console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`) + + assert.equal( + result.body, + "0200000001b29af9aaf74f82ad7ff181fcc6f3d2cf5fa701a1c969620f9185ee3df29cedf70100000000ffffffff0000000000" + ) + }) + }) + + describe("#whOpReturn", () => { + it(`should return tx hex`, async () => { + const options = { + method: "PUT", + uri: `http://localhost:3000/v2/rawtransactions/opreturn`, + resolveWithFullResponse: true, + json: true, + body: { + rawtx: "01000000000000000000", + payload: "00000000000000020000000006dac2c0" + } + } + + const result = await rp(options) + //console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`) + + assert.equal( + result.body, + "0100000000010000000000000000166a140877686300000000000000020000000006dac2c000000000" + ) + }) + }) + + describe("#whReference", () => { + it(`should return tx hex`, async () => { + const options = { + method: "PUT", + uri: `http://localhost:3000/v2/rawtransactions/reference`, + resolveWithFullResponse: true, + json: true, + body: { + rawtx: + "0100000001a7a9402ecd77f3c9f745793c9ec805bfa2e14b89877581c734c774864247e6f50400000000ffffffff03aa0a0000000000001976a9146d18edfe073d53f84dd491dae1379f8fb0dfe5d488ac5c0d0000000000004751210252ce4bdd3ce38b4ebbc5a6e1343608230da508ff12d23d85b58c964204c4cef3210294cc195fc096f87d0f813a337ae7e5f961b1c8a18f1f8604a909b3a5121f065b52aeaa0a0000000000001976a914946cb2e08075bcbaf157e47bcb67eb2b2339d24288ac00000000", + destination: "bitcoincash:qrn60nerx5zug4u4hal06atep3lzhtecvy4pxk75lf", + amount: 0.005 + } + } + + const result = await rp(options) + //console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`) + + assert.isString(result.body) + }) + }) + + describe("#whChangeOutput", () => { + it(`should return tx hex`, async () => { + const options = { + method: "PUT", + uri: `http://localhost:3000/v2/rawtransactions/change`, + resolveWithFullResponse: true, + json: true, + body: { + rawtx: + "0100000001b15ee60431ef57ec682790dec5a3c0d83a0c360633ea8308fbf6d5fc10a779670400000000ffffffff025c0d00000000000047512102f3e471222bb57a7d416c82bf81c627bfcd2bdc47f36e763ae69935bba4601ece21021580b888ff56feb27f17f08802ebed26258c23697d6a462d43fc13b565fda2dd52aeaa0a0000000000001976a914946cb2e08075bcbaf157e47bcb67eb2b2339d24288ac00000000", + destination: "bitcoincash:qrn60nerx5zug4u4hal06atep3lzhtecvy4pxk75lf", + prevtxs: [ + { + txid: + "6779a710fcd5f6fb0883ea3306360c3ad8c0a3c5de902768ec57ef3104e65eb1", + vout: 4, + scriptPubKey: + "76a9147b25205fd98d462880a3e5b0541235831ae959e588ac", + value: 0.00068257 + } + ], + fee: 0.000035 + } + } + + const result = await rp(options) + //console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`) + + assert.isString(result.body) + }) + }) + + describe("#whInput", () => { + it(`should return tx hex`, async () => { + const options = { + method: "PUT", + uri: `http://localhost:3000/v2/rawtransactions/input`, + resolveWithFullResponse: true, + json: true, + body: { + txid: + "b006729017df05eda586df9ad3f8ccfee5be340aadf88155b784d1fc0e8342ee", + n: 0 + } + } + + const result = await rp(options) + //console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`) + + assert.isString(result.body) + }) + }) + + describe("#getRawTransaction", () => { + it(`should return tx hex`, async () => { + const options = { + method: "POST", + uri: `http://localhost:3000/v2/rawtransactions/getRawTransaction`, + resolveWithFullResponse: true, + json: true, + body: { + txids: [ + "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098" + ], + verbose: true + } + } + + const result = await rp(options) + // console.log(`result.body: ${util.inspect(result.body)}`) + + assert.isArray(result.body) + }) + }) +}) diff --git a/test/v3/mining.js b/test/v3/mining.js new file mode 100644 index 0000000..f24fede --- /dev/null +++ b/test/v3/mining.js @@ -0,0 +1,176 @@ +/* + TESTS FOR THE MINING.TS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const miningRoute = require("../../src/routes/v3/mining") +const nock = require("nock") // HTTP mocking + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/mining-mocks") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#Mining", () => { + let req, res + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", async () => { + // root route handler. + const root = miningRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result.status, "mining", "Returns static string") + }) + }) + + describe("#getMiningInfo", async () => { + const getMiningInfo = miningRoute.testableComponents.getMiningInfo + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getMiningInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should GET mining information", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockMiningInfo }) + } + + const result = await getMiningInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, [ + "blocks", + "currentblocksize", + "currentblocktx", + "difficulty", + "blockprioritypercentage", + "errors", + "networkhashps", + "pooledtx", + "chain" + ]) + }) + }) + + describe("#getNetworkHashPS", async () => { + const getNetworkHashPS = miningRoute.testableComponents.getNetworkHashPS + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getNetworkHashPS(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should GET Network Hash per second", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: 517604755.6648782 }) + } + + const result = await getNetworkHashPS(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isNumber(result) + }) + }) +}) diff --git a/test/v3/mocks/address-mock.js b/test/v3/mocks/address-mock.js new file mode 100644 index 0000000..7d4e3b9 --- /dev/null +++ b/test/v3/mocks/address-mock.js @@ -0,0 +1,160 @@ +/* + This library contains mocking data for running unit tests on the address route. +*/ + +"use strict" + +const mockAddressDetails = { + addrStr: "1Fg4r9iDrEkCcDmHTy2T79EusNfhyQpu7W", + balance: 0.00126419, + balanceSat: 126419, + totalReceived: 0.02175868, + totalReceivedSat: 2175868, + totalSent: 0.02049449, + totalSentSat: 2049449, + unconfirmedBalance: 0, + unconfirmedBalanceSat: 0, + unconfirmedTxApperances: 0, + txApperances: 3, + transactions: [ + "2dc053f55a666a3d2a08b1c680b704d62a55506d14ad884add87edcc56b9277d", + "544c15ce35c0f2e808d28f29d6587f1ec9276233e29856b7f2938cf0daef0026", + "81039b1d7b855b133f359f9dc65f776bd105650153a941675fedc504228ddbd3" + ], + legacyAddress: "1Fg4r9iDrEkCcDmHTy2T79EusNfhyQpu7W", + cashAddress: "bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c" +} + +const mockUtxoDetails = [ + { + txid: "15f6a584080b04911121fbaca7bfcf3dd64ef2bfa5a01daf31e05a296c3e5e9e", + vout: 286, + amount: 0.00001, + satoshis: 1000, + height: 546083, + confirmations: 3490 + }, + { + txid: "15f6a584080b04911121fbaca7bfcf3dd64ef2bfa5a01daf31e05a296c3e5e9e", + vout: 287, + amount: 0.00001, + satoshis: 1000, + height: 546083, + confirmations: 3490 + }, + { + txid: "15f6a584080b04911121fbaca7bfcf3dd64ef2bfa5a01daf31e05a296c3e5e9e", + vout: 288, + amount: 0.00001, + satoshis: 1000, + height: 546083, + confirmations: 3490 + } +] + +const mockUnconfirmed = [ + { + address: "1EzdL6TBbkNhnB2fYiBaKmcs5fxaoqwdAp", + txid: "000c00a90fb5031da6e02f7625df2f8b35a4c16e6feb9fc72293e67e5ff75786", + vout: 0, + scriptPubKey: "76a914997fabcd94a1e2aaa13a7664362e5e7b96c169a988ac", + amount: 0.00999626, + satoshis: 999626, + confirmations: 0, + ts: 1537989425 + } +] + +const mockTransactions = { + pagesTotal: 1, + txs: [ + { + txid: "000c00a90fb5031da6e02f7625df2f8b35a4c16e6feb9fc72293e67e5ff75786", + version: 2, + locktime: 549565, + vin: [ + { + txid: + "45c891c6d44619fc85716ba1b593aa83ebc1e500fe611b1ab98531ea203a0f21", + vout: 209, + sequence: 4294967294, + n: 0, + scriptSig: { + hex: + "47304402205b136f348bedae61a87c0500979d47ab02c76f0e4aba866b94c4931fccb5d7dc0220757a141660475b0cd4cb8b1f54b1ace95105f5bea775e817b115fb589fe4477541210246daec651c506f353daa7468714399c773df42ac4d663022d0e446a3331ac64a", + asm: + "304402205b136f348bedae61a87c0500979d47ab02c76f0e4aba866b94c4931fccb5d7dc0220757a141660475b0cd4cb8b1f54b1ace95105f5bea775e817b115fb589fe44775[ALL|FORKID] 0246daec651c506f353daa7468714399c773df42ac4d663022d0e446a3331ac64a" + }, + addr: "14TdovsQUL69xL5g3zSzhLpmDb93d9rU9m", + valueSat: 1979534, + value: 0.01979534, + doubleSpentTxID: null + }, + { + txid: + "6e5fa79547112a912adf6082c975dc80e282045193512ea369adb6bac7420674", + vout: 147, + sequence: 4294967294, + n: 1, + scriptSig: { + hex: + "473044022056674ac83a37d54ffec2c6d33cfc2ae2256f821b8bd818e1b6783356b49f3d28022046d3dd147c8fc51f2da5d16996a3cea1d7314e327aa1ae1017418737ed20136a4121038f23af565f68d8455e07e5c33c5cfc5114de2f478f035db23eb02f8ff5fe7f6e", + asm: + "3044022056674ac83a37d54ffec2c6d33cfc2ae2256f821b8bd818e1b6783356b49f3d28022046d3dd147c8fc51f2da5d16996a3cea1d7314e327aa1ae1017418737ed20136a[ALL|FORKID] 038f23af565f68d8455e07e5c33c5cfc5114de2f478f035db23eb02f8ff5fe7f6e" + }, + addr: "1CEtC2fjELvmAzTd6MWuHb1gvuSq8x3Xpb", + valueSat: 10466, + value: 0.00010466, + doubleSpentTxID: null + } + ], + vout: [ + { + value: "0.00999626", + n: 0, + scriptPubKey: { + hex: "76a914997fabcd94a1e2aaa13a7664362e5e7b96c169a988ac", + asm: + "OP_DUP OP_HASH160 997fabcd94a1e2aaa13a7664362e5e7b96c169a9 OP_EQUALVERIFY OP_CHECKSIG", + addresses: ["1EzdL6TBbkNhnB2fYiBaKmcs5fxaoqwdAp"], + type: "pubkeyhash" + }, + spentTxId: null, + spentIndex: null, + spentHeight: null + }, + { + value: "0.00990000", + n: 1, + scriptPubKey: { + hex: "76a914e3335e4d6babe61ea58311293c1c6bfe802801cb88ac", + asm: + "OP_DUP OP_HASH160 e3335e4d6babe61ea58311293c1c6bfe802801cb OP_EQUALVERIFY OP_CHECKSIG", + addresses: ["1MiKvgaQTFCTEZbSDMZgw9ahaB52Cr4ofb"], + type: "pubkeyhash" + }, + spentTxId: null, + spentIndex: null, + spentHeight: null + } + ], + blockhash: + "00000000000000000158b1a2883688873ec5ea076e3b0f576bcdfe1fd277880f", + blockheight: 549601, + confirmations: 5, + time: 1537990026, + blocktime: 1537990026, + valueOut: 0.01989626, + size: 372, + valueIn: 0.0199, + fees: 0.00000374 + } + ] +} + +module.exports = { + mockAddressDetails, + mockUtxoDetails, + mockUnconfirmed, + mockTransactions +} diff --git a/test/v3/mocks/block-mock.js b/test/v3/mocks/block-mock.js new file mode 100644 index 0000000..069b90d --- /dev/null +++ b/test/v3/mocks/block-mock.js @@ -0,0 +1,42 @@ +/* + This library contains mocking data for running unit tests on the address route. +*/ + +"use strict" + +const mockBlockDetails = { + hash: "00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79", + size: 1319, + height: 1267544, + version: 536870912, + merkleroot: + "c2cd01ec2cf149acc7631385f89ae103d1a2ad212ab810c862d9680a811618ce", + tx: [ + "52bfa89d449fef8070ec33e7a61f5ec8b5417ff62b050cd6a144ebce9557e0fa", + "8988e8742d2523f667c2d1374861919e3a82903e059fec75c10795da2303b93b", + "41def004f22b196d675566a6983ecf97611e72c48375b8ba779983d5ccd369d7", + "8ca8efb06abb94395f8331f87e101504dcf66aee828f2111922f056971fa7502", + "9ab409cbb203be7cf22bd6598d787fcdd5b417d885b72207ecc7253470770649" + ], + time: 1542048973, + nonce: 3936284753, + bits: "1a03c148", + difficulty: 4467892.99177529, + chainwork: "00000000000000000000000000000000000000000000003ee27b503a064045bd", + confirmations: 3, + previousblockhash: + "00000000000001891202cbe18729a02a8763cf04da0ca7aded6e6c7d9b500785", + nextblockhash: + "000000000000039b93b3f3403a0eb21150904b15229d998cf3788fd8bf192cc2", + reward: 0.78125, + isMainChain: true, + poolInfo: {} +} + +const mockBlockHash = + "00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79" + +module.exports = { + mockBlockDetails, + mockBlockHash +} diff --git a/test/v3/mocks/blockchain-mock.js b/test/v3/mocks/blockchain-mock.js new file mode 100644 index 0000000..6025583 --- /dev/null +++ b/test/v3/mocks/blockchain-mock.js @@ -0,0 +1,271 @@ +/* + This library contains mocking data for running unit tests on the address route. +*/ + +"use strict" + +const mockBlockHash = + "00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79" + +const mockBlockchainInfo = { + chain: "test", + blocks: 1267694, + headers: 1267694, + bestblockhash: + "000000000000013eaf80d1e157e32804c36a58ad0bb26ca59833880c80298780", + difficulty: 4105763.969035785, + mediantime: 1542131305, + verificationprogress: 0.9999968911571566, + chainwork: "00000000000000000000000000000000000000000000003f0443b0b5f02ce255", + pruned: false, + softforks: [ + { id: "bip34", version: 2, reject: { status: true } }, + { id: "bip66", version: 3, reject: { status: true } }, + { id: "bip65", version: 4, reject: { status: true } } + ], + bip9_softforks: { + csv: { + status: "active", + startTime: 1456790400, + timeout: 1493596800, + since: 770112 + } + } +} + +const mockChainTips = [ + { + height: 1267696, + hash: "000000000000035bfc43a642ebbff8cfc1e88b1d564de8b0a6c7e2797eeafb21", + branchlen: 0, + status: "active" + }, + { + height: 1267581, + hash: "000000000001bd12e4124207682563135b21353ca3087bc6b0c409f7f9e1da91", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1267375, + hash: "00000000702036979df70236bcc45dfc72d43d5d0e6834007afa1fa627e49587", + branchlen: 1036, + status: "headers-only" + }, + { + height: 1266979, + hash: "00000000e851abbde2174ccdc5c2508b909f81f23c4b7abeac864eee1a4891c7", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1266973, + hash: "000000007e1324d2900ed70947f89ab8fd4a267e87c9244c333215659a7370d5", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1266967, + hash: "000000000f37f12843d4800f50ec4de44dce0432e4d448242d43ff04a7a2948d", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1266963, + hash: "00000000ae11ec99e88898cbd72567cb4cb79e7fc13243357bef79632769deb4", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1266962, + hash: "000000004f97851259b4460a049e44fa5bd4beadee04a85bf81bd2e90c4f24f9", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1266950, + hash: "00000000d12a0e4a827f73ecaadb563df7e99817294939f2be8e943079dc60b2", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1266944, + hash: "00000000612d16af273217818e8e7ac5014a19f68ed04e1bc897b1be0a9c744f", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1266941, + hash: "0000000000002b91cdb15cacf3368da2c9ffce511d64092193b507dad75f4a27", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1266896, + hash: "000000000002745f67a66617b1f9f65c2dfb4c4b8a9ed9b1320b98d3ca46cbe5", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1266481, + hash: "0000000000036babee3ba7654cddb29a9fe5e85c9fbeb44bb53d5ffb694b9670", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1266452, + hash: "00000000502c3ab1490e0839780a4b441bbe21507c454545941e6adcde73c2ff", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1266336, + hash: "000000001c9a0b7cd9eb2210f69d5a9a3f4546ba3707deb40993d729190582d7", + branchlen: 10, + status: "headers-only" + }, + { + height: 1266097, + hash: "000000000028ac0fbd18afe6f001f70cb7549bd2b3082d754dde88ee236368a2", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1265522, + hash: "00000000000000c6df7a2063030e6ce51399e470cc46e4bf6d67aa3f0feca98d", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1265470, + hash: "0000000000168e6442a595a02340fccf8dc2b0e8912c632d39e1f870685f6d7c", + branchlen: 2, + status: "valid-fork" + }, + { + height: 1265275, + hash: "000000008bc2b37ae6af1b886b52e7e1c1122c12badc4ae6c13df64f789267cf", + branchlen: 11027, + status: "headers-only" + }, + { + height: 1265257, + hash: "00000000005bc3e9973d7273211eab02ae02549c7ebae48764c0bd648d4d7bbe", + branchlen: 114, + status: "valid-fork" + }, + { + height: 1265254, + hash: "0000000000e5b909d3541857315e9ee103b042d9ba661f25cc3e8fb25a96c8ce", + branchlen: 111, + status: "valid-fork" + }, + { + height: 1265245, + hash: "000000000066eb8832f6f990baceb379536f437216e22475a6c1b9133d250cb8", + branchlen: 102, + status: "valid-fork" + }, + { + height: 1264455, + hash: "00000000000001befc1bb17a23208d0b77b2449e37093ce8fbb5e346b19cafcc", + branchlen: 1, + status: "valid-headers" + }, + { + height: 1264373, + hash: "00000000000001ed3747e04fb95054cac0358fb4d8009d1999fc3ea34413e84a", + branchlen: 175, + status: "valid-fork" + }, + { + height: 1263912, + hash: "00000000000004635aecbcb97edcd6f5396483338838cb40da804eb354c68bde", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1262906, + hash: "00000000b31643d92a3e7c38e9755844fc8607e3f1055580a8ff28854e8a8ece", + branchlen: 1, + status: "valid-fork" + }, + { + height: 1255749, + hash: "000000001dde5b015a99137f4cba87871370f4b6fcfbcc8b126547e6c33177da", + branchlen: 129, + status: "headers-only" + }, + { + height: 1188789, + hash: "00000000af942ce4eb60b3213cbcb7c98a7330f1f8f1adb4b6376f5e822e15b2", + branchlen: 92, + status: "headers-only" + } +] + +const mockMempoolInfo = { + size: 87, + bytes: 16816, + usage: 66408, + maxmempool: 300000000, + mempoolminfee: 0 +} + +const mockRawMempool = [ + "db045bc3bd1088fa91f5ebb05c35cb9e2a91a22377f79b465cc6920b9893123c", + "6b3df7febf2b9834f1409155f88b866dd516b36376eae00e2b455df82e290405" +] + +const mockBlockHeaderConcise = + "0000ff7f7d217c9b7845ea8b50d620c59a1bf7c276566406e9b7bc7e463e0000000000006d70322c0b697c1c81d2744f87f09f1e9780ba5d30338952e2cdc64e60456f8423bb0a5ceafa091a3e843526" + +const mockBlockHeader = { + hash: "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900", + confirmations: 7, + height: 1272859, + version: 2147418112, + versionHex: "7fff0000", + merkleroot: + "846f45604ec6cde2528933305dba80971e9ff0874f74d2811c7c690b2c32706d", + time: 1544207139, + mediantime: 1544202884, + nonce: 641041470, + bits: "1a09faea", + difficulty: 1681035.704111868, + chainwork: "00000000000000000000000000000000000000000000003fc4752e608403be04", + previousblockhash: + "0000000000003e467ebcb7e906645676c2f71b9ac520d6508bea45789b7c217d", + nextblockhash: + "00000000000006899041cdfd6c0b73a97730c362346dde479b77414ad7f25ace" +} + +const mockTxOut = { + bestblock: "00000000003a7f19730dd9f172d7466658a5c8833dd03ed8f4ba36e3d857b5e7", + confirmations: 10, + value: 0.0001, + scriptPubKey: { + asm: + "OP_DUP OP_HASH160 0ee020c07f39526ac5505c54fa1ab98490979b83 OP_EQUALVERIFY OP_CHECKSIG", + hex: "76a9140ee020c07f39526ac5505c54fa1ab98490979b8388ac", + reqSigs: 1, + type: "pubkeyhash", + addresses: ["bchtest:qq8wqgxq0uu4y6k92pw9f7s6hxzfp9umsvtg39pzqf"] + }, + coinbase: false +} + +const mockTxOutProof = + "000000200798039affceb7a381e15dc62443c286efe7cae852393b656807000000000000cf9a7d6c654fd1c4558229a619a550ff749b373fa0f0027eed68b1abf6175d5c07c50f5cffff001d0452403b34000000070d0765da76980d542b5670d27ccda9e717073270f8921ad017deb7ee83fe6c4c22830a48cc8392197f1742a3d81805bd2aa4eb7469f38c74f674429d682cb87efcff1713fd0c388a50f0d11adf05363397ef79d16cdb786d47c941c2cb89f2588f42aced9c05f4203b4440616a9e4266ec47909b9580a643f402b5d4e82cc3cddd73df05a33abf6af3975d973d3033d10c4168a73d58d01f685a96d5a2519cd0de9c77faf3f8725ddf155cbdc8ab8102f173e2d0a0d74767f3bff22f588158d6d5bc71c079c7659cc864819c43877635007650502eb4060fcc9feec3280a41d602ad0a" + +module.exports = { + mockBlockHash, + mockBlockchainInfo, + mockChainTips, + mockMempoolInfo, + mockRawMempool, + mockBlockHeaderConcise, + mockBlockHeader, + mockTxOut, + mockTxOutProof +} diff --git a/test/v3/mocks/control-mock.js b/test/v3/mocks/control-mock.js new file mode 100644 index 0000000..cca9338 --- /dev/null +++ b/test/v3/mocks/control-mock.js @@ -0,0 +1,27 @@ +/* + This library contains mocking data for running unit tests on the address route. +*/ + +"use strict" + +const mockGetInfo = { + version: 170200, + protocolversion: 70015, + walletversion: 160300, + balance: 0, + blocks: 1266726, + timeoffset: 0, + connections: 8, + proxy: "", + difficulty: 1, + testnet: true, + keypoololdest: 1536331195, + keypoolsize: 2000, + paytxfee: 0, + relayfee: 0.00001, + errors: "Warning: unknown new rules activated (versionbit 28)" +} + +module.exports = { + mockGetInfo +} diff --git a/test/v3/mocks/express-mocks.js b/test/v3/mocks/express-mocks.js new file mode 100644 index 0000000..fcb545f --- /dev/null +++ b/test/v3/mocks/express-mocks.js @@ -0,0 +1,88 @@ +/* + Contains mocks of Express req and res objects. +*/ + +"use strict" + +const sinon = require("sinon") + +// Inspect JS Objects. +const util = require("util") +util.inspect.defaultOptions = { + showHidden: true, + colors: true +} + +// mock for res.send() +function fakeSend(arg) { + //console.log(`res.send: ${util.inspect(arg)}`); + mockRes.output = arg + return arg +} + +// mock for res.json() +function fakeJson(arg) { + //console.log(`res.json: ${util.inspect(arg)}`); + mockRes.output = arg + return arg +} + +// mock for res.setStatus(num) +const setStatusCode = arg => { + mockRes.statusCode = arg +} + +const mockReq = { + accepts: sinon.stub().returns({}), + acceptsCharsets: sinon.stub().returns({}), + acceptsEncodings: sinon.stub().returns({}), + acceptsLanguages: sinon.stub().returns({}), + body: {}, + flash: sinon.stub().returns({}), + get: sinon.stub().returns({}), + is: sinon.stub().returns({}), + params: {}, + query: {}, + session: {}, + locals: {}, + headers: {} +} + +const mockRes = { + append: sinon.stub().returns({}), + attachement: sinon.stub().returns({}), + clearCookie: sinon.stub().returns({}), + cookie: sinon.stub().returns({}), + download: sinon.stub().returns({}), + end: sinon.stub().returns({}), + format: {}, + get: sinon.stub().returns({}), + headersSent: sinon.stub().returns({}), + json: sinon.stub().callsFake(fakeJson), + jsonp: sinon.stub().returns({}), + links: sinon.stub().returns({}), + locals: {}, + location: sinon.stub().returns({}), + output: null, // Used for retrieving output data. + redirect: sinon.stub().returns({}), + render: sinon.stub().returns({}), + send: sinon.stub().callsFake(fakeSend), + sendFile: sinon.stub().returns({}), + sendStatus: sinon.stub().returns({}), + set: sinon.stub().returns({}), + status: sinon.stub().callsFake(setStatusCode), + statusCode: null, // Default value before calling stats(); + type: sinon.stub().returns({}), + vary: sinon.stub().returns({}), + write: sinon.stub().returns({}), + setHeader: sinon.stub().returns({}), + format: sinon.stub().returns({}) +} + +const mockNext = sinon.stub().returns() + +module.exports = { + mockReq, + mockRes, + mockNext +} diff --git a/test/v3/mocks/mining-mocks.js b/test/v3/mocks/mining-mocks.js new file mode 100644 index 0000000..8ef8ada --- /dev/null +++ b/test/v3/mocks/mining-mocks.js @@ -0,0 +1,22 @@ +/* + This library contains mocking data for running unit tests on the address route. +*/ + +"use strict" + +const mockMiningInfo = { + blocks: 1270185, + currentblocksize: 0, + currentblocktx: 0, + difficulty: 1, + blockprioritypercentage: 5, + errors: + "Warning: Unknown block versions being mined! It's possible unknown rules are in effect", + networkhashps: 517410290.9365583, + pooledtx: 5, + chain: "test" +} + +module.exports = { + mockMiningInfo +} diff --git a/test/v3/mocks/raw-transactions-mocks.js b/test/v3/mocks/raw-transactions-mocks.js new file mode 100644 index 0000000..08620a3 --- /dev/null +++ b/test/v3/mocks/raw-transactions-mocks.js @@ -0,0 +1,157 @@ +/* + This library contains mocking data for running unit tests. +*/ + +"use strict" + +const mockDecodeRawTransaction = { + txid: "a332237d82a2543af1b0e1ae3c8cea1610c290ebcaf084a7e9894a61de0be988", + hash: "a332237d82a2543af1b0e1ae3c8cea1610c290ebcaf084a7e9894a61de0be988", + size: 226, + version: 2, + locktime: 0, + vin: [ + { + txid: "21cced645eab150585ed7ca7c96edebab5793cc0a3b3b286c42fd7d6d798b5b9", + vout: 1, + scriptSig: { + asm: + "3045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd41390[ALL|FORKID] 0360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413d", + hex: + "483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413d" + }, + sequence: 4294967295 + } + ], + vout: [ + { + value: 0.0001, + n: 0, + scriptPubKey: { + asm: + "OP_DUP OP_HASH160 eb4b180def88e3f5625b2d8ae2c098ff7d85f664 OP_EQUALVERIFY OP_CHECKSIG", + hex: "76a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac", + reqSigs: 1, + type: "pubkeyhash", + addresses: [Array] + } + }, + { + value: 0.09989752, + n: 1, + scriptPubKey: { + asm: + "OP_DUP OP_HASH160 eb4b180def88e3f5625b2d8ae2c098ff7d85f664 OP_EQUALVERIFY OP_CHECKSIG", + hex: "76a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac", + reqSigs: 1, + type: "pubkeyhash", + addresses: [Array] + } + } + ] +} + +const mockDecodeScript = { + asm: + "0 0 -57 OP_NOP6 OP_LSHIFT OP_UNKNOWN OP_UNKNOWN OP_UNKNOWN c486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa OP_2OVER OP_NUMEQUALVERIFY OP_OR OP_INVERT OP_UNKNOWN OP_UNKNOWN 2ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab 67c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b [error]", + type: "nonstandard", + p2sh: "bchtest:pzy6dwfy6yf373w0dr05a6flfqksurjhwcl3awhdvm" +} + +const mockRawTransactionConcise = + "02000000014e6b52500110b1c30315b85805fb274f0f4afceffc1589f889b27709e59e987d000000006a473044022052762770baa71c1a0b9544ad0f1ea343d32c22aa87c5f8397b6852f464c15b1e02201f4390745cb470e21e0e3c14229f39fef55ea0643a4c997f99d9f3501eae09b7412103c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792ffffffff02d1778f950a0000001976a91436d2f27bbd826a86db1e93618ce3de89ef33169388ac80969800000000001976a914152ea3cd65f18cb8fa9146c84ea1a97af8f051de88ac00000000" + +const mockRawTransactionVerbose = { + hex: + "02000000014e6b52500110b1c30315b85805fb274f0f4afceffc1589f889b27709e59e987d000000006a473044022052762770baa71c1a0b9544ad0f1ea343d32c22aa87c5f8397b6852f464c15b1e02201f4390745cb470e21e0e3c14229f39fef55ea0643a4c997f99d9f3501eae09b7412103c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792ffffffff02d1778f950a0000001976a91436d2f27bbd826a86db1e93618ce3de89ef33169388ac80969800000000001976a914152ea3cd65f18cb8fa9146c84ea1a97af8f051de88ac00000000", + txid: "bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971", + hash: "bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971", + size: 225, + version: 2, + locktime: 0, + vin: [ + { + txid: "7d989ee50977b289f88915fceffc4a0f4f27fb0558b81503c3b1100150526b4e", + vout: 0, + scriptSig: { + asm: + "3044022052762770baa71c1a0b9544ad0f1ea343d32c22aa87c5f8397b6852f464c15b1e02201f4390745cb470e21e0e3c14229f39fef55ea0643a4c997f99d9f3501eae09b7[ALL|FORKID] 03c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792", + hex: + "473044022052762770baa71c1a0b9544ad0f1ea343d32c22aa87c5f8397b6852f464c15b1e02201f4390745cb470e21e0e3c14229f39fef55ea0643a4c997f99d9f3501eae09b7412103c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792" + }, + sequence: 4294967295 + } + ], + vout: [ + { + value: 454.58880465, + n: 0, + scriptPubKey: { + asm: + "OP_DUP OP_HASH160 36d2f27bbd826a86db1e93618ce3de89ef331693 OP_EQUALVERIFY OP_CHECKSIG", + hex: "76a91436d2f27bbd826a86db1e93618ce3de89ef33169388ac", + reqSigs: 1, + type: "pubkeyhash", + addresses: ["bchtest:qqmd9unmhkpx4pkmr6fkrr8rm6y77vckjvqe8aey35"] + } + }, + { + value: 0.1, + n: 1, + scriptPubKey: { + asm: + "OP_DUP OP_HASH160 152ea3cd65f18cb8fa9146c84ea1a97af8f051de OP_EQUALVERIFY OP_CHECKSIG", + hex: "76a914152ea3cd65f18cb8fa9146c84ea1a97af8f051de88ac", + reqSigs: 1, + type: "pubkeyhash", + addresses: ["bchtest:qq2jag7dvhccew86j9rvsn4p49a03uz3mcpw3d6aca"] + } + } + ], + blockhash: "000000000000026fa244de975ca89ea08008aa566564ce2e8ebb3144361b601b", + confirmations: 125, + time: 1542646373, + blocktime: 1542646373 +} + +const mockWHDecode = { + txid: "f8a9857fe3b8a288b5fcafb1b0fc196731f433add6d962f77acd7c10b970ff89", + fee: "500", + sendingaddress: "bchtest:qzjtnzcvzxx7s0na88yrg3zl28wwvfp97538sgrrmr", + referenceaddress: "bchtest:qzjtnzcvzxx7s0na88yrg3zl28wwvfp97538sgrrmr", + ismine: false, + version: 0, + type_int: 0, + type: "Simple Send", + propertyid: 368, + precision: "8", + amount: "10.00000000", + valid: true, + blockhash: "0000000046ba0bcef78caaa4492622176bbc563cf249ab52340a0449bc8e26f6", + blocktime: 1542814183, + positioninblock: 57, + block: 1269008, + confirmations: 2 +} + +const mockWHCreateInput = { + txid: "f7ed9cf23dee85910f6269c9a101a75fcfd2f3c6fc81f17fad824ff7aaf99ab2", + vout: 1, + scriptPubKey: "76a914a4b98b0c118de83e7d39c834445f51dce62425f588ac", + amount: 0.09984138, + satoshis: 9984138, + height: 1269011, + confirmations: 4, + legacyAddress: "mvXwPH74hW2yVTWwDwzsjGoaUAqJvWk7ZJ", + cashAddress: "bchtest:qzjtnzcvzxx7s0na88yrg3zl28wwvfp97538sgrrmr", + value: 0.09984138 +} + +module.exports = { + mockDecodeRawTransaction, + mockDecodeScript, + mockRawTransactionConcise, + mockRawTransactionVerbose, + mockWHDecode, + mockWHCreateInput +} diff --git a/test/v3/mocks/slp-mocks.js b/test/v3/mocks/slp-mocks.js new file mode 100644 index 0000000..1d70df0 --- /dev/null +++ b/test/v3/mocks/slp-mocks.js @@ -0,0 +1,327 @@ +/* + This library contains mocking data for running unit tests. +*/ + +"use strict" + +const mockList = { + t: [ + { + tokenDetails: { + tokenIdHex: + "df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb", + documentUri: "", + documentSha256: "", + symbol: "NAKAMOTO", + name: "NAKAMOTO", + decimals: 8 + }, + tokenStats: { + qty_valid_txns_since_genesis: 241, + qty_valid_token_utxos: 151, + qty_valid_token_addresses: 113, + qty_token_circulating_supply: "20995990", + qty_token_burned: "4010", + qty_token_minted: "21000000", + qty_satoshis_locked_up: 81900 + } + } + ] +} + +const mockSingleToken = { + t: [ + { + tokenDetails: { + decimals: 8, + tokenIdHex: + "650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a", + timestamp: "2018-08-26 07:06", + transactionType: "GENESIS", + versionType: 1, + documentUri: "", + documentSha256Hex: null, + symbol: "", + name: "TESTYCOIN", + batonVout: null, + containsBaton: false, + genesisOrMintQuantity: "9999", + sendOutputs: null + }, + tokenStats: { + block_created: 1253802, + block_last_active_send: 1253802, + block_last_active_mint: null, + qty_valid_txns_since_genesis: 2, + qty_valid_token_utxos: 0, + qty_valid_token_addresses: 0, + qty_token_minted: "9999", + qty_token_burned: "9999", + qty_token_circulating_supply: "0", + qty_satoshis_locked_up: 0, + minting_baton_status: "NEVER_CREATED" + } + } + ] +} + +const mockSingleTokenError = { + t: [] +} + +const mockSingleAddress = { + a: [ + { + _id: "5c93ed62a19119333d1595bc", + tokenDetails: { + tokenIdHex: + "6b081fcd1f78b187be1464313dac8ff257251b727a42b613552a4040870aeb29" + }, + address: "slptest:pz0qcslrqn7hr44hsszwl4lw5r6udkg6zqv7sq3kk7", + satoshis_balance: 546, + token_balance: "4616984" + } + ], + t: [ + { + tokenDetails: { + decimals: 8, + tokenIdHex: + "6b081fcd1f78b187be1464313dac8ff257251b727a42b613552a4040870aeb29", + timestamp: "2019-02-25 10:05", + transactionType: "GENESIS", + versionType: 1, + documentUri: "https://developer.bitcoin.com", + documentSha256Hex: "", + symbol: "DEV", + name: "DEVCOIN", + batonVout: 2, + containsBaton: true, + genesisOrMintQuantity: "500000000", + sendOutputs: null + }, + tokenStats: { + block_created: 571272, + block_last_active_send: 574758, + block_last_active_mint: 571272, + qty_valid_txns_since_genesis: 172, + qty_valid_token_utxos: 170, + qty_valid_token_addresses: 170, + qty_token_minted: "1000000000", + qty_token_burned: "4.99999995", + qty_token_circulating_supply: "999999995.00000005", + qty_satoshis_locked_up: 92820, + minting_baton_status: "ALIVE" + } + } + ] +} + +const mockTx = { + txid: "57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b9457822d446", + version: 2, + locktime: 0, + vin: [ + { + txid: "61e71554a3dc18158f30d9e8f5c9b6641a789690b32302899f81cbea9fe3bb49", + vout: 2, + sequence: 4294967295, + n: 0, + scriptSig: { + hex: + "4730440220409e79fec552f01203f41d3d621ae3db89c720af261c8268ce5f0453de009f5d022001e7ffefeba7b0716d32ea55cb6ace267b6ee9cbcc8a017bb9c3b6acf7889418412103c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585", + asm: + "30440220409e79fec552f01203f41d3d621ae3db89c720af261c8268ce5f0453de009f5d022001e7ffefeba7b0716d32ea55cb6ace267b6ee9cbcc8a017bb9c3b6acf7889418[ALL|FORKID] 03c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585" + }, + value: 546, + legacyAddress: "mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL", + cashAddress: "bchtest:qz4qnxcxwvmacgye8wlakhz0835x0w3vtvaga95c09" + }, + { + txid: "61e71554a3dc18158f30d9e8f5c9b6641a789690b32302899f81cbea9fe3bb49", + vout: 1, + sequence: 4294967295, + n: 1, + scriptSig: { + hex: + "483045022100a743bee56c99bd103be48a78fa4c7342100815d9d2448dbe6e1d338c3a13b241022066728b5279fc22eef5cd019582ff34771e29175835fc98aa3168a1548fd78ac8412103c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585", + asm: + "3045022100a743bee56c99bd103be48a78fa4c7342100815d9d2448dbe6e1d338c3a13b241022066728b5279fc22eef5cd019582ff34771e29175835fc98aa3168a1548fd78ac8[ALL|FORKID] 03c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585" + }, + value: 546, + legacyAddress: "mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL", + cashAddress: "bchtest:qz4qnxcxwvmacgye8wlakhz0835x0w3vtvaga95c09" + }, + { + txid: "61e71554a3dc18158f30d9e8f5c9b6641a789690b32302899f81cbea9fe3bb49", + vout: 3, + sequence: 4294967295, + n: 2, + scriptSig: { + hex: + "483045022100821473902eec5f1ce7d43b1ba7f9ec453bfe8b8dfc3de3e0723c883ab109922f02206162960e80618531fab2c16aee260fddd7979bab62471c8686af7de75f8732ec412103c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585", + asm: + "3045022100821473902eec5f1ce7d43b1ba7f9ec453bfe8b8dfc3de3e0723c883ab109922f02206162960e80618531fab2c16aee260fddd7979bab62471c8686af7de75f8732ec[ALL|FORKID] 03c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585" + }, + value: 9997521, + legacyAddress: "mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL", + cashAddress: "bchtest:qz4qnxcxwvmacgye8wlakhz0835x0w3vtvaga95c09" + } + ], + vout: [ + { + value: "0.00000000", + n: 0, + scriptPubKey: { + hex: + "6a04534c500001010453454e44207ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796080000000049504f80080000001c71e64280", + asm: + "OP_RETURN 5262419 1 1145980243 7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796 0000000049504f80 0000001c71e64280" + }, + spentTxId: null, + spentIndex: null, + spentHeight: null + }, + { + value: "0.00000546", + n: 1, + scriptPubKey: { + hex: "76a914396b8e57ad0cb58d30e2992f22047b3c20377aa688ac", + asm: + "OP_DUP OP_HASH160 396b8e57ad0cb58d30e2992f22047b3c20377aa6 OP_EQUALVERIFY OP_CHECKSIG", + addresses: ["mkkZf7T3fU3vHSzNPy51HBmM46ghN1gnN9"], + type: "pubkeyhash" + }, + spentTxId: null, + spentIndex: null, + spentHeight: null + }, + { + value: "0.00000546", + n: 2, + scriptPubKey: { + hex: "76a914aa099b067337dc20993bbfdb5c4f3c6867ba2c5b88ac", + asm: + "OP_DUP OP_HASH160 aa099b067337dc20993bbfdb5c4f3c6867ba2c5b OP_EQUALVERIFY OP_CHECKSIG", + addresses: ["mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL"], + type: "pubkeyhash" + }, + spentTxId: null, + spentIndex: null, + spentHeight: null + }, + { + value: "0.09996891", + n: 3, + scriptPubKey: { + hex: "76a914aa099b067337dc20993bbfdb5c4f3c6867ba2c5b88ac", + asm: + "OP_DUP OP_HASH160 aa099b067337dc20993bbfdb5c4f3c6867ba2c5b OP_EQUALVERIFY OP_CHECKSIG", + addresses: ["mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL"], + type: "pubkeyhash" + }, + spentTxId: null, + spentIndex: null, + spentHeight: null + } + ], + blockhash: "000000000000ce978accc64a6bb567acf0c653c202309a0f8e220149bf0c6968", + blockheight: 1287490, + confirmations: 746, + time: 1550855104, + blocktime: 1550855104, + valueOut: 0.09997983, + size: 628, + valueIn: 0.09998613, + fees: 0.0000063, + tokenInfo: { + versionType: 1, + transactionType: "SEND", + tokenIdHex: + "7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796", + sendOutputs: ["0", "1230000000", "122170000000"] + }, + tokenIsValid: true +} + +const mockConvert = { + slpAddress: "slptest:qz35h5mfa8w2pqma2jq06lp7dnv5fxkp2shlcycvd5", + cashAddress: "bchtest:qz35h5mfa8w2pqma2jq06lp7dnv5fxkp2svtllzmlf", + legacyAddress: "mvQPGnzRT6gMWASZBMg7NcT3vmvsSKSQtf" +} + +const mockTokenDetails = { + tokenIdHex: + "df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb", + documentUri: "", + symbol: "NAKAMOTO", + name: "NAKAMOTO", + decimals: 8, + timestamp: "", + containsBaton: true, + versionType: 1 +} + +const mockTokenStats = { + qty_valid_txns_since_genesis: 241, + qty_valid_token_utxos: 151, + qty_valid_token_addresses: 113, + qty_token_circulating_supply: "20995990", + qty_token_burned: "4010", + qty_token_minted: "21000000", + qty_satoshis_locked_up: 81900 +} + +const mockBalance = { + slpAddress: "simpleledger:qp9d8mn8ypryfvea2mev0ggc3wg6plpn4suuaeuss3", + satoshis_balance: 546, + token_balance: "1000" +} + +const mockTransactions = [ + { + txid: "a302f045be8efa1cd982833a7f187ff4fac8baac36da0c887eb2787d8b45e2af", + tokenDetails: { + valid: true, + detail: { + decimals: null, + tokenIdHex: + "495322b37d6b2eae81f045eda612b95870a0c2b6069c58f70cf8ef4e6a9fd43a", + timestamp: null, + transactionType: "MINT", + versionType: 1, + documentUri: null, + documentSha256Hex: null, + symbol: null, + name: null, + batonVout: 2, + containsBaton: true, + genesisOrMintQuantity: { + $numberDecimal: "1000" + }, + sendOutputs: null + }, + invalidReason: null, + schema_version: 30 + } + } +] + +const mockFoobar = { + c: [], + u: [] +} + +module.exports = { + mockList, + mockSingleToken, + mockConvert, + mockTokenDetails, + mockTokenStats, + mockTx, + mockBalance, + mockTransactions, + mockSingleTokenError, + mockSingleAddress, + mockFoobar +} diff --git a/test/v3/mocks/slpjs-mocks.js b/test/v3/mocks/slpjs-mocks.js new file mode 100644 index 0000000..c130935 --- /dev/null +++ b/test/v3/mocks/slpjs-mocks.js @@ -0,0 +1,71 @@ +/* + Mocks used for unit tests that interact with slpjs. +*/ + +"use strict" + +const sinon = require("sinon") +const proxyquire = require("proxyquire") +const BigNumber = require("bignumber.js") +const slpMocks = require("./slp-mocks") + +// Mock the BitboxNetwork class. +class BitboxNetwork { + constructor() {} + + async getAllSlpBalancesAndUtxos(address) { + return { + satoshis_available_bch: 9996891, + satoshis_in_slp_baton: 546, + satoshis_in_slp_token: 546, + satoshis_in_invalid_token_dag: 0, + satoshis_in_invalid_baton_dag: 0, + slpTokenBalances: { + "7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796": new BigNumber( + 123400000000 + ) + }, + slpTokenUtxos: { + "7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796": [] + }, + slpBatonUtxos: { + "7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796": [] + }, + nonSlpUtxos: [{}], + invalidTokenUtxos: [], + invalidBatonUtxos: [] + } + } + + async getTokenInformation(txid) { + BigNumber.set({ DECIMAL_PLACES: 8, ROUNDING_MODE: 4 }) + + const obj = { + versionType: 1, + transactionType: 0, + symbol: "SLPSDK", + name: "SLP SDK example using BITBOX", + documentUri: "developer.bitcoin.com", + documentSha256: null, + decimals: 8, + batonVout: 2, + containsBaton: true, + genesisOrMintQuantity: new BigNumber(123400000000) + } + + return obj + } + + async getTransactionDetails(txid) { + return slpMocks.mockTx + } +} + +// Mock the slpjs library. +const slpjs = { + BitboxNetwork, + slp: {}, + validator: {} +} + +module.exports = slpjs diff --git a/test/v3/mocks/transaction-mocks.js b/test/v3/mocks/transaction-mocks.js new file mode 100644 index 0000000..a8d22e2 --- /dev/null +++ b/test/v3/mocks/transaction-mocks.js @@ -0,0 +1,74 @@ +/* + This library contains mocking data for running unit tests on the address route. +*/ + +"use strict" + +/* +const mockDetails = { + txid: "6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40", + version: 2, + locktime: 0, + vin: [Array], + vout: [Array], + blockhash: "00000000e7232ff12462dedf9c11985f5b54202515277c337ccc59812758f28b", + blockheight: 1270188, + confirmations: 2, + time: 1543436253, + blocktime: 1543436253, + valueOut: 450.78867333, + size: 226, + valueIn: 450.78867559, + fees: 0.00000226 +} +*/ + +const mockDetails = { + txid: "6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40", + version: 2, + locktime: 0, + vin: [ + { + txid: "273d616d1c48f4b075c497f36ffdc79da5c8d6ed75485808b3599aac504f8525", + vout: 0, + sequence: 4294967295, + n: 0, + scriptSig: [{}], + addr: "bchtest:qqmd9unmhkpx4pkmr6fkrr8rm6y77vckjvqe8aey35", + valueSat: 45078867559, + value: 450.78867559, + doubleSpentTxID: null + } + ], + vout: [ + { + value: "450.68867333", + n: 0, + scriptPubKey: [{}], + spentTxId: null, + spentIndex: null, + spentHeight: null + }, + { + value: "0.10000000", + n: 1, + scriptPubKey: [{}], + spentTxId: null, + spentIndex: null, + spentHeight: null + } + ], + blockhash: "00000000e7232ff12462dedf9c11985f5b54202515277c337ccc59812758f28b", + blockheight: 1270188, + confirmations: 3, + time: 1543436253, + blocktime: 1543436253, + valueOut: 450.78867333, + size: 226, + valueIn: 450.78867559, + fees: 0.00000226 +} + +module.exports = { + mockDetails +} diff --git a/test/v3/mocks/util-mocks.js b/test/v3/mocks/util-mocks.js new file mode 100644 index 0000000..4118358 --- /dev/null +++ b/test/v3/mocks/util-mocks.js @@ -0,0 +1,18 @@ +/* + This library contains mocking data for running unit tests on the address route. +*/ + +"use strict" + +const mockAddress = { + isvalid: true, + address: "bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y", + scriptPubKey: "76a914016a935f87e8deeab04501889b22d87e4e52db0988ac", + ismine: false, + iswatchonly: false, + isscript: false +} + +module.exports = { + mockAddress +} diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js new file mode 100644 index 0000000..56df9db --- /dev/null +++ b/test/v3/rate-limits.js @@ -0,0 +1,166 @@ +"use strict" + +const chai = require("chai") +const assert = chai.assert +const nock = require("nock") // HTTP mocking + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +// Mocking data. +const { mockReq, mockRes, mockNext } = require("./mocks/express-mocks") + +// Libraries under test +const rateLimitMiddleware = require("../../src/middleware/route-ratelimit") +const controlRoute = require("../../src/routes/v3/control") + +let req, res, next +let originalEnvVars // Used during transition from integration to unit tests. + +describe("#route-ratelimits", () => { + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + next = mockNext + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + }) + + describe("#routeRateLimit", () => { + const routeRateLimit = rateLimitMiddleware.routeRateLimit + const getInfo = controlRoute.testableComponents.getInfo + /* + it("should pass through rate-limit middleware", async () => { + req.baseUrl = "/v2" + req.path = "/control/getInfo" + req.method = "GET" + + await routeRateLimit(req, res, next) + + // next() will be called if rate-limit is not triggered + assert.equal(next.called, true) + }) + + it("should trigger rate-limit handler if rate limits exceeds 60 request per minute", async () => { + req.baseUrl = "/v2" + req.path = "/control/getInfo" + req.method = "GET" + + for (let i = 0; i < 65; i++) { + next.reset() // reset the stubbed next() function. + + await routeRateLimit(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + false, + `next should not be called if rate limit was triggered.` + ) + }) + */ + /* + it("should NOT trigger rate-limit handler for pro-tier at 65 RPM", async () => { + // Clear the require cache before running this test. + delete require.cache[ + require.resolve("../../dist/middleware/route-ratelimit") + ] + rateLimitMiddleware = require("../../dist/middleware/route-ratelimit") + routeRateLimit = rateLimitMiddleware.routeRateLimit + + req.baseUrl = "/v2" + req.path = "/control/getInfo" + req.method = "GET" + + req.locals.proLimit = true + + //console.log(`req.locals before test: ${util.inspect(req.locals)}`) + + // Prepare the authorization header + //req.headers.authorization = generateAuthHeader("BITBOX") + + for (let i = 0; i < 65; i++) { + next.reset() // reset the stubbed next() function. + + await routeRateLimit(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + //console.log(`req.locals after test: ${util.inspect(req.locals)}`) + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + true, + `next should be called if rate limit was not triggered.` + ) + }) + + it("rate-limiting should still kick in at a higher RPM for pro-tier", async () => { + // Clear the require cache before running this test. + delete require.cache[ + require.resolve("../../dist/middleware/route-ratelimit") + ] + rateLimitMiddleware = require("../../dist/middleware/route-ratelimit") + routeRateLimit = rateLimitMiddleware.routeRateLimit + + req.baseUrl = "/v2" + req.path = "/control/getInfo" + req.method = "GET" + + req.locals.proLimit = true + + //console.log(`req.locals before test: ${util.inspect(req.locals)}`) + + // Prepare the authorization header + //req.headers.authorization = generateAuthHeader("BITBOX") + + for (let i = 0; i < 650; i++) { + next.reset() // reset the stubbed next() function. + + await routeRateLimit(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + //console.log(`req.locals after test: ${util.inspect(req.locals)}`) + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + false, + `next should NOT be called if rate limit was triggered.` + ) + }) + */ + }) +}) + +// Generates a Basic authorization header. +function generateAuthHeader(pass) { + // https://en.wikipedia.org/wiki/Basic_access_authentication + const username = "BITBOX" + const combined = `${username}:${pass}` + + var base64Credential = Buffer.from(combined).toString("base64") + var readyCredential = `Basic ${base64Credential}` + + return readyCredential +} diff --git a/test/v3/raw-transactions.js b/test/v3/raw-transactions.js new file mode 100644 index 0000000..35f3467 --- /dev/null +++ b/test/v3/raw-transactions.js @@ -0,0 +1,807 @@ +/* + TESTS FOR THE RAWTRANSACTIONS.TS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. + + TODO: + -Create e2e test for sendRawTransaction. + +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const rawtransactions = require("../../src/routes/v3/rawtransactions") +const nock = require("nock") // HTTP mocking + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +//delete require.cache[require.resolve("./mocks/express-mocks")] // Fixes bug +const { mockReq, mockRes, mockNext } = require("./mocks/express-mocks") +const mockData = require("./mocks/raw-transactions-mocks") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 5 } + +describe("#Raw-Transactions", () => { + let req, res, next + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + next = mockNext + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + req.locals = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", async () => { + // root route handler. + const root = rawtransactions.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result.status, "rawtransactions", "Returns static string") + }) + }) + + describe("decodeRawTransactionSingle()", () => { + // block route handler. + const decodeRawTransaction = + rawtransactions.testableComponents.decodeRawTransactionSingle + + it("should throw error if hex is missing", async () => { + const result = await decodeRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "hex can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.hex = + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + + const result = await decodeRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should GET /decodeRawTransaction", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockDecodeRawTransaction }) + } + + req.params.hex = + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + + const result = await decodeRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "txid", + "hash", + "size", + "version", + "locktime", + "vin", + "vout" + ]) + assert.isArray(result.vin) + assert.isArray(result.vout) + }) + }) + + describe("decodeRawTransactionBulk()", () => { + const decodeRawTransactionBulk = + rawtransactions.testableComponents.decodeRawTransactionBulk + + it("should throw 400 error if hexes array is missing", async () => { + const result = await decodeRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "hexes must be an array") + }) + + it("should throw 400 error if hexes array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.hexes = testArray + + const result = await decodeRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw 400 error if hexes is empty", async () => { + req.body.hexes = [""] + + const result = await decodeRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Encountered empty hex") + }) + + it("should error on non-array single hex", async () => { + req.body.hexes = + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + + const result = await decodeRawTransactionBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hexes must be an array", + "Proper error message" + ) + }) + + it("should decode an array with a single hex", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockDecodeRawTransaction }) + } + + req.body.hexes = [ + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + ] + + const result = await decodeRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "txid", + "hash", + "size", + "version", + "locktime", + "vin", + "vout" + ]) + assert.isArray(result[0].vin) + assert.isArray(result[0].vout) + }) + + it("should decode an array with multiple hexes", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: mockData.mockDecodeRawTransaction }) + } + + req.body.hexes = [ + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000", + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + ] + + const result = await decodeRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "txid", + "hash", + "size", + "version", + "locktime", + "vin", + "vout" + ]) + assert.isArray(result[0].vin) + assert.isArray(result[0].vout) + }) + }) + + describe("decodeScriptSingle()", () => { + // block route handler. + const decodeScriptSingle = + rawtransactions.testableComponents.decodeScriptSingle + + it("should throw error if hex is missing", async () => { + const result = await decodeScriptSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "hex can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.hex = + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + + const result = await decodeScriptSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should GET /decodeScriptSingle", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockDecodeScript }) + } + + req.params.hex = + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + + const result = await decodeScriptSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["asm", "type", "p2sh"]) + }) + }) + + describe("decodeScriptBulk()", () => { + const decodeScriptBulk = rawtransactions.testableComponents.decodeScriptBulk + + it("should throw 400 error if hexes array is missing", async () => { + const result = await decodeScriptBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "hexes must be an array") + }) + + it("should throw 400 error if hexes array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.hexes = testArray + + const result = await decodeScriptBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw 400 error if hexes is empty", async () => { + req.body.hexes = [""] + + const result = await decodeScriptBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Encountered empty hex") + }) + + it("should error on non-array single hex", async () => { + req.body.hexes = + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + + const result = await decodeScriptBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hexes must be an array", + "Proper error message" + ) + }) + + it("should decode an array with a single hex", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockDecodeScript }) + } + + req.body.hexes = [ + "4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16" + ] + + const result = await decodeScriptBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], ["asm", "type", "p2sh"]) + }) + + it("should decode an array with a multiple hexes", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: mockData.mockDecodeScript }) + } + + req.body.hexes = [ + "4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16", + "4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16" + ] + + const result = await decodeScriptBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2) + assert.hasAllKeys(result[0], ["asm", "type", "p2sh"]) + }) + }) + + describe("getRawTransactionBulk()", () => { + // block route handler. + const getRawTransactionBulk = + rawtransactions.testableComponents.getRawTransactionBulk + + it("should throw 400 error if txids array is missing", async () => { + const result = await getRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txids must be an array") + }) + + it("should throw 400 error if txids array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.txids = testArray + + const result = await getRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw 400 error if txid is empty", async () => { + req.body.txids = [""] + + const result = await getRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Encountered empty TXID") + }) + + it("should throw 400 error if txid is invalid", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(500, { + error: { message: "parameter 1 must be of length 64 (not 6)" } + }) + } + + req.body.txids = ["abc123"] + + const result = await getRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "parameter 1 must be of length 64 (not 6)") + }) + + it("should get concise transaction data", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockRawTransactionConcise }) + } + + req.body.txids = [ + "bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971" + ] + + const result = await getRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.isString(result[0]) + }) + + it("should get verbose transaction data", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockRawTransactionVerbose }) + } + + req.body.txids = [ + "bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971" + ] + req.body.verbose = true + + const result = await getRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "hex", + "txid", + "hash", + "size", + "version", + "locktime", + "vin", + "vout", + "blockhash", + "confirmations", + "time", + "blocktime" + ]) + assert.isArray(result[0].vin) + assert.isArray(result[0].vout) + }) + }) + + describe("getRawTransactionSingle()", () => { + // block route handler. + const getRawTransactionSingle = + rawtransactions.testableComponents.getRawTransactionSingle + + it("should throw 400 error if txid is missing", async () => { + const result = await getRawTransactionSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should throw 400 error if txid is invalid", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(500, { + error: { message: "parameter 1 must be of length 64 (not 6)" } + }) + } + + req.params.txid = "abc123" + + const result = await getRawTransactionSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "parameter 1 must be of length 64 (not 6)") + }) + + it("should get concise transaction data", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockRawTransactionConcise }) + } + + req.params.txid = + "bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971" + + const result = await getRawTransactionSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isString(result) + }) + + it("should get verbose transaction data", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockRawTransactionVerbose }) + } + + req.params.txid = + "bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971" + req.query.verbose = "true" + + const result = await getRawTransactionSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "hex", + "txid", + "hash", + "size", + "version", + "locktime", + "vin", + "vout", + "blockhash", + "confirmations", + "time", + "blocktime" + ]) + assert.isArray(result.vin) + assert.isArray(result.vout) + }) + }) + + describe("sendRawTransactionBulk()", () => { + const sendRawTransaction = + rawtransactions.testableComponents.sendRawTransactionBulk + + it("should throw 400 error if hexs array is missing", async () => { + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "hex must be an array") + }) + + it("should throw 400 error if hexs array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.hexes = testArray + + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw 400 error if hex array element is empty", async () => { + req.body.hexes = [""] + + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Encountered empty hex") + }) + + it("should throw 500 error if hex is invalid", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(500, { + error: { message: "TX decode failed" } + }) + } + + req.body.hexes = ["abc123"] + + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "TX decode failed") + }) + + it("should submit hex encoded transaction", async () => { + // This is a difficult test to run as transaction hex is invalid after a + // block confirmation. So the unit tests simulates what the output 'should' + // be, but the integration asserts an expected failure. + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { + result: + "aef8848396e67532b42008b9d75b5a5a3459a6717740f31f0553b74102b4b118" + }) + } + + req.body.hexes = [ + "0200000001189f7cf4303e2e0bcc5af4be323b9b397dd4104ca2de09528eb90a1450b8a999010000006a4730440220212ec2ffce136a30cec1bc86a40b08a2afdeb6f8dbd652d7bcb07b1aad6dfa8c022041f59585273b89d88879a9a531ba3272dc953f48ff57dad955b2dee70e76c0624121030143ffd18f1c4add75c86b2f930d9551d51f7a6bd786314247022b7afc45d231ffffffff0230d39700000000001976a914af64a026e06910c59463b000d18c3d125d7e951a88ac58c20000000000001976a914af64a026e06910c59463b000d18c3d125d7e951a88ac00000000" + ] + + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + if (process.env.TEST === "unit") { + assert.isArray(result) + assert.isString(result[0]) + + // Integration test + } else { + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "transaction already in block chain") + } + }) + }) + + describe("sendRawTransactionSingle()", () => { + // block route handler. + const sendRawTransaction = + rawtransactions.testableComponents.sendRawTransactionSingle + + it("should throw an error for an empty hex", async () => { + req.params.hex = "" + + const result = await sendRawTransaction(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Encountered empty hex", + "Proper error message" + ) + }) + + it("should throw an error for a non-string", async () => { + req.params.hex = 456 + + const result = await sendRawTransaction(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hex must be a string", + "Proper error message" + ) + }) + + it("should throw 500 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl = process.env.BITCOINCOM_BASEURL + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.hex = + "0200000001189f7cf4303e2e0bcc5af4be323b9b397dd4104ca2de09528eb90a1450b8a999010000006a4730440220212ec2ffce136a30cec1bc86a40b08a2afdeb6f8dbd652d7bcb07b1aad6dfa8c022041f59585273b89d88879a9a531ba3272dc953f48ff57dad955b2dee70e76c0624121030143ffd18f1c4add75c86b2f930d9551d51f7a6bd786314247022b7afc45d231ffffffff0230d39700000000001976a914af64a026e06910c59463b000d18c3d125d7e951a88ac58c20000000000001976a914af64a026e06910c59463b000d18c3d125d7e951a88ac00000000" + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or great expected." + ) + }) + + it("should throw an error for invalid hex", async () => { + req.params.hex = "abc123" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(500, { + error: { message: "TX decode failed" } + }) + } + + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "TX decode failed") + }) + + it("should GET /sendRawTransaction/:hex", async () => { + // This is a difficult test to run as transaction hex is invalid after a + // block confirmation. So the unit tests simulates what the output 'should' + // be, but the integration asserts an expected failure. + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { + result: + "aef8848396e67532b42008b9d75b5a5a3459a6717740f31f0553b74102b4b118" + }) + } + + req.params.hex = + "0200000001189f7cf4303e2e0bcc5af4be323b9b397dd4104ca2de09528eb90a1450b8a999010000006a4730440220212ec2ffce136a30cec1bc86a40b08a2afdeb6f8dbd652d7bcb07b1aad6dfa8c022041f59585273b89d88879a9a531ba3272dc953f48ff57dad955b2dee70e76c0624121030143ffd18f1c4add75c86b2f930d9551d51f7a6bd786314247022b7afc45d231ffffffff0230d39700000000001976a914af64a026e06910c59463b000d18c3d125d7e951a88ac58c20000000000001976a914af64a026e06910c59463b000d18c3d125d7e951a88ac00000000" + + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + if (process.env.TEST === "unit") { + assert.isString(result) + + // Integration test + } else { + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "transaction already in block chain") + } + }) + }) +}) diff --git a/test/v3/slp.js b/test/v3/slp.js new file mode 100644 index 0000000..5a2641c --- /dev/null +++ b/test/v3/slp.js @@ -0,0 +1,971 @@ +/* + TESTS FOR THE SLP.TS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. + + TODO: + -See listSingleToken() tests. +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const nock = require("nock") // HTTP mocking +const sinon = require("sinon") +const proxyquire = require("proxyquire").noPreserveCache() + +// Prepare the slpRoute for stubbing dependcies on slpjs. +const slpRoute = require("../../src/routes/v3/slp") +const pathStub = {} // Used to stub methods within slpjs. +const slpRouteStub = proxyquire("../../src/routes/v3/slp", { slpjs: pathStub }) + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/slp-mocks") +const slpjsMock = require("./mocks/slpjs-mocks") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#SLP", () => { + let req, res, mockServerUrl + let sandbox + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITDB_URL: process.env.BITDB_URL, + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + SLPDB_URL: process.env.SLPDB_URL + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + + // Block network connections for unit tests. + if (process.env.TEST === "unit") { + process.env.BITDB_URL = "http://fakeurl/" + process.env.BITCOINCOM_BASEURL = "http://fakeurl/" + process.env.SLPDB_URL = "http://fakeurl/" + mockServerUrl = `http://fakeurl` + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + //req.params = {} + req.body = {} + req.query = {} + req.locals = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + + sandbox.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITDB_URL = originalEnvVars.BITDB_URL + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.SLPDB_URL = originalEnvVars.SLPDB_URL + }) + + describe("#root", async () => { + // root route handler. + const root = slpRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result.status, "slp", "Returns static string") + }) + }) + + describe("list()", () => { + // list route handler + const list = slpRoute.testableComponents.list + + it("should throw 500 when network issues", async () => { + // Save the existing SLPDB_URL. + const savedUrl2 = process.env.SLPDB_URL + + // Manipulate the URL to cause a 500 network error. + process.env.SLPDB_URL = "http://fakeurl/api/" + + const result = await list(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.SLPDB_URL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should GET list", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + const b64 = `eyJ2IjozLCJxIjp7ImRiIjpbInQiXSwiZmluZCI6eyIkcXVlcnkiOnt9fSwicHJvamVjdCI6eyJ0b2tlbkRldGFpbHMiOjEsInRva2VuU3RhdHMiOjEsIl9pZCI6MH0sImxpbWl0IjoxMDB9fQ==` + + nock(process.env.SLPDB_URL) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockList) + } + + const result = await list(req, res) + // console.log(`test result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "id", + "timestamp", + "symbol", + "name", + "documentUri", + "documentHash", + "decimals", + "initialTokenQty" + ]) + }) + }) + + describe("listSingleToken()", () => { + const listSingleToken = slpRoute.testableComponents.listSingleToken + + it("should throw 400 if tokenId is empty", async () => { + const result = await listSingleToken(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "tokenId can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing BITDB_URL. + const savedUrl2 = process.env.SLPDB_URL + + // Manipulate the URL to cause a 500 network error. + process.env.SLPDB_URL = "http://fakeurl/api/" + + req.params.tokenId = + "650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + + const result = await listSingleToken(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.SLPDB_URL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should return 'not found' for mainnet txid on testnet", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockSingleToken) + } + + req.params.tokenId = + // testnet + //"650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + // mainnet + "259908ae44f46ef585edef4bcc1e50dc06e4c391ac4be929fae27235b8158cf1" + + const result = await listSingleToken(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["id"]) + assert.include(result.id, "not found") + }) + + it("should get token information", async () => { + // testnet + const tokenIdToTest = + "650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockSingleToken) + } + + req.params.tokenId = tokenIdToTest + + const result = await listSingleToken(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, [ + "id", + "blockCreated", + "blockLastActiveMint", + "blockLastActiveSend", + "circulatingSupply", + "containsBaton", + "mintingBatonStatus", + "txnsSinceGenesis", + "versionType", + "timestamp", + "symbol", + "name", + "documentUri", + "documentHash", + "decimals", + "initialTokenQty", + "totalBurned", + "totalMinted", + "validAddresses" + ]) + }) + }) + + describe("listBulkToken()", () => { + const listBulkToken = slpRoute.testableComponents.listBulkToken + + it("should throw 400 if tokenIds array is empty", async () => { + const result = await listBulkToken(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "tokenIds needs to be an array") + assert.equal(res.statusCode, 400) + }) + + it("should throw 400 error if array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.tokenIds = testArray + + const result = await listBulkToken(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw 400 if tokenId is empty", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockEmptyTokenId) + } + req.body.tokenIds = "" + + const result = await listBulkToken(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include( + result.error, + "tokenIds needs to be an array. Use GET for single tokenId." + ) + }) + + it("should throw 503 when network issues", async () => { + // Save the existing BITDB_URL. + const savedUrl2 = process.env.SLPDB_URL + + // Manipulate the URL to cause a 500 network error. + process.env.SLPDB_URL = "http://fakeurl/api/" + + req.body.tokenIds = [ + "650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + ] + + const result = await listBulkToken(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.SLPDB_URL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should return 'not found' for mainnet txid on testnet", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockSingleTokenError) + } + + req.body.tokenIds = + // testnet + //"650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + // mainnet + ["0b314bc2b2905b8844222871c6b665ae3494117c83b11302824561bb904efb6b"] + + const result = await listBulkToken(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], ["id", "valid"]) + assert.strictEqual(result[0].valid, false) + }) + + it("should get token information for single token ID", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockSingleToken) + } + + req.body.tokenIds = + // testnet + ["650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a"] + + const result = await listBulkToken(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "blockCreated", + "blockLastActiveMint", + "blockLastActiveSend", + "circulatingSupply", + "containsBaton", + "mintingBatonStatus", + "txnsSinceGenesis", + "versionType", + "timestamp", + "symbol", + "name", + "documentUri", + "documentHash", + "decimals", + "initialTokenQty", + "id", + "totalBurned", + "totalMinted", + "validAddresses" + ]) + }) + + it("should get token information for multiple token IDs", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .times(2) + .reply(200, mockData.mockSingleToken) + } + + req.body.tokenIds = + // testnet + [ + "650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a", + "c35a87afad11c8d086c1449ffd8b0a84324e72b15b1bcfdf166a493551b4eea6" + ] + + const result = await listBulkToken(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "blockCreated", + "blockLastActiveMint", + "blockLastActiveSend", + "circulatingSupply", + "containsBaton", + "mintingBatonStatus", + "txnsSinceGenesis", + "versionType", + "timestamp", + "symbol", + "name", + "documentUri", + "documentHash", + "decimals", + "initialTokenQty", + "id", + "totalBurned", + "totalMinted", + "validAddresses" + ]) + }) + }) + + describe("balancesForAddress()", () => { + const balancesForAddress = slpRoute.testableComponents.balancesForAddress + + it("should throw 400 if address is empty", async () => { + const result = await balancesForAddress(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should throw 400 if address is invalid", async () => { + req.params.address = "badAddress" + + const result = await balancesForAddress(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Invalid BCH address.") + }) + + it("should throw 400 if address network mismatch", async () => { + req.params.address = + "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk" + + const result = await balancesForAddress(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Invalid") + }) + + it("should throw 5XX error when network issues", async () => { + // Save the existing SLPDB_URL. + const savedUrl2 = process.env.SLPDB_URL + + // Manipulate the URL to cause a 500 network error. + process.env.SLPDB_URL = "http://fakeurl/api/" + + req.params.address = "slptest:qz35h5mfa8w2pqma2jq06lp7dnv5fxkp2shlcycvd5" + + const result = await balancesForAddress(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.SLPDB_URL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + assert.include( + result.error, + "Network error: Could not communicate", + "Error message expected" + ) + }) + + it("should get token balance for an address", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .times(2) + .reply(200, mockData.mockSingleAddress) + } + + req.params.address = "slptest:pz0qcslrqn7hr44hsszwl4lw5r6udkg6zqv7sq3kk7" + + const result = await balancesForAddress(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "tokenId", + "balance", + "slpAddress", + "decimalCount" + ]) + }) + }) + + describe("balancesForAddressByTokenID()", () => { + const balancesForAddressByTokenID = + slpRoute.testableComponents.balancesForAddressByTokenID + + it("should throw 400 if address is empty", async () => { + req.params.address = "" + req.params.tokenId = + "650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + const result = await balancesForAddressByTokenID(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should throw 400 if tokenId is empty", async () => { + req.params.address = + "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk" + req.params.tokenId = "" + const result = await balancesForAddressByTokenID(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "tokenId can not be empty") + }) + + it("should throw 400 if address is invalid", async () => { + req.params.address = "badAddress" + req.params.tokenId = + "650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + + const result = await balancesForAddressByTokenID(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Invalid BCH address.") + }) + + it("should throw 400 if address network mismatch", async () => { + req.params.address = + "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk" + + const result = await balancesForAddressByTokenID(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Invalid") + }) + + it("should throw 5XX error when network issues", async () => { + // Save the existing SLPDB_URL. + const savedUrl2 = process.env.SLPDB_URL + + // Manipulate the URL to cause a 500 network error. + process.env.SLPDB_URL = "http://fakeurl/api/" + + req.params.address = "slptest:qz4qnxcxwvmacgye8wlakhz0835x0w3vtvxu67w0ac" + req.params.tokenId = + "7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796" + + const result = await balancesForAddressByTokenID(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.SLPDB_URL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + assert.include( + result.error, + "Network error: Could not communicate", + "Error message expected" + ) + }) + + it("should get token information", async () => { + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .times(2) + .reply(200, mockData.mockSingleAddress) + } + + req.params.address = "slptest:pz0qcslrqn7hr44hsszwl4lw5r6udkg6zqv7sq3kk7" + req.params.tokenId = + "6b081fcd1f78b187be1464313dac8ff257251b727a42b613552a4040870aeb29" + + const result = await balancesForAddressByTokenID(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // TODO - add decimalCount + // assert.hasAllKeys(result, ["tokenId", "balance", "decimalCount"]) + assert.hasAllKeys(result, ["tokenId", "balance"]) + }) + }) + + describe("convertAddressSingle()", () => { + const convertAddressSingle = + slpRoute.testableComponents.convertAddressSingle + + it("should throw 400 if address is empty", async () => { + req.params.address = "" + const result = await convertAddressSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + // + it("should convert address", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.SLPDB_URL}`) + .post(``) + .reply(200, { result: mockData.mockConvert }) + } + + req.params.address = "slptest:qz35h5mfa8w2pqma2jq06lp7dnv5fxkp2shlcycvd5" + + const result = await convertAddressSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["cashAddress", "legacyAddress", "slpAddress"]) + }) + }) + + describe("convertAddressBulk()", () => { + const convertAddressBulk = slpRoute.testableComponents.convertAddressBulk + + it("should throw 400 if addresses array is empty", async () => { + const result = await convertAddressBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "addresses needs to be an array") + assert.equal(res.statusCode, 400) + }) + + it("should throw 400 error if array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await convertAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should error on malformed address", async () => { + try { + req.body.addresses = ["bitcoincash:qzs02v05l7qs5s5dwuj0cx5ehjm2c"] + + await convertAddressBulk(req, res) + + assert.equal(true, false, "Unsupported address format") + } catch (err) { + // console.log(`err.message: ${util.inspect(err.message)}`) + + assert.include(err.message, `Unsupported address format`) + } + }) + + it("should validate array with single element", async () => { + req.body.addresses = [ + "bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c" + ] + + const result = await convertAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "slpAddress", + "cashAddress", + "legacyAddress" + ]) + }) + + it("should validate array with multiple elements", async () => { + req.body.addresses = [ + "bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c", + "bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0" + ] + + const result = await convertAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "slpAddress", + "cashAddress", + "legacyAddress" + ]) + }) + }) + + describe("validateBulk()", () => { + const validateBulk = slpRoute.testableComponents.validateBulk + + it("should throw 400 if txid array is empty", async () => { + const result = await validateBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txids needs to be an array") + assert.equal(res.statusCode, 400) + }) + + it("should throw 400 error if array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.txids = testArray + + const result = await validateBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should validate array with single element", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(slpRoute.testableComponents, "isValidSlpTxid") + .resolves(true) + } + + req.body.txids = [ + "78d57a82a0dd9930cc17843d9d06677f267777dd6b25055bad0ae43f1b884091" + ] + + const result = await validateBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], ["txid", "valid"]) + }) + + it("should validate array with two elements", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(slpRoute.testableComponents, "isValidSlpTxid") + .resolves(true) + } + + req.body.txids = [ + "78d57a82a0dd9930cc17843d9d06677f267777dd6b25055bad0ae43f1b884091", + "82d996847a861b08b1601284ef7d40a1777d019154a6c4ed11571609dd3555ac" + ] + + const result = await validateBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], ["txid", "valid"]) + assert.equal(result.length, 2) + }) + }) + + describe("tokenStatsSingle()", () => { + const tokenStatsSingle = slpRoute.testableComponents.tokenStats + + it("should throw 400 if tokenID is empty", async () => { + req.params.tokenId = "" + const result = await tokenStatsSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "tokenId can not be empty") + }) + // + it("should get token stats for tokenId", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.SLPDB_URL}`) + .get(uri => uri.includes("/")) + .reply(200, { + t: [ + { + tokenDetails: mockData.mockTokenDetails, + tokenStats: mockData.mockTokenStats + } + ] + }) + } + + req.params.tokenId = + "37279c7dc81ceb34d12f03344b601c582e931e05d0e552c29c428bfa39d39af3" + + const result = await tokenStatsSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, [ + "blockCreated", + "blockLastActiveMint", + "blockLastActiveSend", + "containsBaton", + "initialTokenQty", + "mintingBatonStatus", + "circulatingSupply", + "decimals", + "documentHash", + "versionType", + "timestamp", + "documentUri", + "name", + "symbol", + "id", + "totalBurned", + "totalMinted", + "txnsSinceGenesis", + "validAddresses" + ]) + }) + }) + + describe("balancesForTokenSingle()", () => { + const balancesForTokenSingle = + slpRoute.testableComponents.balancesForTokenSingle + + it("should throw 400 if tokenID is empty", async () => { + req.params.tokenId = "" + const result = await balancesForTokenSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "tokenId can not be empty") + }) + // + it("should get balances for tokenId", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.SLPDB_URL}`) + .get(uri => uri.includes("/")) + .reply(200, { + a: [mockData.mockBalance] + }) + } + + req.params.tokenId = + "37279c7dc81ceb34d12f03344b601c582e931e05d0e552c29c428bfa39d39af3" + + const result = await balancesForTokenSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result[0], ["tokenId", "slpAddress", "tokenBalance"]) + }) + }) + + describe("txDetails()", () => { + let txDetails = slpRoute.testableComponents.txDetails + + it("should throw 400 if txid is empty", async () => { + const result = await txDetails(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should throw 400 for malformed txid", async () => { + req.params.txid = + "57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b9457" + + const result = await txDetails(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "This is not a txid") + }) + + it("should throw 400 for non-existant txid", async () => { + // Integration test + if (process.env.TEST !== "unit") { + req.params.txid = + "57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b94578223333" + + const result = await txDetails(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "TXID not found") + } + }) + + it("should get tx details with token info", async () => { + if (process.env.TEST === "unit") { + // Mock the slpjs library for unit tests. + pathStub.BitboxNetwork = slpjsMock.BitboxNetwork + txDetails = slpRouteStub.testableComponents.txDetails + } + + req.params.txid = + "57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b9457822d446" + + const result = await txDetails(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAnyKeys(result, ["tokenIsValid", "tokenInfo"]) + }) + }) + + describe("txsTokenIdAddressSingle()", () => { + const txsTokenIdAddressSingle = + slpRoute.testableComponents.txsTokenIdAddressSingle + + it("should throw 400 if tokenId is empty", async () => { + req.params.tokenId = "" + const result = await txsTokenIdAddressSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "tokenId can not be empty") + }) + + it("should throw 400 if address is empty", async () => { + req.params.tokenId = + "495322b37d6b2eae81f045eda612b95870a0c2b6069c58f70cf8ef4e6a9fd43a" + req.params.address = "" + const result = await txsTokenIdAddressSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + /* + it("should get tx details with tokenId and address", async () => { + if (process.env.TEST === "unit") { + nock(`${process.env.SLPDB_URL}`) + .get(uri => uri.includes("/")) + .reply(200, { + c: mockData.mockTransactions + }) + } + + //req.params.tokenId = + // "37279c7dc81ceb34d12f03344b601c582e931e05d0e552c29c428bfa39d39af3" + //req.params.address = "slptest:qr83cu3p7yg9yac7qthwm0nul2ev2kukvsqmes3vl0" + + req.params.tokenId = + "7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796" + req.params.address = "slptest:qpwa35xq0q0cnmdu0rwzkct369hddzsqpsqdzw6h9h" + + + const result = await txsTokenIdAddressSingle(req, res) + console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAnyKeys(result[0], ["txid", "tokenDetails"]) + }) +*/ + }) +}) diff --git a/test/v3/transaction.js b/test/v3/transaction.js new file mode 100644 index 0000000..5d218b5 --- /dev/null +++ b/test/v3/transaction.js @@ -0,0 +1,333 @@ +/* + TESTS FOR THE TRANSACTION.TS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. + + TODO: + -See "should throw an error for an invalid txid" for detailsSingle: + --The error handler should be refactored to return an intelligent error message, + instead of the 503 error it is returning now. +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const transactionRoute = require("../../src/routes/v3/transaction") +const nock = require("nock") // HTTP mocking + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/transaction-mocks") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#Transactions", () => { + let req, res + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", async () => { + // root route handler. + const root = transactionRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result.status, "transaction", "Returns static string") + }) + }) + + describe("#detailsBulk", async () => { + const detailsBulk = transactionRoute.testableComponents.detailsBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await detailsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txids needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single txid", async () => { + req.body = { + txids: `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40` + } + + const result = await detailsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txids needs to be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid txid", async () => { + const fakeTXID = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/tx/${fakeTXID}`) + .reply(400, { + result: { error: "parameter 1 must be hexadecimal string" } + }) + } + + req.body = { + txids: [fakeTXID] + } + + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + }) + + it("should process a single txid", async () => { + const txid = `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/tx/${txid}`) + .reply(200, mockData.mockDetails) + } + + req.body = { + txids: [txid] + } + + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "txid", + "version", + "locktime", + "vin", + "vout", + "blockhash", + "blockheight", + "confirmations", + "time", + "blocktime", + "valueOut", + "size", + "valueIn", + "fees" + ]) + }) + + it("should process a multiple txids", async () => { + const txid1 = `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40` + const txid2 = `8d4fd4dcaa9d8051dc7d862dc23d8aa23e20b77b9c928c49380685459caa7043` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/tx/${txid1}`) + .reply(200, mockData.mockDetails) + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/tx/${txid2}`) + .reply(200, mockData.mockDetails) + } + + req.body = { + txids: [txid1, txid2] + } + + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "txid", + "version", + "locktime", + "vin", + "vout", + "blockhash", + "blockheight", + "confirmations", + "time", + "blocktime", + "valueOut", + "size", + "valueIn", + "fees" + ]) + }) + }) + + describe("#detailsSingle", () => { + // details route handler. + const detailsSingle = transactionRoute.testableComponents.detailsSingle + + it("should throw 400 if txid is empty", async () => { + const result = await detailsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should error on an array", async () => { + req.params.txid = [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + + const result = await detailsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txid can not be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid txid", async () => { + if (process.env.TEST !== "unit") { + req.params.txid = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + const result = await detailsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // The error handling code should probably be updated to respond with a better + // error message. + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "parameter 1 must be hexadecimal string", + "Proper error message" + ) + } + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.params.txid = `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40` + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await detailsSingle(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get details for a single address", async () => { + const txid = `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40` + req.params.txid = txid + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/tx/${txid}`) + .reply(200, mockData.mockDetails) + } + + // Call the details API. + const result = await detailsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.hasAllKeys(result, [ + "txid", + "version", + "locktime", + "vin", + "vout", + "blockhash", + "blockheight", + "confirmations", + "time", + "blocktime", + "valueOut", + "size", + "valueIn", + "fees" + ]) + assert.isArray(result.vin) + assert.isArray(result.vout) + }) + }) +}) diff --git a/test/v3/util.js b/test/v3/util.js new file mode 100644 index 0000000..905157d --- /dev/null +++ b/test/v3/util.js @@ -0,0 +1,300 @@ +/* + TESTS FOR THE UTIL.TS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const utilRoute = require("../../src/routes/v3/util") +const nock = require("nock") // HTTP mocking + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/util-mocks") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#Util", () => { + let req, res + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", async () => { + // root route handler. + const root = utilRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result.status, "util", "Returns static string") + }) + }) + + describe("#validateAddressSingle", async () => { + const validateAddress = utilRoute.testableComponents.validateAddressSingle + + it("should throw an error for an empty address", async () => { + const result = await validateAddress(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "address can not be empty", + "Proper error message" + ) + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.address = `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y` + + const result = await validateAddress(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should validate address", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockAddress }) + } + + req.params.address = `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y` + + const result = await validateAddress(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, [ + "isvalid", + "address", + "scriptPubKey", + "ismine", + "iswatchonly", + "isscript" + ]) + }) + }) + + describe("#validateAddressBulk", async () => { + const validateAddressBulk = utilRoute.testableComponents.validateAddressBulk + + it("should throw an error for an empty body", async () => { + const result = await validateAddressBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array. Use GET for single address.", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + addresses: `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y` + } + + const result = await validateAddressBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array. Use GET for single address.", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await validateAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should error on invalid address", async () => { + req.body = { + addresses: [`bchtest:qqqk4y6lsl5da64sg5qc3xezmpl`] + } + + const result = await validateAddressBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address. Double check your address is valid", + "Proper error message" + ) + }) + + it("should error on mainnet address when using testnet", async () => { + req.body = { + addresses: [`bitcoincash:qrcc3jsqpgwqcdru70sk54sd0g3l04q7c53ycm6ucj`] + } + + const result = await validateAddressBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid network. Trying to use a testnet address on mainnet, or vice versa.", + "Proper error message" + ) + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.body.addresses = [ + `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y` + ] + + const result = await validateAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + }) + + it("should validate a single address", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockAddress }) + } + + req.body.addresses = [ + `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y` + ] + + const result = await validateAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "isvalid", + "address", + "scriptPubKey", + "ismine", + "iswatchonly", + "isscript" + ]) + }) + + it("should validate a multiple addresses", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: mockData.mockAddress }) + } + + req.body.addresses = [ + `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y`, + `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y` + ] + + const result = await validateAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "isvalid", + "address", + "scriptPubKey", + "ismine", + "iswatchonly", + "isscript" + ]) + }) + }) +})