Merge pull request #22 from christroutner/insight

Adding Insight routes back to v3
This commit is contained in:
Chris Troutner
2019-07-01 20:49:09 -07:00
committed by GitHub
9 changed files with 3624 additions and 3 deletions
-3
View File
@@ -6,9 +6,6 @@ This is a fork and alternative implementation of
the [rest.bitcoin.com](https://github.com/Bitcoin-com/rest.bitcoin.com) repository.
The function of this code is to create a REST API server to provide a common
interface for working with a BCH full node and various indexers.
This repository is intended to be used for my own personal use, but it's licensed
under the MIT license like the original rest repo was, so people are free to use
it and modify it as they see fit.
This repository is intended to be paired with my alternative implementation
of [BITBOX SDK](https://github.com/Bitcoin-com/bitbox-sdk):
+12
View File
@@ -56,6 +56,12 @@ const rawtransactionsV3 = require("./routes/v3/rawtransactions")
const utilV3 = require("./routes/v3/util")
const slpV3 = require("./routes/v3/slp")
const xpubV3 = require("./routes/v3/xpub")
const bitcoreV3 = require("./routes/v3/bitcore")
const blockbookV3 = require("./routes/v3/blockbook")
const insightV3 = require("./routes/v3/insight")
const insightBlockV3 = require("./routes/v3/insight/block")
const insightTranactionV3 = require("./routes/v3/insight/transaction")
const insightAddressV3 = require("./routes/v3/insight/address")
require("dotenv").config()
@@ -135,6 +141,12 @@ app.use(`/${v3prefix}/` + `rawtransactions`, rawtransactionsV3.router)
app.use(`/${v3prefix}/` + `util`, utilV3.router)
app.use(`/${v3prefix}/` + `slp`, slpV3.router)
app.use(`/${v3prefix}/` + `xpub`, xpubV3.router)
app.use(`/${v3prefix}/` + `bitcore`, bitcoreV3.router)
app.use(`/${v3prefix}/` + `blockbook`, blockbookV3.router)
app.use(`/${v3prefix}/` + `insight`, insightV3.router)
app.use(`/${v3prefix}/` + `insight/block`, insightBlockV3.router)
app.use(`/${v3prefix}/` + `insight/transaction`, insightTranactionV3.router)
app.use(`/${v3prefix}/` + `insight/address`, insightAddressV3.router)
// catch 404 and forward to error handler
app.use((req, res, next) => {
+792
View File
@@ -0,0 +1,792 @@
/*
Address 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()
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.`
})
}
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."
})
}
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.`
})
}
}
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."
})
}
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" })
}
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."
})
}
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" })
}
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."
})
}
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."
})
}
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
}
}
+279
View File
@@ -0,0 +1,279 @@
"use strict"
const express = require("express")
const requestUtils = require("../services/requestUtils")
const axios = require("axios")
const bitbox = require("../services/bitbox")
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.`
})
}
wlogger.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
}
}
+38
View File
@@ -0,0 +1,38 @@
/*
Insight API route
A parent route that encompases the other libraries.
*/
"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)
// Root API endpoint. Simply acknowledges that it exists.
function root(req, res, next) {
return res.json({ status: "address" })
}
module.exports = {
router,
testableComponents: {
root
}
}
+187
View File
@@ -0,0 +1,187 @@
"use strict"
const express = require("express")
const router = express.Router()
const axios = require("axios")
const routeUtils = require("../route-utils")
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.`
})
}
wlogger.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."
})
}
wlogger.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
}
}
+1373
View File
File diff suppressed because it is too large Load Diff
+610
View File
@@ -0,0 +1,610 @@
"use strict"
const blockRoute = require("../../src/routes/v3/insight/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")
})
})
})
+333
View File
@@ -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/insight/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)
})
})
})