diff --git a/src/app.js b/src/app.js index b317fc8..6b6c9a2 100644 --- a/src/app.js +++ b/src/app.js @@ -32,7 +32,7 @@ const RawtransactionsV3 = require('./routes/v3/full-node/rawtransactions') const utilV3 = require('./routes/v3/util') const slpV3 = require('./routes/v3/slp') const xpubV3 = require('./routes/v3/xpub') -const blockbookV3 = require('./routes/v3/blockbook') +const BlockbookV3 = require('./routes/v3/blockbook') const Ninsight = require('./routes/v3/ninsight') require('dotenv').config() @@ -42,6 +42,7 @@ const blockchainV3 = new BlockchainV3() const controlV3 = new ControlV3() const miningV3 = new MiningV3() const rawtransactionsV3 = new RawtransactionsV3() +const blockbookV3 = new BlockbookV3() const app = express() diff --git a/src/routes/v3/blockbook.js b/src/routes/v3/blockbook.js index cb4d15c..ab0d840 100644 --- a/src/routes/v3/blockbook.js +++ b/src/routes/v3/blockbook.js @@ -6,9 +6,11 @@ const express = require('express') const axios = require('axios') -const routeUtils = require('./route-utils') const wlogger = require('../../util/winston-logging') +const RouteUtils = require('../../util/route-utils') +const routeUtils = new RouteUtils() + // Library for easily switching the API paths to use different instances of // Blockbook. const BlockbookPath = require('../../util/blockbook-path') @@ -24,588 +26,561 @@ util.inspect.defaultOptions = { depth: 1 } const BCHJS = require('@chris.troutner/bch-js') const bchjs = new BCHJS() -// const BLOCKBOOK_URL = process.env.BLOCKBOOK_URL +let _this -// Connect the route endpoints to their handler functions. -router.get('/', root) -router.get('/balance/:address', balanceSingle) -router.post('/balance', balanceBulk) -router.get('/utxos/:address', utxosSingle) -router.post('/utxos', utxosBulk) -router.get('/tx/:txid', txSingle) -router.post('/tx', txBulk) +class Blockbook { + constructor () { + _this = this -// Root API endpoint. Simply acknowledges that it exists. -function root (req, res, next) { - return res.json({ status: 'address' }) -} + _this.axios = axios + _this.routeUtils = routeUtils + _this.bchjs = bchjs + _this.BLOCKBOOKPATH = BLOCKBOOKPATH -// Query the Blockbook Node API for a balance on a single BCH address. -// Returns a Promise. -async function balanceFromBlockbook (thisAddress) { - try { - // console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`) - - // Convert the address to a cashaddr without a prefix. - const addr = bchjs.Address.toCashAddress(thisAddress) - - const path = `${BLOCKBOOKPATH.addrPath}${addr}` - // console.log(`path: ${path}`) - - // Query the Blockbook 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. - wlogger.debug('Error in blockbook.js/balanceFromBlockbook()') - throw err + _this.router = router + _this.router.get('/', _this.root) + _this.router.get('/balance/:address', _this.balanceSingle) + _this.router.post('/balance', _this.balanceBulk) + _this.router.get('/utxos/:address', _this.utxosSingle) + _this.router.post('/utxos', _this.utxosBulk) + _this.router.get('/tx/:txid', _this.txSingle) + _this.router.post('/tx', _this.txBulk) } -} -/** - * @api {get} /blockbook/balance/{addr} Get balance for a single address. - * @apiName Balance for a single address - * @apiGroup Blockbook - * @apiDescription Returns an object with balance and details about an address. - * - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockbook/balance/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json" - * - */ -// 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 blockbook/balanceSingle with this address: ', - address - ) - - // Ensure the input is a valid BCH address. - try { - // const legacyAddr = bchjs.Address.toLegacyAddress(address) - bchjs.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 Blockbook Node API. - const retData = await balanceFromBlockbook(address) - - // Return the retrieved address information. - res.status(200) - return res.json(retData) - } catch (err) { + // DRY error handler. + errorHandler (err, res) { // Attempt to decode the error message. - const { msg, status } = routeUtils.decodeError(err) + const { msg, status } = _this.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) }) } -} -/** - * @api {post} /blockbook/balance Get balance for an array of addresses. - * @apiName Balance for an array of addresses - * @apiGroup Blockbook - * @apiDescription Return balances and details for an array of addresses. - * Limited to 20 items per request. - * - * @apiExample Example usage: - * curl -X POST "https://mainnet.bchjs.cash/v3/blockbook/balance" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' - * - * - */ -// 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 + // Root API endpoint. Simply acknowledges that it exists. + root (req, res, next) { + return res.json({ status: 'address' }) + } - // 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.' - }) + // Query the Blockbook Node API for a balance on a single BCH address. + // Returns a Promise. + async balanceFromBlockbook (thisAddress) { + try { + // console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`) + + // Convert the address to a cashaddr without a prefix. + const addr = _this.bchjs.Address.toCashAddress(thisAddress) + + const path = `${_this.BLOCKBOOKPATH.addrPath}${addr}` + // console.log(`path: ${path}`) + + // Query the Blockbook Node API. + const options = { + method: 'get', + baseURL: path + } + + const axiosResponse = await _this.axios.request(options) + 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. + wlogger.debug('Error in blockbook.js/balanceFromBlockbook()') + throw err } + } - // 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.' - }) - } + /** + * @api {get} /blockbook/balance/{addr} Get balance for a single address. + * @apiName Balance for a single address + * @apiGroup Blockbook + * @apiDescription Returns an object with balance and details about an address. + * + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockbook/balance/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json" + * + */ + // GET handler for single balance + async balanceSingle (req, res, next) { + try { + const address = req.params.address - wlogger.debug( - 'Executing blockbook.js/balanceBulk with these addresses: ', - addresses - ) + if (!address || address === '') { + res.status(400) + return res.json({ error: 'address can not be empty' }) + } - // Validate each element in the address array. - for (let i = 0; i < addresses.length; i++) { - const thisAddress = addresses[i] + // 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 blockbook/balanceSingle with this address: ', + address + ) // Ensure the input is a valid BCH address. try { - bchjs.Address.toLegacyAddress(thisAddress) + // const legacyAddr = bchjs.Address.toLegacyAddress(address) + _this.bchjs.Address.toLegacyAddress(address) } catch (err) { res.status(400) return res.json({ - error: `Invalid BCH address. Double check your address is valid: ${thisAddress}` + 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(thisAddress) + const networkIsValid = _this.routeUtils.validateNetwork(address) 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.` + error: 'Invalid network. 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) => - // console.log(`address: ${address}`) - balanceFromBlockbook(address) - ) + // Query the Blockbook Node API. + const retData = await _this.balanceFromBlockbook(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 blockbook.js/balanceBulk().', err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -// Query the Blockbook API for utxos associated with a BCH address. -// Returns a Promise. -async function utxosFromBlockbook (thisAddress) { - try { - // console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`) - - // Convert the address to a cashaddr without a prefix. - const addr = bchjs.Address.toCashAddress(thisAddress) - - const path = `${BLOCKBOOKPATH.utxoPath}${addr}` - // console.log(`path: ${path}`) - - // Query the Blockbook API. - const axiosResponse = await axios.get(path) - const retData = axiosResponse.data - // console.log(`retData: ${util.inspect(retData)}`) - - // Add the satoshis property to each UTXO. - for (let i = 0; i < retData.length; i++) { retData[i].satoshis = Number(retData[i].value) } - - return retData - } catch (err) { - // Dev Note: Do not log error messages here. Throw them instead and let the - // parent function handle it. - wlogger.debug('Error in blockbook.js/utxosFromBlockbook()') - throw err - } -} - -/** - * @api {get} /blockbook/utxos/{addr} Get utxos for a single address. - * @apiName UTXOs for a single address - * @apiGroup Blockbook - * @apiDescription Returns an object with UTXOs associated with an address. - * - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockbook/utxos/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" - * - */ -// 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 blockbook/utxosSingle with this address: ', - address - ) - - // Ensure the input is a valid BCH address. - try { - // const legacyAddr = bchjs.Address.toLegacyAddress(address) - bchjs.Address.toLegacyAddress(address) + // Return the retrieved address information. + res.status(200) + return res.json(retData) } catch (err) { - res.status(400) - return res.json({ - error: `Invalid BCH address. Double check your address is valid: ${address}` - }) + // Write out error to error log. + wlogger.error('Error in blockbook.js/balanceSingle().', err) + + return _this.errorHandler(err, res) } - - // 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 Blockbook API. - const retData = await utxosFromBlockbook(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/utxosSingle().', err) - - res.status(500) - return res.json({ error: util.inspect(err) }) } -} -/** - * @api {post} /blockbook/utxos Get UTXOs for an array of addresses. - * @apiName UTXOs for an array of addresses - * @apiGroup Blockbook - * @apiDescription Return UTXOs associate with an array of addresses. - * Limited to 20 items per request. - * - * @apiExample Example usage: - * curl -X POST "https://mainnet.bchjs.cash/v3/blockbook/utxos" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3","bitcoincash:qzy8wnj0dz927eu6kvh8v2pqsr5w8jh33ys757tdtq"]}' - * - * - */ -// 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 + /** + * @api {post} /blockbook/balance Get balance for an array of addresses. + * @apiName Balance for an array of addresses + * @apiGroup Blockbook + * @apiDescription Return balances and details for an array of addresses. + * Limited to 20 items per request. + * + * @apiExample Example usage: + * curl -X POST "https://mainnet.bchjs.cash/v3/blockbook/balance" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' + * + * + */ + // POST handler for bulk queries on address details + async 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.' - }) + // 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 blockbook.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 { + _this.bchjs.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 = _this.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) => + // console.log(`address: ${address}`) + _this.balanceFromBlockbook(address) + ) + + // Wait for all parallel Insight requests to return. + const result = await _this.axios.all(addresses) + + // Return the array of retrieved address information. + res.status(200) + return res.json(result) + } catch (err) { + wlogger.error('Error in blockbook.js/balanceBulk().', err) + + return _this.errorHandler(err, res) } + } - // 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.' - }) + // Query the Blockbook API for utxos associated with a BCH address. + // Returns a Promise. + async utxosFromBlockbook (thisAddress) { + try { + // console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`) + + // Convert the address to a cashaddr without a prefix. + const addr = _this.bchjs.Address.toCashAddress(thisAddress) + + const path = `${_this.BLOCKBOOKPATH.utxoPath}${addr}` + // console.log(`path: ${path}`) + + // Query the Blockbook API. + // Query the Blockbook Node API. + const options = { + method: 'get', + baseURL: path + } + const axiosResponse = await _this.axios.request(options) + const retData = axiosResponse.data + // console.log(`retData: ${util.inspect(retData)}`) + + // Add the satoshis property to each UTXO. + for (let i = 0; i < retData.length; i++) { retData[i].satoshis = Number(retData[i].value) } + + return retData + } catch (err) { + // Dev Note: Do not log error messages here. Throw them instead and let the + // parent function handle it. + wlogger.debug('Error in blockbook.js/utxosFromBlockbook()') + throw err } + } - wlogger.debug( - 'Executing blockbook.js/utxosBulk with these addresses: ', - addresses - ) + /** + * @api {get} /blockbook/utxos/{addr} Get utxos for a single address. + * @apiName UTXOs for a single address + * @apiGroup Blockbook + * @apiDescription Returns an object with UTXOs associated with an address. + * + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockbook/utxos/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" + * + */ + // GET handler for single balance + async utxosSingle (req, res, next) { + try { + const address = req.params.address - // Validate each element in the address array. - for (let i = 0; i < addresses.length; i++) { - const thisAddress = addresses[i] + 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 blockbook/utxosSingle with this address: ', + address + ) // Ensure the input is a valid BCH address. try { - bchjs.Address.toLegacyAddress(thisAddress) + // const legacyAddr = bchjs.Address.toLegacyAddress(address) + _this.bchjs.Address.toLegacyAddress(address) } catch (err) { res.status(400) return res.json({ - error: `Invalid BCH address. Double check your address is valid: ${thisAddress}` + 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(thisAddress) + const networkIsValid = _this.routeUtils.validateNetwork(address) 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.` + error: 'Invalid network. 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) => + // Query the Blockbook API. + const retData = await _this.utxosFromBlockbook(address) + + // Return the retrieved address information. + res.status(200) + return res.json(retData) + } catch (err) { + // Write out error to error log. + wlogger.error('Error in blockbook.js/utxosSingle().', err) + return _this.errorHandler(err, res) + } + } + + /** + * @api {post} /blockbook/utxos Get UTXOs for an array of addresses. + * @apiName UTXOs for an array of addresses + * @apiGroup Blockbook + * @apiDescription Return UTXOs associate with an array of addresses. + * Limited to 20 items per request. + * + * @apiExample Example usage: + * curl -X POST "https://mainnet.bchjs.cash/v3/blockbook/utxos" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3","bitcoincash:qzy8wnj0dz927eu6kvh8v2pqsr5w8jh33ys757tdtq"]}' + * + * + */ + // POST handler for bulk queries on address utxos + async 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 (!_this.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 blockbook.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 { + _this.bchjs.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 = _this.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) => // console.log(`address: ${address}`) - utxosFromBlockbook(address) - ) + _this.utxosFromBlockbook(address) + ) - // Wait for all parallel Insight requests to return. - const result = await axios.all(addresses) + // Wait for all parallel Insight requests to return. + const result = await _this.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 }) + // Return the array of retrieved address information. + res.status(200) + return res.json(result) + } catch (err) { + wlogger.error('Error in blockbook.js/utxosBulk().', err) + return _this.errorHandler(err, res) } - - wlogger.error('Error in blockbook.js/utxosBulk().', err) - - res.status(500) - return res.json({ error: util.inspect(err) }) } -} -// Query the Blockbook Node API for transactions on a single TXID. -// Returns a Promise. -async function transactionsFromBlockbook (txid) { - try { - // console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`) + // Query the Blockbook Node API for transactions on a single TXID. + // Returns a Promise. + async transactionsFromBlockbook (txid) { + try { + // console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`) - const path = `${BLOCKBOOKPATH.txPath}${txid}` - // console.log(`path: ${path}`) + const path = `${_this.BLOCKBOOKPATH.txPath}${txid}` + // console.log(`path: ${path}`) - // Query the Blockbook Node API. - const axiosResponse = await axios.get(path) - const retPromise = axiosResponse.data - // console.log(`retData: ${util.inspect(retData)}`) + // Query the Blockbook Node API. + const options = { + method: 'get', + baseURL: path + } + const axiosResponse = await _this.axios.request(options) + const retPromise = axiosResponse.data + // console.log(`retData: ${util.inspect(retData)}`) - return retPromise - } catch (err) { - // Dev Note: Do not log error messages here. Throw them instead and let the - // parent function handle it. - wlogger.debug('Error in blockbook.js/transactionsFromBlockbook()') - throw err + return retPromise + } catch (err) { + // Dev Note: Do not log error messages here. Throw them instead and let the + // parent function handle it. + wlogger.debug('Error in blockbook.js/transactionsFromBlockbook()') + throw err + } } -} -/** - * @api {get} /blockbook/tx/{txid} Get details for a single transaction. - * @apiName Details for a single transaction - * @apiGroup Blockbook - * @apiDescription Returns an object with details for a single transaction - * - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockbook/tx/6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d" -H "accept: application/json" - * - */ -// GET handler for single transaction details. -async function txSingle (req, res, next) { - try { - const txid = req.params.txid + /** + * @api {get} /blockbook/tx/{txid} Get details for a single transaction. + * @apiName Details for a single transaction + * @apiGroup Blockbook + * @apiDescription Returns an object with details for a single transaction + * + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockbook/tx/6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d" -H "accept: application/json" + * + */ + // GET handler for single transaction details. + async txSingle (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.' - }) - } - - // TODO: Add regex comparison of txid to ensure it's valid. - if (txid.length !== 64) { - res.status(400) - return res.json({ - error: `txid must be of length 64 (not ${txid.length})` - }) - } - - wlogger.debug('Executing blockbook/txSingle with this txid: ', txid) - - // Query the Blockbook Node API. - const retData = await transactionsFromBlockbook(txid) - - // 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/txSingle().', err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @api {post} /blockbook/tx Get details for an array of transactions. - * @apiName Details for an array of transactions - * @apiGroup Blockbook - * @apiDescription Return details for an array of transactions. - * Limited to 20 items per request. - * - * @apiExample Example usage: - * curl -X POST "https://mainnet.bchjs.cash/v3/blockbook/tx" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d","6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d"]}' - * - * - */ -// POST handler for bulk queries on tx details -async function txBulk (req, res, next) { - try { - let txids = req.body.txids - // const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0 - - // Reject if txids is not an array. - if (!Array.isArray(txids)) { - res.status(400) - return res.json({ - error: 'txids need to be an array. Use GET for single address.' - }) - } - - // 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.' - }) - } - - wlogger.debug('Executing blockbook.js/txBulk with these txids: ', txids) - - // Validate each element in the txids array. - for (let i = 0; i < txids.length; i++) { - const thisTxid = txids[i] - - if (!thisTxid || thisTxid === '') { + if (!txid || txid === '') { res.status(400) return res.json({ error: 'txid can not be empty' }) } - // TODO: Add regex comparison of txid to ensure it's valid. - if (thisTxid.length !== 64) { + // Reject if address is an array. + if (Array.isArray(txid)) { res.status(400) return res.json({ - error: `txid must be of length 64 (not ${thisTxid.length})` + error: 'txid can not be an array. Use POST for bulk upload.' }) } - } - // Loops through each address and creates an array of Promises, querying - // Insight API in parallel. - txids = txids.map(async (txid, index) => + // TODO: Add regex comparison of txid to ensure it's valid. + if (txid.length !== 64) { + res.status(400) + return res.json({ + error: `txid must be of length 64 (not ${txid.length})` + }) + } + + wlogger.debug('Executing blockbook/txSingle with this txid: ', txid) + + // Query the Blockbook Node API. + const retData = await _this.transactionsFromBlockbook(txid) + + // Return the retrieved address information. + res.status(200) + return res.json(retData) + } catch (err) { + // Write out error to error log. + wlogger.error('Error in blockbook.js/txSingle().', err) + + return _this.errorHandler(err, res) + } + } + + /** + * @api {post} /blockbook/tx Get details for an array of transactions. + * @apiName Details for an array of transactions + * @apiGroup Blockbook + * @apiDescription Return details for an array of transactions. + * Limited to 20 items per request. + * + * @apiExample Example usage: + * curl -X POST "https://mainnet.bchjs.cash/v3/blockbook/tx" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d","6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d"]}' + * + * + */ + // POST handler for bulk queries on tx details + async txBulk (req, res, next) { + try { + let txids = req.body.txids + // const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0 + + // Reject if txids is not an array. + if (!Array.isArray(txids)) { + res.status(400) + return res.json({ + error: 'txids need to be an array. Use GET for single address.' + }) + } + + // Enforce array size rate limits + if (!_this.routeUtils.validateArraySize(req, txids)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: 'Array too large.' + }) + } + + wlogger.debug('Executing blockbook.js/txBulk with these txids: ', txids) + + // Validate each element in the txids array. + for (let i = 0; i < txids.length; i++) { + const thisTxid = txids[i] + + if (!thisTxid || thisTxid === '') { + res.status(400) + return res.json({ error: 'txid can not be empty' }) + } + + // TODO: Add regex comparison of txid to ensure it's valid. + if (thisTxid.length !== 64) { + res.status(400) + return res.json({ + error: `txid must be of length 64 (not ${thisTxid.length})` + }) + } + } + + // Loops through each address and creates an array of Promises, querying + // Insight API in parallel. + txids = txids.map(async (txid, index) => // console.log(`address: ${address}`) - transactionsFromBlockbook(txid) - ) + _this.transactionsFromBlockbook(txid) + ) - // Wait for all parallel Insight requests to return. - const result = await axios.all(txids) + // Wait for all parallel Insight requests to return. + const result = await _this.axios.all(txids) - // 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 }) + // Return the array of retrieved address information. + res.status(200) + return res.json(result) + } catch (err) { + wlogger.error('Error in blockbook.js/txBulk().', err) + return _this.errorHandler(err, res) } - - wlogger.error('Error in blockbook.js/txBulk().', err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -module.exports = { - router, - testableComponents: { - root, - balanceSingle, - balanceBulk, - utxosSingle, - utxosBulk, - txSingle, - txBulk, - balanceFromBlockbook, - utxosFromBlockbook } } +module.exports = Blockbook diff --git a/src/routes/v3/util.js b/src/routes/v3/util.js index 7a41e83..abc6faf 100644 --- a/src/routes/v3/util.js +++ b/src/routes/v3/util.js @@ -6,7 +6,8 @@ const axios = require('axios') const routeUtils = require('./route-utils') const wlogger = require('../../util/winston-logging') -const blockbook = require('./blockbook') +const Blockbook = require('./blockbook') +const blockbook = new Blockbook() const util = require('util') util.inspect.defaultOptions = { depth: 1 } @@ -242,7 +243,7 @@ class UtilRoute { const fromAddr = bchjs.ECPair.toCashAddress(ecPair) // Get a balance on the public address - const balances = await _this.blockbook.testableComponents.balanceFromBlockbook( + const balances = await _this.blockbook.balanceFromBlockbook( fromAddr ) // console.log(`balances: ${JSON.stringify(balances, null, 2)}`) @@ -264,7 +265,7 @@ class UtilRoute { } // Get all UTXOs help by the address. - const utxos = await _this.blockbook.testableComponents.utxosFromBlockbook( + const utxos = await _this.blockbook.utxosFromBlockbook( fromAddr ) // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) diff --git a/test/v3/blockbook.js b/test/v3/blockbook.js index d9db4a5..7b4ac71 100644 --- a/test/v3/blockbook.js +++ b/test/v3/blockbook.js @@ -12,20 +12,20 @@ const chai = require('chai') const assert = chai.assert -const nock = require('nock') // HTTP mocking -let originalUrl, mockServerUrl // Used during transition from integration to unit tests. -// originalUrl = process.env.BLOCKBOOK_URL +const sinon = require('sinon') + +let originalUrl // Used during transition from integration to unit tests. // 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' } // Only load blockbook library after setting BLOCKBOOK_URL env var. -const blockbookRoute = require('../../src/routes/v3/blockbook') +const BlockbookRoute = require('../../src/routes/v3/blockbook') +const blockbookRoute = new BlockbookRoute() // Mocking data. const { mockReq, mockRes } = require('./mocks/express-mocks') @@ -37,7 +37,7 @@ util.inspect.defaultOptions = { depth: 1 } describe('#Blockbook Router', () => { let req, res - + let sandbox before(() => { // console.log(`Testing type is: ${process.env.TEST}`) @@ -55,14 +55,11 @@ describe('#Blockbook Router', () => { req.body = {} req.query = {} - // 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(() => { @@ -71,7 +68,7 @@ describe('#Blockbook Router', () => { describe('#root', () => { // root route handler. - const root = blockbookRoute.testableComponents.root + const root = blockbookRoute.root it('should respond to GET for base route', async () => { const result = root(req, res) @@ -82,10 +79,10 @@ describe('#Blockbook Router', () => { describe('#Balance Single', () => { // details route handler. - const balanceSingle = blockbookRoute.testableComponents.balanceSingle + // const balanceSingle = blockbookRoute.balanceSingle it('should throw 400 if address is empty', async () => { - const result = await balanceSingle(req, res) + const result = await blockbookRoute.balanceSingle(req, res) // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ['error']) @@ -95,7 +92,7 @@ describe('#Blockbook Router', () => { it('should error on an array', async () => { req.params.address = ['qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'] - const result = await balanceSingle(req, res) + const result = await blockbookRoute.balanceSingle(req, res) assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') assert.include( @@ -106,9 +103,10 @@ describe('#Blockbook Router', () => { }) it('should throw an error for an invalid address', async () => { - req.params.address = '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' + req.params.address = + '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' - const result = await balanceSingle(req, res) + const result = await blockbookRoute.balanceSingle(req, res) assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') assert.include( @@ -119,9 +117,10 @@ describe('#Blockbook Router', () => { }) it('should detect a network mismatch', async () => { - req.params.address = 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' + req.params.address = + 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' - const result = await balanceSingle(req, res) + const result = await blockbookRoute.balanceSingle(req, res) assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') assert.include(result.error, 'Invalid network', 'Proper error message') @@ -136,7 +135,7 @@ describe('#Blockbook Router', () => { // Switch the Insight URL to something that will error out. process.env.BLOCKBOOK_URL = 'http://fakeurl/api/' - const result = await balanceSingle(req, res) + const result = await blockbookRoute.balanceSingle(req, res) // Restore the saved URL. process.env.BLOCKBOOK_URL = savedUrl @@ -148,21 +147,60 @@ describe('#Blockbook Router', () => { process.env.BLOCKBOOK_URL = savedUrl } }) + it('returns proper error when downstream service stalls', async () => { + req.params.address = + 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + + // Mock the timeout error. + sandbox.stub(blockbookRoute.axios, 'request').throws({ + code: 'ECONNABORTED' + }) + + const result = await blockbookRoute.balanceSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') + assert.include( + result.error, + 'Could not communicate with full node', + 'Error message expected' + ) + }) + it('returns proper error when downstream service is down', async () => { + req.params.address = + 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + + // Mock the timeout error. + sandbox.stub(blockbookRoute.axios, 'request').throws({ + code: 'ECONNREFUSED' + }) + + const result = await blockbookRoute.balanceSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') + assert.include( + result.error, + 'Could not communicate with full node', + 'Error message expected' + ) + }) it('should get balance for a single address', async () => { - req.params.address = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + req.params.address = + 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' // console.log(`process.env.BLOCKBOOK_URL: ${process.env.BLOCKBOOK_URL}`) // 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) + sandbox.stub(blockbookRoute.axios, 'request').resolves({ + data: mockData.mockBalance + }) } // Call the details API. - const result = await balanceSingle(req, res) + const result = await blockbookRoute.balanceSingle(req, res) // console.log(`result: ${util.inspect(result)}`) assert.hasAnyKeys(result, [ @@ -184,7 +222,7 @@ describe('#Blockbook Router', () => { describe('#Balance Bulk', () => { // details route handler. - const balanceBulk = blockbookRoute.testableComponents.balanceBulk + const balanceBulk = blockbookRoute.balanceBulk it('should throw an error for an empty body', async () => { req.body = {} @@ -284,7 +322,44 @@ describe('#Blockbook Router', () => { process.env.BLOCKBOOK_URL = savedUrl } }) + it('returns proper error when downstream service stalls', async () => { + req.body = { + addresses: ['bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'] + } + // Mock the timeout error. + sandbox.stub(blockbookRoute.axios, 'request').throws({ + code: 'ECONNABORTED' + }) + const result = await balanceBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') + assert.include( + result.error, + 'Could not communicate with full node', + 'Error message expected' + ) + }) + it('returns proper error when downstream service is down', async () => { + req.body = { + addresses: ['bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'] + } + // Mock the timeout error. + sandbox.stub(blockbookRoute.axios, 'request').throws({ + code: 'ECONNREFUSED' + }) + + const result = await balanceBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') + assert.include( + result.error, + 'Could not communicate with full node', + 'Error message expected' + ) + }) it('should get details for a single address', async () => { req.body = { addresses: ['bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'] @@ -292,9 +367,9 @@ describe('#Blockbook Router', () => { // Mock the Insight URL for unit tests. if (process.env.TEST === 'unit') { - nock(mockServerUrl) - .get(uri => uri.includes('/')) - .reply(200, mockData.mockBalance) + sandbox.stub(blockbookRoute.axios, 'request').resolves({ + data: mockData.mockBalance + }) } // Call the details API. @@ -328,10 +403,9 @@ describe('#Blockbook Router', () => { // Mock the Insight URL for unit tests. if (process.env.TEST === 'unit') { - nock(mockServerUrl) - .get(uri => uri.includes('/')) - .times(2) - .reply(200, mockData.mockBalance) + sandbox.stub(blockbookRoute.axios, 'request').resolves({ + data: mockData.mockBalance + }) } // Call the details API. @@ -345,7 +419,7 @@ describe('#Blockbook Router', () => { describe('#UTXOs Single', () => { // details route handler. - const utxosSingle = blockbookRoute.testableComponents.utxosSingle + const utxosSingle = blockbookRoute.utxosSingle it('should throw 400 if address is empty', async () => { const result = await utxosSingle(req, res) @@ -382,7 +456,8 @@ describe('#Blockbook Router', () => { }) it('should detect a network mismatch', async () => { - req.params.address = 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' + req.params.address = + 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' const result = await utxosSingle(req, res) @@ -411,21 +486,58 @@ describe('#Blockbook Router', () => { process.env.BLOCKBOOK_URL = savedUrl } }) + it('returns proper error when downstream service stalls', async () => { + req.params.address = + 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7' + // Mock the timeout error. + sandbox.stub(blockbookRoute.axios, 'request').throws({ + code: 'ECONNABORTED' + }) + + const result = await utxosSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') + assert.include( + result.error, + 'Could not communicate with full node', + 'Error message expected' + ) + }) + it('returns proper error when downstream service is down', async () => { + req.params.address = + 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7' + + // Mock the timeout error. + sandbox.stub(blockbookRoute.axios, 'request').throws({ + code: 'ECONNREFUSED' + }) + + const result = await utxosSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') + assert.include( + result.error, + 'Could not communicate with full node', + 'Error message expected' + ) + }) it('should get utxos for a single address', async () => { req.params.address = 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7' // 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) + sandbox.stub(blockbookRoute.axios, 'request').resolves({ + data: mockData.mockUtxos + }) } // Call the details API. const result = await utxosSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) + // console.log(`result utxosSingle: ${util.inspect(result)}`) assert.isArray(result) assert.hasAnyKeys(result[0], [ @@ -440,7 +552,7 @@ describe('#Blockbook Router', () => { describe('#UTXO Bulk', () => { // details route handler. - const utxosBulk = blockbookRoute.testableComponents.utxosBulk + const utxosBulk = blockbookRoute.utxosBulk it('should throw an error for an empty body', async () => { req.body = {} @@ -540,7 +652,46 @@ describe('#Blockbook Router', () => { process.env.BLOCKBOOK_URL = savedUrl } }) + it('returns proper error when downstream service stalls', async () => { + req.body = { + addresses: ['bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'] + } + // Mock the timeout error. + sandbox.stub(blockbookRoute.axios, 'request').throws({ + code: 'ECONNABORTED' + }) + + const result = await utxosBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') + assert.include( + result.error, + 'Could not communicate with full node', + 'Error message expected' + ) + }) + it('returns proper error when downstream service is down', async () => { + req.body = { + addresses: ['bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'] + } + + // Mock the timeout error. + sandbox.stub(blockbookRoute.axios, 'request').throws({ + code: 'ECONNREFUSED' + }) + + const result = await utxosBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') + assert.include( + result.error, + 'Could not communicate with full node', + 'Error message expected' + ) + }) it('should get details for a single address', async () => { req.body = { addresses: ['bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'] @@ -548,9 +699,9 @@ describe('#Blockbook Router', () => { // Mock the Insight URL for unit tests. if (process.env.TEST === 'unit') { - nock(mockServerUrl) - .get(uri => uri.includes('/')) - .reply(200, mockData.mockUtxos) + sandbox.stub(blockbookRoute.axios, 'request').resolves({ + data: mockData.mockUtxos + }) } // Call the details API. @@ -578,10 +729,9 @@ describe('#Blockbook Router', () => { // Mock the Insight URL for unit tests. if (process.env.TEST === 'unit') { - nock(mockServerUrl) - .get(uri => uri.includes('/')) - .times(2) - .reply(200, mockData.mockUtxos) + sandbox.stub(blockbookRoute.axios, 'request').resolves({ + data: mockData.mockUtxos + }) } // Call the details API. @@ -596,7 +746,7 @@ describe('#Blockbook Router', () => { describe('#txSingle', () => { // route handler - const txSingle = blockbookRoute.testableComponents.txSingle + const txSingle = blockbookRoute.txSingle it('should throw 400 if txid is empty', async () => { const result = await txSingle(req, res) @@ -652,15 +802,54 @@ describe('#Blockbook Router', () => { process.env.BLOCKBOOK_URL = savedUrl } }) + it('returns proper error when downstream service stalls', async () => { + req.params.txid = + '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d' + + // Mock the timeout error. + sandbox.stub(blockbookRoute.axios, 'request').throws({ + code: 'ECONNABORTED' + }) + + const result = await txSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') + assert.include( + result.error, + 'Could not communicate with full node', + 'Error message expected' + ) + }) + it('returns proper error when downstream service is down', async () => { + req.params.txid = + '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d' + + // Mock the timeout error. + sandbox.stub(blockbookRoute.axios, 'request').throws({ + code: 'ECONNREFUSED' + }) + + const result = await txSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') + assert.include( + result.error, + 'Could not communicate with full node', + 'Error message expected' + ) + }) it('should get tx details for a single txid', async () => { - req.params.txid = '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d' + req.params.txid = + '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d' // Mock the Insight URL for unit tests. if (process.env.TEST === 'unit') { - nock(`${process.env.BLOCKBOOK_URL}`) - .get(uri => uri.includes('/')) - .reply(200, mockData.mockTx) + sandbox.stub(blockbookRoute.axios, 'request').resolves({ + data: mockData.mockTx + }) } // process.env.BLOCKBOOK_URL = `https://157.230.178.198:19131/` @@ -710,7 +899,7 @@ describe('#Blockbook Router', () => { describe('#txBulk', () => { // route handler - const txBulk = blockbookRoute.testableComponents.txBulk + const txBulk = blockbookRoute.txBulk it('should throw an error for an empty body', async () => { req.body = {} @@ -727,7 +916,8 @@ describe('#Blockbook Router', () => { }) it('should error on non-array single address', async () => { - req.body.txids = '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d' + req.body.txids = + '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d' const result = await txBulk(req, res) @@ -801,7 +991,50 @@ describe('#Blockbook Router', () => { process.env.BLOCKBOOK_URL = savedUrl } }) + it('returns proper error when downstream service stalls', async () => { + req.body = { + txids: [ + '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d' + ] + } + // Mock the timeout error. + sandbox.stub(blockbookRoute.axios, 'request').throws({ + code: 'ECONNABORTED' + }) + + const result = await txBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') + assert.include( + result.error, + 'Could not communicate with full node', + 'Error message expected' + ) + }) + it('returns proper error when downstream service is down', async () => { + req.body = { + txids: [ + '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d' + ] + } + + // Mock the timeout error. + sandbox.stub(blockbookRoute.axios, 'request').throws({ + code: 'ECONNREFUSED' + }) + + const result = await txBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.') + assert.include( + result.error, + 'Could not communicate with full node', + 'Error message expected' + ) + }) it('should get details for a single txid', async () => { req.body = { txids: [ @@ -811,9 +1044,9 @@ describe('#Blockbook Router', () => { // Mock the Insight URL for unit tests. if (process.env.TEST === 'unit') { - nock(mockServerUrl) - .get(uri => uri.includes('/')) - .reply(200, mockData.mockTx) + sandbox.stub(blockbookRoute.axios, 'request').resolves({ + data: mockData.mockTx + }) } // Call the details API. @@ -868,10 +1101,9 @@ describe('#Blockbook Router', () => { // Mock the Insight URL for unit tests. if (process.env.TEST === 'unit') { - nock(mockServerUrl) - .get(uri => uri.includes('/')) - .times(2) - .reply(200, mockData.mockTx) + sandbox.stub(blockbookRoute.axios, 'request').resolves({ + data: mockData.mockTx + }) } // Call the details API. diff --git a/test/v3/util.js b/test/v3/util.js index dcef3f8..4701e0d 100644 --- a/test/v3/util.js +++ b/test/v3/util.js @@ -357,14 +357,14 @@ describe('#Util', () => { sandbox .stub( - utilRouteInst.blockbook.testableComponents, + utilRouteInst.blockbook, 'balanceFromBlockbook' ) .resolves(mockData.mockBalance) sandbox .stub( - utilRouteInst.blockbook.testableComponents, + utilRouteInst.blockbook, 'utxosFromBlockbook' ) .resolves(mockData.mockUtxos) @@ -396,7 +396,7 @@ describe('#Util', () => { if (process.env.TEST === 'unit') { sandbox .stub( - utilRouteInst.blockbook.testableComponents, + utilRouteInst.blockbook, 'balanceFromBlockbook' ) .resolves(mockData.mockBalance) @@ -424,14 +424,14 @@ describe('#Util', () => { it('should generate transaction for valid BCH-only sweep', async () => { sandbox .stub( - utilRouteInst.blockbook.testableComponents, + utilRouteInst.blockbook, 'balanceFromBlockbook' ) .resolves(mockData.mockBalance) sandbox .stub( - utilRouteInst.blockbook.testableComponents, + utilRouteInst.blockbook, 'utxosFromBlockbook' ) .resolves(mockData.mockUtxos) @@ -461,14 +461,14 @@ describe('#Util', () => { it('should throw 422 error if no non-token UTXOs', async () => { sandbox .stub( - utilRouteInst.blockbook.testableComponents, + utilRouteInst.blockbook, 'balanceFromBlockbook' ) .resolves(mockData.mockBalance) sandbox .stub( - utilRouteInst.blockbook.testableComponents, + utilRouteInst.blockbook, 'utxosFromBlockbook' ) .resolves(mockData.mockUtxos) @@ -503,14 +503,14 @@ describe('#Util', () => { it('should detect and throw error for multiple token classes', async () => { sandbox .stub( - utilRouteInst.blockbook.testableComponents, + utilRouteInst.blockbook, 'balanceFromBlockbook' ) .resolves(mockData.mockBalance) sandbox .stub( - utilRouteInst.blockbook.testableComponents, + utilRouteInst.blockbook, 'utxosFromBlockbook' ) .resolves(mockData.mockThreeUtxos)