mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-21 16:52:04 -07:00
Revert "Revert "Blockchain refactor [WIP]""
This commit is contained in:
@@ -31,6 +31,7 @@ const utilV3 = require("./routes/v3/util")
|
|||||||
const slpV3 = require("./routes/v3/slp")
|
const slpV3 = require("./routes/v3/slp")
|
||||||
const xpubV3 = require("./routes/v3/xpub")
|
const xpubV3 = require("./routes/v3/xpub")
|
||||||
const blockbookV3 = require("./routes/v3/blockbook")
|
const blockbookV3 = require("./routes/v3/blockbook")
|
||||||
|
const Ninsight = require("./routes/v3/ninsight")
|
||||||
|
|
||||||
require("dotenv").config()
|
require("dotenv").config()
|
||||||
|
|
||||||
@@ -93,6 +94,9 @@ app.use(`/${v3prefix}/` + `slp`, slpV3.router)
|
|||||||
app.use(`/${v3prefix}/` + `xpub`, xpubV3.router)
|
app.use(`/${v3prefix}/` + `xpub`, xpubV3.router)
|
||||||
app.use(`/${v3prefix}/` + `blockbook`, blockbookV3.router)
|
app.use(`/${v3prefix}/` + `blockbook`, blockbookV3.router)
|
||||||
|
|
||||||
|
const ninsight = new Ninsight()
|
||||||
|
app.use(`/${v3prefix}/` + `ninsight`, ninsight.router)
|
||||||
|
|
||||||
// catch 404 and forward to error handler
|
// catch 404 and forward to error handler
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const err = {
|
const err = {
|
||||||
|
|||||||
@@ -56,18 +56,14 @@ function root(req, res, next) {
|
|||||||
*/
|
*/
|
||||||
async function getBestBlockHash(req, res, next) {
|
async function getBestBlockHash(req, res, next) {
|
||||||
try {
|
try {
|
||||||
const {
|
// Axios options
|
||||||
BitboxHTTP,
|
const options = routeUtils.getAxiosOptions()
|
||||||
username,
|
options.data.id = "getbestblockhash"
|
||||||
password,
|
options.data.method = "getbestblockhash"
|
||||||
requestConfig
|
options.data.params = []
|
||||||
} = routeUtils.setEnvVars()
|
|
||||||
|
|
||||||
requestConfig.data.id = "getbestblockhash"
|
const response = await axios.request(options)
|
||||||
requestConfig.data.method = "getbestblockhash"
|
|
||||||
requestConfig.data.params = []
|
|
||||||
|
|
||||||
const response = await BitboxHTTP(requestConfig)
|
|
||||||
return res.json(response.data.result)
|
return res.json(response.data.result)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Attempt to decode the error message.
|
// Attempt to decode the error message.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -17,7 +17,8 @@ module.exports = {
|
|||||||
validateNetwork, // Prevents a common user error
|
validateNetwork, // Prevents a common user error
|
||||||
setEnvVars, // Allows RPC variables to be set dynamically based on changing env vars.
|
setEnvVars, // Allows RPC variables to be set dynamically based on changing env vars.
|
||||||
decodeError, // Extract and interpret error messages.
|
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.
|
// 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.
|
// Dynamically set these based on env vars. Allows unit testing.
|
||||||
function setEnvVars() {
|
function setEnvVars() {
|
||||||
const BitboxHTTP = axios.create({
|
const BitboxHTTP = axios.create({
|
||||||
baseURL: process.env.RPC_BASEURL
|
baseURL: process.env.RPC_BASEURL,
|
||||||
|
timeout: 15000
|
||||||
})
|
})
|
||||||
const username = process.env.RPC_USERNAME
|
const username = process.env.RPC_USERNAME
|
||||||
const password = process.env.RPC_PASSWORD
|
const password = process.env.RPC_PASSWORD
|
||||||
@@ -93,6 +95,22 @@ function setEnvVars() {
|
|||||||
return { BitboxHTTP, username, password, requestConfig }
|
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 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 object returned by Axios. This function attempts to extract and interpret
|
||||||
// error messages.
|
// error messages.
|
||||||
@@ -113,6 +131,9 @@ function decodeError(err) {
|
|||||||
if (err.response && err.response.data)
|
if (err.response && err.response.data)
|
||||||
return { msg: err.response.data, status: err.response.status }
|
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.
|
// Attempt to detect a network connection error.
|
||||||
if (err.message && err.message.indexOf("ENOTFOUND") > -1) {
|
if (err.message && err.message.indexOf("ENOTFOUND") > -1) {
|
||||||
return {
|
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 }
|
return { msg: false, status: 500 }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
wlogger.error(`unhandled error in route-utils.js/decodeError(): `, err)
|
wlogger.error(`unhandled error in route-utils.js/decodeError(): `, err)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -115,6 +115,10 @@ describe("#BlockchainRouter", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// it("return proper error connection is refused", async () => {
|
||||||
|
//
|
||||||
|
// })
|
||||||
|
|
||||||
it("should GET /getBestBlockHash", async () => {
|
it("should GET /getBestBlockHash", async () => {
|
||||||
// Mock the RPC call for unit tests.
|
// Mock the RPC call for unit tests.
|
||||||
if (process.env.TEST === "unit") {
|
if (process.env.TEST === "unit") {
|
||||||
@@ -124,7 +128,7 @@ describe("#BlockchainRouter", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await getBestBlockHash(req, res)
|
const result = await getBestBlockHash(req, res)
|
||||||
//console.log(`result: ${util.inspect(result)}`)
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
assert.isString(result)
|
assert.isString(result)
|
||||||
assert.equal(result.length, 64, "Hash string is fixed length")
|
assert.equal(result.length, 64, "Hash string is fixed length")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user