Revert "Revert "Blockchain refactor [WIP]""

This commit is contained in:
Chris Troutner
2020-01-28 20:54:43 -08:00
committed by GitHub
parent 912cc9e537
commit 059667b41a
8 changed files with 1649 additions and 13 deletions
+4
View File
@@ -31,6 +31,7 @@ const utilV3 = require("./routes/v3/util")
const slpV3 = require("./routes/v3/slp")
const xpubV3 = require("./routes/v3/xpub")
const blockbookV3 = require("./routes/v3/blockbook")
const Ninsight = require("./routes/v3/ninsight")
require("dotenv").config()
@@ -93,6 +94,9 @@ app.use(`/${v3prefix}/` + `slp`, slpV3.router)
app.use(`/${v3prefix}/` + `xpub`, xpubV3.router)
app.use(`/${v3prefix}/` + `blockbook`, blockbookV3.router)
const ninsight = new Ninsight()
app.use(`/${v3prefix}/` + `ninsight`, ninsight.router)
// catch 404 and forward to error handler
app.use((req, res, next) => {
const err = {
+6 -10
View File
@@ -56,18 +56,14 @@ function root(req, res, next) {
*/
async function getBestBlockHash(req, res, next) {
try {
const {
BitboxHTTP,
username,
password,
requestConfig
} = routeUtils.setEnvVars()
// Axios options
const options = routeUtils.getAxiosOptions()
options.data.id = "getbestblockhash"
options.data.method = "getbestblockhash"
options.data.params = []
requestConfig.data.id = "getbestblockhash"
requestConfig.data.method = "getbestblockhash"
requestConfig.data.params = []
const response = await axios.request(options)
const response = await BitboxHTTP(requestConfig)
return res.json(response.data.result)
} catch (err) {
// Attempt to decode the error message.
+133
View File
@@ -0,0 +1,133 @@
/*
A library for interacting with the Full Node
*/
"use strict"
const express = require("express")
const router = express.Router()
const axios = require("axios")
const wlogger = require("../../../util/winston-logging")
const RouteUtils = require("../route-utils2")
const routeUtils = new RouteUtils()
// Used to convert error messages to strings, to safely pass to users.
const util = require("util")
util.inspect.defaultOptions = { depth: 1 }
const BCHJS = require("@chris.troutner/bch-js")
const bchjs = new BCHJS()
let _this
class Blockchain {
constructor() {
_this = this
this.bchjs = bchjs
this.axios = axios
this.routeUtils = routeUtils
this.router = router
this.router.get("/", this.root)
this.router.get("/getBestBlockHash", this.getBestBlockHash)
this.router.get("/getBlockchainInfo", this.getBlockchainInfo)
}
root(req, res, next) {
return res.json({ status: "blockchain" })
}
// DRY error handler.
errorHandler(err, res) {
// Attempt to decode the error message.
const { msg, status } = this.routeUtils.decodeError(err)
if (msg) {
res.status(status)
return res.json({ error: msg })
}
res.status(500)
return res.json({ error: util.inspect(err) })
}
/**
* @api {get} /blockchain/getBestBlockHash Get best block hash
* @apiName GetBestBlockHash
* @apiGroup Blockchain
* @apiDescription Returns the hash of the best (tip) block in the longest
* block chain.
*
* @apiExample Example usage:
* curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getBestBlockHash" -H "accept: application/json"
*
* @apiSuccess {String} bestBlockHash 000000000000000002bc884334336d99c9a9c616670a9244c6a8c1fc35aa91a1
*/
async getBestBlockHash(req, res, next) {
try {
// Axios options
const options = this.routeUtils.getAxiosOptions()
options.data.id = "getbestblockhash"
options.data.method = "getbestblockhash"
options.data.params = []
const response = await this.axios.request(options)
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
wlogger.error(`Error in blockchain.ts/getBestBlockHash().`, err)
return this.errorHandler(err, res)
}
}
/**
* @api {get} /blockchain/getBlockchainInfo Get blockchain info
* @apiName GetBlockchainInfo
* @apiGroup Blockchain
* @apiDescription Returns an object containing various state info regarding blockchain processing.
*
* @apiExample Example usage:
* curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getBlockchainInfo" -H "accept: application/json"
*
* @apiSuccess {Object} object Object containing data
* @apiSuccess {String} object.chain "main"
* @apiSuccess {Number} object.blocks 561838
* @apiSuccess {Number} object.headers 561838
* @apiSuccess {String} object.bestblockhash "000000000000000002307dd38cd01c7308b8febfcdf5772cf087b5bb023d55bc"
* @apiSuccess {Number} object.difficulty 246585566638.1496
* @apiSuccess {String} object.mediantime 1545402693
* @apiSuccess {Number} object.verificationprogress 0.999998831622689
* @apiSuccess {Boolean} object.chainwork "000000000000000000000000000000000000000000d8c09a8ab7262080266b3e"
* @apiSuccess {Number} object.pruned false
* @apiSuccess {Array} object.softforks Array of objects
* @apiSuccess {String} object.softforks.id "bip34"
* @apiSuccess {String} object.softforks.version 2
* @apiSuccess {Object} object.softforks.reject
* @apiSuccess {String} object.softforks.reject.status true
*/
async getBlockchainInfo(req, res, next) {
try {
// Axios options
const options = this.routeUtils.getAxiosOptions()
options.data.id = "getblockchaininfo"
options.data.method = "getblockchaininfo"
options.data.params = []
const response = await this.axios.request(options)
return res.json(response.data.result)
} catch (err) {
// Write out error to error log.
wlogger.error(`Error in blockchain.ts/getBlockchainInfo().`, err)
return this.errorHandler(err, res)
}
}
}
module.exports = Blockchain
+32
View File
@@ -0,0 +1,32 @@
/*
A library for interacting with the Bitcoin.com ninsight (not Insight) indexer.
*/
"use strict"
const express = require("express")
const axios = require("axios")
const routeUtils = require("./route-utils")
const wlogger = require("../../util/winston-logging")
const router = express.Router()
const BCHJS = require("@chris.troutner/bch-js")
const bchjs = new BCHJS()
let _this
class Ninsight {
constructor() {
_this = this
this.router = router
this.router.get("/", this.root)
}
root(req, res, next) {
return res.json({ status: "ninsight" })
}
}
module.exports = Ninsight
+35 -2
View File
@@ -17,7 +17,8 @@ module.exports = {
validateNetwork, // Prevents a common user error
setEnvVars, // Allows RPC variables to be set dynamically based on changing env vars.
decodeError, // Extract and interpret error messages.
validateArraySize // Ensure the passed array meets rate limiting requirements.
validateArraySize, // Ensure the passed array meets rate limiting requirements.
getAxiosOptions
}
// This function expects the Request Express.js object and an array as input.
@@ -74,7 +75,8 @@ function validateNetwork(addr) {
// Dynamically set these based on env vars. Allows unit testing.
function setEnvVars() {
const BitboxHTTP = axios.create({
baseURL: process.env.RPC_BASEURL
baseURL: process.env.RPC_BASEURL,
timeout: 15000
})
const username = process.env.RPC_USERNAME
const password = process.env.RPC_PASSWORD
@@ -93,6 +95,22 @@ function setEnvVars() {
return { BitboxHTTP, username, password, requestConfig }
}
// Axios options used when calling axios.post() to talk with a full node.
function getAxiosOptions() {
return {
method: "post",
baseURL: process.env.RPC_BASEURL,
timeout: 15000,
auth: {
username: process.env.RPC_USERNAME,
password: process.env.RPC_PASSWORD
},
data: {
jsonrpc: "1.0"
}
}
}
// Error messages returned by a full node can be burried pretty deep inside the
// error object returned by Axios. This function attempts to extract and interpret
// error messages.
@@ -113,6 +131,9 @@ function decodeError(err) {
if (err.response && err.response.data)
return { msg: err.response.data, status: err.response.status }
// console.log(`err.message: ${err.message}`)
// console.log(`err: `, err)
// Attempt to detect a network connection error.
if (err.message && err.message.indexOf("ENOTFOUND") > -1) {
return {
@@ -140,6 +161,18 @@ function decodeError(err) {
}
}
// Axios timeout (aborted) error, or service is down (connection refused).
if (
err.code &&
(err.code === "ECONNABORTED" || err.code === "ECONNREFUSED")
) {
return {
msg:
"Network error: Could not communicate with full node or other external service.",
status: 503
}
}
return { msg: false, status: 500 }
} catch (err) {
wlogger.error(`unhandled error in route-utils.js/decodeError(): `, err)
+163
View File
@@ -0,0 +1,163 @@
/*
A private library of utility functions used by several different routes.
*/
"use strict"
const axios = require("axios")
const wlogger = require("../../util/winston-logging")
const util = require("util")
util.inspect.defaultOptions = { depth: 1 }
const BCHJS = require("@chris.troutner/bch-js")
const bchjs = new BCHJS()
let _this
class RouteUtils {
constructor() {
_this = this
this.bchjs = bchjs
this.axios = axios
}
// This function expects the Request Express.js object and an array as input.
// The array is then validated against freemium and pro-tier rate limiting
// requirements. A boolean is returned to indicate if the array size if valid
// or not.
validateArraySize(req, array) {
const FREEMIUM_INPUT_SIZE = 20
const PRO_INPUT_SIZE = 20
if (req.locals && req.locals.proLimit) {
if (array.length <= PRO_INPUT_SIZE) return true
} else if (array.length <= FREEMIUM_INPUT_SIZE) {
return true
}
return false
}
// Axios options used when calling axios.post() to talk with a full node.
getAxiosOptions() {
return {
method: "post",
baseURL: process.env.RPC_BASEURL,
timeout: 15000,
auth: {
username: process.env.RPC_USERNAME,
password: process.env.RPC_PASSWORD
},
data: {
jsonrpc: "1.0"
}
}
}
// Returns true if user-provided cash address matches the correct network,
// mainnet or testnet. If NETWORK env var is not defined, it returns false.
// This prevent a common user-error issue that is easy to make: passing a
// testnet address into rest.bitcoin.com or passing a mainnet address into
// trest.bitcoin.com.
validateNetwork(addr) {
try {
const network = process.env.NETWORK
// Return false if NETWORK is not defined.
if (!network || network === "") {
console.log(`Warning: NETWORK environment variable is not defined!`)
return false
}
// Convert the user-provided address to a cashaddress, for easy detection
// of the intended network.
const cashAddr = this.bchjs.Address.toCashAddress(addr)
// Return true if the network and address both match testnet
const addrIsTest = this.bchjs.Address.isTestnetAddress(cashAddr)
if (network === "testnet" && addrIsTest) return true
// Return true if the network and address both match mainnet
const addrIsMain = this.bchjs.Address.isMainnetAddress(cashAddr)
if (network === "mainnet" && addrIsMain) return true
return false
} catch (err) {
logger.error(`Error in validateNetwork()`)
return false
}
}
// Error messages returned by a full node can be burried pretty deep inside the
// error object returned by Axios. This function attempts to extract and interpret
// error messages.
// Returns an object. If successful, obj.msg is a string.
// If there is a failure, obj.msg is false.
decodeError(err) {
try {
// Attempt to extract the full node error message.
if (
err.response &&
err.response.data &&
err.response.data.error &&
err.response.data.error.message
)
return { msg: err.response.data.error.message, status: 400 }
// Attempt to extract the Insight error message
if (err.response && err.response.data)
return { msg: err.response.data, status: err.response.status }
// console.log(`err.message: ${err.message}`)
// console.log(`err: `, err)
// Attempt to detect a network connection error.
if (err.message && err.message.indexOf("ENOTFOUND") > -1) {
return {
msg:
"Network error: Could not communicate with full node or other external service.",
status: 503
}
}
// Different kind of network error
if (err.message && err.message.indexOf("ENETUNREACH") > -1) {
return {
msg:
"Network error: Could not communicate with full node or other external service.",
status: 503
}
}
// Different kind of network error
if (err.message && err.message.indexOf("EAI_AGAIN") > -1) {
return {
msg:
"Network error: Could not communicate with full node or other external service.",
status: 503
}
}
// Axios timeout (aborted) error, or service is down (connection refused).
if (
err.code &&
(err.code === "ECONNABORTED" || err.code === "ECONNREFUSED")
) {
return {
msg:
"Network error: Could not communicate with full node or other external service.",
status: 503
}
}
return { msg: false, status: 500 }
} catch (err) {
wlogger.error(`unhandled error in route-utils.js/decodeError(): `, err)
return { msg: false, status: 500 }
}
}
}
module.exports = RouteUtils
+5 -1
View File
@@ -115,6 +115,10 @@ describe("#BlockchainRouter", () => {
)
})
// it("return proper error connection is refused", async () => {
//
// })
it("should GET /getBestBlockHash", async () => {
// Mock the RPC call for unit tests.
if (process.env.TEST === "unit") {
@@ -124,7 +128,7 @@ describe("#BlockchainRouter", () => {
}
const result = await getBestBlockHash(req, res)
//console.log(`result: ${util.inspect(result)}`)
// console.log(`result: ${util.inspect(result)}`)
assert.isString(result)
assert.equal(result.length, 64, "Hash string is fixed length")
File diff suppressed because it is too large Load Diff