diff --git a/src/routes/v3/blockbook.js b/src/routes/v3/blockbook.js new file mode 100644 index 0000000..eded070 --- /dev/null +++ b/src/routes/v3/blockbook.js @@ -0,0 +1,382 @@ +/* + Bitcore Node API route +*/ + +"use strict" + +const express = require("express") +const requestUtils = require("./services/requestUtils") +const axios = require("axios") +const routeUtils = require("./route-utils") +const wlogger = require("../../util/winston-logging") + +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() + +//const BITCORE_URL = process.env.BITCORE_URL + +// Connect the route endpoints to their handler functions. +router.get("/", root) +router.get("/balance/:address", balanceSingle) + +// Root API endpoint. Simply acknowledges that it exists. +function root(req, res, next) { + return res.json({ status: "address" }) +} + +// Query the Bitcore Node API for a balance on a single BCH address. +// Returns a Promise. +async function balanceFromBlockbook(thisAddress) { + try { + //console.log(`BITCORE_URL: ${BITCORE_URL}`) + + // Convert the address to a cashaddr without a prefix. + const addr = BITBOX.Address.toCashAddress(thisAddress, false) + + // Determine if we are working with the testnet or mainnet networks. + let network = "mainnet" + if (process.env.NETWORK === "testnet") network = "testnet" + + const path = `${process.env.BLOCKBOOK_URL}api/v2/address/${addr}` + + // Query the Bitcore Node API. + const axiosResponse = await axios.get(path) + const retData = axiosResponse.data + //console.log(`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 + } +} + +// GET handler for single balance +async function balanceSingle(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." + }) + } + + wlogger.debug( + `Executing bitcore/balanceSingle with this address: `, + address + ) + + // Ensure the input is a valid BCH address. + try { + const 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 Bitcore Node API. + const retData = await balanceFromBlockbook(address) + + // 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. + wlogger.error(`Error in blockbook.js/balanceSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} +/* +// POST handler for bulk queries on address details +async function balanceBulk(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.` + }) + } + + wlogger.debug( + `Executing bitcore.js/balanceBulk 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) => + balanceFromBitcore(address) + ) + + // 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 }) + } + + wlogger.error(`Error in bitcore.js/balanceBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// Query the Bitcore Node API for utxos associated with a BCH address. +// Returns a Promise. +async function utxosFromBitcore(thisAddress) { + try { + //console.log(`BITCORE_URL: ${BITCORE_URL}`) + + // Convert the address to a cashaddr without a prefix. + const addr = BITBOX.Address.toCashAddress(thisAddress, false) + + // Determine if we are working with the testnet or mainnet networks. + let network = "mainnet" + if (process.env.NETWORK === "testnet") network = "testnet" + + const path = `${process.env.BITCORE_URL}api/BCH/${network}/address/${addr}/?unspent=true` + + // Query the Bitcore Node API. + const axiosResponse = await axios.get(path) + const retData = axiosResponse.data + //console.log(`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 + } +} + +// GET handler for single balance +async function utxosSingle(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." + }) + } + + wlogger.debug( + `Executing bitcore/balanceSingle with this address: `, + address + ) + + // Ensure the input is a valid BCH address. + try { + const 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 Bitcore Node API. + const retData = await utxosFromBitcore(address) + + // 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. + wlogger.error(`Error in bitcore.js/utxosSingle().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} + +// POST handler for bulk queries on address utxos +async function utxosBulk(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.` + }) + } + + wlogger.debug( + `Executing bitcore.js/utxosBulk 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) => + utxosFromBitcore(address) + ) + + // 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 }) + } + + wlogger.error(`Error in bitcore.js/utxosBulk().`, err) + + res.status(500) + return res.json({ error: util.inspect(err) }) + } +} +*/ +module.exports = { + router, + testableComponents: { + root, + balanceSingle + //balanceBulk, + //utxosSingle, + //utxosBulk + } +} diff --git a/test/v3/blockbook.js b/test/v3/blockbook.js new file mode 100644 index 0000000..b8af026 --- /dev/null +++ b/test/v3/blockbook.js @@ -0,0 +1,600 @@ +/* + TESTS FOR THE BLOCKBOOK.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: +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const blockbookRoute = require("../../src/routes/v3/blockbook") +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/bitcore-mock") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#Blockbook Router", () => { + let req, res, mockServerUrl + + before(() => { + originalUrl = process.env.BLOCKBOOK_URL + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BLOCKBOOK_URL = "http://fakeurl/api/" + mockServerUrl = `http://fakeurl` + } + // 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.BLOCKBOOK_URL = originalUrl + }) + + describe("#root", () => { + // root route handler. + const root = blockbookRoute.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("#Balance Single", () => { + // details route handler. + const balanceSingle = blockbookRoute.testableComponents.balanceSingle + + it("should throw 400 if address is empty", async () => { + const result = await balanceSingle(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 balanceSingle(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 balanceSingle(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 balanceSingle(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.BLOCKBOOK_URL + + try { + req.params.address = `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + // Switch the Insight URL to something that will error out. + process.env.BLOCKBOOK_URL = "http://fakeurl/api/" + + const result = await balanceSingle(req, res) + + // Restore the saved URL. + process.env.BLOCKBOOK_URL = 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.BLOCKBOOK_URL = savedUrl + } + }) + + it("should get balance 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.BLOCKBOOK_URL}`) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockBalance) + } + + // Call the details API. + const result = await balanceSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["confirmed", "unconfirmed", "balance"]) + assert.isNumber(result.confirmed) + assert.isNumber(result.unconfirmed) + assert.isNumber(result.balance) + }) + }) + /* + describe("#Balance Bulk", () => { + // details route handler. + const balanceBulk = blockbookRoute.testableComponents.balanceBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await balanceBulk(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 balanceBulk(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 balanceBulk(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 balanceBulk(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 balanceBulk(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 balanceBulk(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 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(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockBalance) + } + + // Call the details API. + const result = await balanceBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], ["confirmed", "unconfirmed", "balance"]) + assert.isNumber(result[0].confirmed) + assert.isNumber(result[0].unconfirmed) + assert.isNumber(result[0].balance) + }) + + 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(mockServerUrl) + .get(uri => uri.includes("/")) + .times(2) + .reply(200, mockData.mockBalance) + } + + // Call the details API. + const result = await balanceBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "2 outputs for 2 inputs") + }) + }) + + describe("#UTXOs Single", () => { + // details route handler. + const utxosSingle = blockbookRoute.testableComponents.utxosSingle + + it("should throw 400 if address is empty", async () => { + const result = await utxosSingle(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 utxosSingle(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 utxosSingle(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 utxosSingle(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.BLOCKBOOK_URL + + try { + req.params.address = `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + // Switch the Insight URL to something that will error out. + process.env.BLOCKBOOK_URL = "http://fakeurl/api/" + + const result = await utxosSingle(req, res) + + // Restore the saved URL. + process.env.BLOCKBOOK_URL = 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.BLOCKBOOK_URL = savedUrl + } + }) + + it("should get utxos 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.BLOCKBOOK_URL}`) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockUtxos) + } + + // Call the details API. + const result = await utxosSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "_id", + "chain", + "network", + "coinbase", + "mintIndex", + "spentTxid", + "mintTxid", + "mintHeight", + "spentHeight", + "address", + "script", + "value", + "confirmations" + ]) + }) + }) + + describe("#UTXO Bulk", () => { + // details route handler. + const utxosBulk = blockbookRoute.testableComponents.utxosBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await utxosBulk(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 utxosBulk(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 utxosBulk(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 utxosBulk(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 utxosBulk(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 utxosBulk(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 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(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockUtxos) + } + + // Call the details API. + const result = await utxosBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.isArray(result[0]) + assert.hasAnyKeys(result[0][0], [ + "_id", + "chain", + "network", + "coinbase", + "mintIndex", + "spentTxid", + "mintTxid", + "mintHeight", + "spentHeight", + "address", + "script", + "value", + "confirmations" + ]) + }) + + 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(mockServerUrl) + .get(uri => uri.includes("/")) + .times(2) + .reply(200, mockData.mockUtxos) + } + + // Call the details API. + const result = await utxosBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.isArray(result[0]) + assert.hasAnyKeys(result[0][0], [ + "_id", + "chain", + "network", + "coinbase", + "mintIndex", + "spentTxid", + "mintTxid", + "mintHeight", + "spentHeight", + "address", + "script", + "value", + "confirmations" + ]) + }) + }) + */ +})