mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-21 16:52:04 -07:00
Merge pull request #22 from christroutner/insight
Adding Insight routes back to v3
This commit is contained in:
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user