From 13b41a50c678659772db391f6ec1694804a81e4f Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 4 Feb 2020 16:12:38 -0800 Subject: [PATCH 01/30] Editing comments and notes --- package-lock.json | 64 ++++++++++++++++++++++++++ package.json | 6 ++- src/app.js | 5 ++- src/middleware/auth.js | 3 ++ src/middleware/jwt-auth.js | 2 + src/middleware/route-ratelimit.js | 2 +- test/v3/rate-limits.js | 74 ------------------------------- 7 files changed, 78 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index c00d725..9608aa0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2256,6 +2256,11 @@ "wrap-ansi": "^5.1.0" } }, + "cluster-key-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", + "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -3041,6 +3046,11 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -5690,6 +5700,22 @@ "p-is-promise": "^3.0.0" } }, + "ioredis": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.14.1.tgz", + "integrity": "sha512-94W+X//GHM+1GJvDk6JPc+8qlM7Dul+9K+lg3/aHixPN7ZGkW6qlvX0DG6At9hWtH2v3B32myfZqWoANUJYGJA==", + "requires": { + "cluster-key-slot": "^1.1.0", + "debug": "^4.1.1", + "denque": "^1.1.0", + "lodash.defaults": "^4.2.0", + "lodash.flatten": "^4.4.0", + "redis-commands": "1.5.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.0.1" + } + }, "ipaddr.js": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", @@ -6473,12 +6499,22 @@ "integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk=", "dev": true }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, "lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=", "dev": true }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, "lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -12165,6 +12201,11 @@ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, + "rate-limiter-flexible": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-1.3.2.tgz", + "integrity": "sha512-f+xNvGn+52G4nZVok9VB3LTE1kfDmqbnWKRayX5n2k/LEQ7doWrYvzmzFVh7ltmLgwEdEzoYwxGaXlfCjFg4Ag==" + }, "raw-body": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", @@ -12332,6 +12373,24 @@ "esprima": "~4.0.0" } }, + "redis-commands": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz", + "integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg==" + }, + "redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" + }, + "redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "requires": { + "redis-errors": "^1.0.0" + } + }, "referrer-policy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz", @@ -13628,6 +13687,11 @@ } } }, + "standard-as-callback": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.0.1.tgz", + "integrity": "sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg==" + }, "standard-engine": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-8.0.1.tgz", diff --git a/package.json b/package.json index 8d848f3..7ea61fb 100644 --- a/package.json +++ b/package.json @@ -15,15 +15,15 @@ "test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/blockchain.js", "test:integration": "mocha test/v3/integration", "coverage": "nyc report --reporter=text-lcov | coveralls", - "coverage:report": "export NETWORK=testnet && nyc --reporter=html mocha test/v2/", + "coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v3/", "docs": "./node_modules/.bin/apidoc -i src/routes/v3 -o docs" }, "engines": { "node": ">=10.15.1" }, "dependencies": { - "apidoc": "^0.20.0", "@chris.troutner/bch-js": "^2.0.0", + "apidoc": "^0.20.0", "axios": "^0.19.0", "body-parser": "^1.18.3", "cookie-parser": "~1.4.3", @@ -34,6 +34,7 @@ "express-basic-auth": "^1.1.3", "express-rate-limit": "^5.0.0", "helmet": "^3.21.2", + "ioredis": "^4.14.1", "level": "^6.0.0", "mkdirp": "^1.0.0", "mocha": "^7.0.1", @@ -44,6 +45,7 @@ "passport-http": "^0.3.0", "pg": "^7.11.0", "pg-hstore": "^2.3.2", + "rate-limiter-flexible": "^1.3.2", "strftime": "^0.10.0", "winston": "^3.2.1", "winston-daily-rotate-file": "^4.0.0" diff --git a/src/app.js b/src/app.js index 5e4dc52..95597a8 100644 --- a/src/app.js +++ b/src/app.js @@ -78,11 +78,14 @@ const v3prefix = "v3" app.use(`/${v3prefix}/`, jwtAuth.getTokenFromHeaders) // Instantiate the authorization middleware, used to implement pro-tier rate limiting. +// Handles Anonymous and Basic Authorization schemes used by passport.js const auth = new AuthMW() app.use(`/${v3prefix}/`, auth.mw()) // Rate limit on all v3 routes -app.use(`/${v3prefix}/`, routeRateLimit) // Establish and enforce rate limits. +// Establish and enforce rate limits. +app.use(`/${v3prefix}/`, routeRateLimit) + app.use(`/${v3prefix}/` + `health-check`, healthCheckV3) app.use(`/${v3prefix}/` + `blockchain`, blockchainV3.router) app.use(`/${v3prefix}/` + `control`, controlV3.router) diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 307a583..77510ff 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -1,4 +1,7 @@ /* + CT 2/4/20 Note: This library handles anonymous and Basic auth. This library + can be phased out with the chage to JWT tokens and the new rate-limit library. + Handle authorization for bypassing rate limits. 1) Default is 'Anonymous Authentication', which unlocks the freemimum tier by diff --git a/src/middleware/jwt-auth.js b/src/middleware/jwt-auth.js index 6880ea0..27b90cd 100644 --- a/src/middleware/jwt-auth.js +++ b/src/middleware/jwt-auth.js @@ -1,4 +1,6 @@ /* + This is a middleware library for handling and processing JWT tokens. + This middleware inspects the request header for a JWT token. If found, will populate req.locals.jwtToken with the JWT token. */ diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index cdad7a6..3dd8b6a 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -159,7 +159,7 @@ const routeRateLimit = async function(req, res, next) { uniqueRateLimits[route](req, res, next) } -// This function returns the an object with proLimit and apiLevel properties. +// This function returns an object with proLimit and apiLevel properties. // It does fine-grane analysis on the data coming from the auth servers and // uses its output to adjust rate limits on-the-fly based on the users // permission level. diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js index 50d7ca6..3753ccc 100644 --- a/test/v3/rate-limits.js +++ b/test/v3/rate-limits.js @@ -71,80 +71,6 @@ describe("#route-ratelimits & jwt-auth", () => { assert.equal(req.locals.jwtToken, jwt) }) }) - - // TODO: This code has been refactored and these unit tests no longer apply. - // I think I forgot to create new unit tests to reflect the change in code though. - /* - describe("#routeAccess", () => { - it("should do nothing if req.locals.jwtToken is undefined", () => { - // Initialize req.locals - req.locals = { - proLimit: false, - apiLevel: 0 - } - req.url = "/insight/address/details" - - // Reset the history of the stub and assert it has not been called. - res.status.resetHistory() - assert.equal(res.status.called, false, "stub history reset") - - jwtAuth.routeAccess(req, res, next) - - // console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`) - assert.equal( - res.status.called, - false, - "stub should NOT have been called." - ) - }) - - it("should throw error if full-node tier tries to access indexer", () => { - // Initialize req.locals - req.locals = { - proLimit: true, - apiLevel: 10, - jwtToken: jwt - } - req.url = "/insight/address/details" - - try { - res.status.resetHistory() - assert.equal(res.status.called, false, "stub history reset") - - jwtAuth.routeAccess(req, res, next) - - assert.equal(res.status.called, true, "stub should have been called.") - } catch (err) { - console.log(`caught error: `, err) - } - }) - - it("should allow indexer tier tries access indexer endpoints", () => { - // Initialize req.locals - req.locals = { - proLimit: true, - apiLevel: 20, - jwtToken: jwt - } - req.url = "/insight/address/details" - - try { - res.status.resetHistory() - assert.equal(res.status.called, false, "stub history reset") - - jwtAuth.routeAccess(req, res, next) - - assert.equal( - res.status.called, - false, - "stub should NOT have been called." - ) - } catch (err) { - console.log(`caught error: `, err) - } - }) - }) - */ }) describe("#routeRateLimit", () => { From 5ecc321b3e958bdab881bcf6b90620587bf7620f Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 4 Feb 2020 16:35:31 -0800 Subject: [PATCH 02/30] fix(rate-limit.js): Refactored to use JS Class --- src/app.js | 6 +- src/middleware/route-ratelimit.js | 304 +++++++++++++++-------------- test/v3/integration/rate-limits.js | 2 +- test/v3/rate-limits.js | 33 ++-- 4 files changed, 181 insertions(+), 164 deletions(-) diff --git a/src/app.js b/src/app.js index 95597a8..1a972f0 100644 --- a/src/app.js +++ b/src/app.js @@ -3,7 +3,9 @@ const express = require("express") // Middleware -const { routeRateLimit } = require("./middleware/route-ratelimit") +// const { routeRateLimit } = require("./middleware/route-ratelimit") +const RateLimits = require("./middleware/route-ratelimit") +const rateLimits = new RateLimits() const path = require("path") const logger = require("morgan") @@ -84,7 +86,7 @@ app.use(`/${v3prefix}/`, auth.mw()) // Rate limit on all v3 routes // Establish and enforce rate limits. -app.use(`/${v3prefix}/`, routeRateLimit) +app.use(`/${v3prefix}/`, rateLimits.routeRateLimit) app.use(`/${v3prefix}/` + `health-check`, healthCheckV3) app.use(`/${v3prefix}/` + `blockchain`, blockchainV3.router) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index 3dd8b6a..f8f3935 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -32,169 +32,177 @@ const maxRequests = process.env.RATE_LIMIT_MAX_REQUESTS // Unique route mapped to its rate limit const uniqueRateLimits = {} -const routeRateLimit = async function(req, res, next) { - // Disable rate limiting if 0 passed from RATE_LIMIT_MAX_REQUESTS - if (maxRequests === 0) return next() +let _this - // Create a res.locals object if not passed in. - if (!req.locals) { - req.locals = { - // default values - jwtToken: "", - proLimit: false, - apiLevel: 0 - } +class RateLimits { + constructor() { + _this = this } - // Warn if JWT_AUTH_SERVER env var is not set. - const authServer = process.env.JWT_AUTH_SERVER - if (!authServer || authServer === "") { - console.warn( - "JWT_AUTH_SERVER env var is not set. JWT tokens not being evaluated." - ) - } else { - // If a JWT token is passed in, validate it and enable pro-tier rate limits - // if it's valid. - if (req.locals.jwtToken) { - // console.log(`req.locals.jwtToken: ${req.locals.jwtToken}`) + async routeRateLimit(req, res, next) { + // Disable rate limiting if 0 passed from RATE_LIMIT_MAX_REQUESTS + if (maxRequests === 0) return next() - // URL for the auth server. - const path = `${authServer}apitoken/isvalid/${req.locals.jwtToken}` - - // Ask Auth server if the JWT token is valid. - // Get the API level for this user. - let jwtInfo = await axios.get(path) - jwtInfo = jwtInfo.data - // console.log(`jwtInfo: ${JSON.stringify(jwtInfo, null, 2)}`) - - // If JWT if valid, evaluate the API level for the user. - if (jwtInfo.isValid) { - // Set fine-grain permissions for each user based on the JWT token. - const userPermissions = evalUserPermissioins(req, jwtInfo) - // console.log( - // `userPermissions: ${JSON.stringify(userPermissions, null, 2)}` - // ) - - req.locals.proLimit = userPermissions.proLimit - req.locals.apiLevel = userPermissions.apiLevel + // Create a res.locals object if not passed in. + if (!req.locals) { + req.locals = { + // default values + jwtToken: "", + proLimit: false, + apiLevel: 0 } } - } - // Current route - const rateLimitTier = req.locals.proLimit ? "PRO" : "BASIC" - const path = req.baseUrl + req.path + // Warn if JWT_AUTH_SERVER env var is not set. + const authServer = process.env.JWT_AUTH_SERVER + if (!authServer || authServer === "") { + console.warn( + "JWT_AUTH_SERVER env var is not set. JWT tokens not being evaluated." + ) + } else { + // If a JWT token is passed in, validate it and enable pro-tier rate limits + // if it's valid. + if (req.locals.jwtToken) { + // console.log(`req.locals.jwtToken: ${req.locals.jwtToken}`) - // Create a unique string as a route identifier. - const route = - rateLimitTier + - req.method + - req.locals.apiLevel + // Generates new rate limit when user upgrades JWT token. - path - .split("/") - .slice(0, 4) - .join("/") - //console.log(`route identifier: ${JSON.stringify(route, null, 2)}`) + // URL for the auth server. + const path = `${authServer}apitoken/isvalid/${req.locals.jwtToken}` - // console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`) + // Ask Auth server if the JWT token is valid. + // Get the API level for this user. + let jwtInfo = await axios.get(path) + jwtInfo = jwtInfo.data + // console.log(`jwtInfo: ${JSON.stringify(jwtInfo, null, 2)}`) - // This boolean value is passed from the auth.js middleware. - const proRateLimits = req.locals.proLimit + // If JWT if valid, evaluate the API level for the user. + if (jwtInfo.isValid) { + // Set fine-grain permissions for each user based on the JWT token. + const userPermissions = _this.evalUserPermissioins(req, jwtInfo) + // console.log( + // `userPermissions: ${JSON.stringify(userPermissions, null, 2)}` + // ) - // console.log(`proRateLimits: ${proRateLimits}`) - - // Pro level rate limits - if (proRateLimits || proRateLimits === 0) { - // TODO: replace the console.logs with calls to our logging system. - // console.log(`applying pro-rate limits`) - - let PRO_RPM = 10 // Default value for free tier - if (req.locals.apiLevel > 0) PRO_RPM = 100 // RPM for paid tiers. - - // console.log(`PRO_RPM: ${PRO_RPM}, apiLevel: ${req.locals.apiLevel}`) - - // Create new RateLimit if none exists for this route - if (!uniqueRateLimits[route]) { - uniqueRateLimits[route] = new RateLimit({ - windowMs: 60 * 1000, // 1 minute window - delayMs: 0, // disable delaying - full speed until the max limit is reached - max: PRO_RPM, // start blocking after this many requests per minute - handler: function(req, res) { - //console.log(`pro-tier rate-handler triggered.`) - - res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 - return res.json({ - error: `Too many requests. Limits are ${PRO_RPM} requests per minute. Increase rate limits at https://account.bchjs.cash` - }) + req.locals.proLimit = userPermissions.proLimit + req.locals.apiLevel = userPermissions.apiLevel } - }) - } - - // Freemium level rate limits - } else { - // TODO: replace the console.logs with calls to our logging system. - // console.log(`applying freemium limits`) - - // Create new RateLimit if none exists for this route - if (!uniqueRateLimits[route]) { - uniqueRateLimits[route] = new RateLimit({ - windowMs: 60 * 1000, // 1 minute window - delayMs: 0, // disable delaying - full speed until the max limit is reached - max: maxRequests, // start blocking after maxRequests - handler: function(req, res) { - //console.log(`freemium rate-handler triggered.`) - - res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 - return res.json({ - error: `Too many requests. Your limits are currently ${maxRequests} requests per minute. Increase rate limits at https://account.bchjs.cash` - }) - } - }) - } - } - - //console.log(`calling uniqueRateLimits() on this route: ${route}`) - - // Call rate limit for this route - uniqueRateLimits[route](req, res, next) -} - -// This function returns an object with proLimit and apiLevel properties. -// It does fine-grane analysis on the data coming from the auth servers and -// uses its output to adjust rate limits on-the-fly based on the users -// permission level. -function evalUserPermissioins(req, authData) { - // console.log(`authData: ${JSON.stringify(authData, null, 2)}`) - - // Return object with default values - const retObj = { - proLimit: authData.isValid, - apiLevel: authData.apiLevel - } - - // if apiLevel = 0 (free tier), then return the default values. - if (retObj.apiLevel === 0) return retObj - - const level20Routes = ["insight", "bitcore", "blockbook"] - - const locals = req.locals - // console.log(`locals: ${JSON.stringify(locals, null, 2)}`) - const url = req.url - // console.log(`url: ${JSON.stringify(url, null, 2)}`) - - if (authData.apiLevel < 20) { - // Loop through the routes that are not accessible to this tier. - for (let i = 0; i < level20Routes.length; i++) { - // If the requested route is for a higher tier, - // revert to anonymous level permissions. - if (url.indexOf(level20Routes[i]) > -1) { - retObj.proLimit = false - retObj.apiLevel = 0 } } + + // Current route + const rateLimitTier = req.locals.proLimit ? "PRO" : "BASIC" + const path = req.baseUrl + req.path + + // Create a unique string as a route identifier. + const route = + rateLimitTier + + req.method + + req.locals.apiLevel + // Generates new rate limit when user upgrades JWT token. + path + .split("/") + .slice(0, 4) + .join("/") + //console.log(`route identifier: ${JSON.stringify(route, null, 2)}`) + + // console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`) + + // This boolean value is passed from the auth.js middleware. + const proRateLimits = req.locals.proLimit + + // console.log(`proRateLimits: ${proRateLimits}`) + + // Pro level rate limits + if (proRateLimits || proRateLimits === 0) { + // TODO: replace the console.logs with calls to our logging system. + // console.log(`applying pro-rate limits`) + + let PRO_RPM = 10 // Default value for free tier + if (req.locals.apiLevel > 0) PRO_RPM = 100 // RPM for paid tiers. + + // console.log(`PRO_RPM: ${PRO_RPM}, apiLevel: ${req.locals.apiLevel}`) + + // Create new RateLimit if none exists for this route + if (!uniqueRateLimits[route]) { + uniqueRateLimits[route] = new RateLimit({ + windowMs: 60 * 1000, // 1 minute window + delayMs: 0, // disable delaying - full speed until the max limit is reached + max: PRO_RPM, // start blocking after this many requests per minute + handler: function(req, res) { + //console.log(`pro-tier rate-handler triggered.`) + + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Too many requests. Limits are ${PRO_RPM} requests per minute. Increase rate limits at https://account.bchjs.cash` + }) + } + }) + } + + // Freemium level rate limits + } else { + // TODO: replace the console.logs with calls to our logging system. + // console.log(`applying freemium limits`) + + // Create new RateLimit if none exists for this route + if (!uniqueRateLimits[route]) { + uniqueRateLimits[route] = new RateLimit({ + windowMs: 60 * 1000, // 1 minute window + delayMs: 0, // disable delaying - full speed until the max limit is reached + max: maxRequests, // start blocking after maxRequests + handler: function(req, res) { + //console.log(`freemium rate-handler triggered.`) + + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Too many requests. Your limits are currently ${maxRequests} requests per minute. Increase rate limits at https://account.bchjs.cash` + }) + } + }) + } + } + + //console.log(`calling uniqueRateLimits() on this route: ${route}`) + + // Call rate limit for this route + uniqueRateLimits[route](req, res, next) } - return retObj + // This function returns an object with proLimit and apiLevel properties. + // It does fine-grane analysis on the data coming from the auth servers and + // uses its output to adjust rate limits on-the-fly based on the users + // permission level. + evalUserPermissioins(req, authData) { + // console.log(`authData: ${JSON.stringify(authData, null, 2)}`) + + // Return object with default values + const retObj = { + proLimit: authData.isValid, + apiLevel: authData.apiLevel + } + + // if apiLevel = 0 (free tier), then return the default values. + if (retObj.apiLevel === 0) return retObj + + const level20Routes = ["insight", "bitcore", "blockbook"] + + const locals = req.locals + // console.log(`locals: ${JSON.stringify(locals, null, 2)}`) + const url = req.url + // console.log(`url: ${JSON.stringify(url, null, 2)}`) + + if (authData.apiLevel < 20) { + // Loop through the routes that are not accessible to this tier. + for (let i = 0; i < level20Routes.length; i++) { + // If the requested route is for a higher tier, + // revert to anonymous level permissions. + if (url.indexOf(level20Routes[i]) > -1) { + retObj.proLimit = false + retObj.apiLevel = 0 + } + } + } + + return retObj + } } -module.exports = { routeRateLimit } +module.exports = RateLimits diff --git a/test/v3/integration/rate-limits.js b/test/v3/integration/rate-limits.js index 99ba139..4142369 100644 --- a/test/v3/integration/rate-limits.js +++ b/test/v3/integration/rate-limits.js @@ -18,7 +18,7 @@ util.inspect.defaultOptions = { depth: 1 } const SERVER = `http://localhost:3000/v3/` const TEST_JWT = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYTc5ZDk4OTYyMjRjNjM2MmQwYzkwMiIsImlhdCI6MTU3MTUzOTU1MSwiZXhwIjoxNTc0MTMxNTUxfQ.PfPW_Z2NYT1O2zUHXopcz2aLGHSGudaKOIGnt7SuAi4" + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlM2EwNDE1ZWIyOWE5NjJkYTI3MDhiNCIsImFwaUxldmVsIjowLCJyYXRlTGltaXQiOjEwLCJpYXQiOjE1ODA4NjA0NjcsImV4cCI6MTU4MzQ1MjQ2N30.fuY5S-YrF0J11h5uyMjPe7wiVkYRnIyXi4dL9-V-C6pLJm33p0dSq_pSheVVWw78n5kAvL_9kFHngbnmQiOJYQ" describe("#rate limits", () => { it("should get control/getNetworkInfo() with no auth", async () => { diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js index 3753ccc..3ded94a 100644 --- a/test/v3/rate-limits.js +++ b/test/v3/rate-limits.js @@ -12,7 +12,10 @@ util.inspect.defaultOptions = { depth: 1 } const { mockReq, mockRes, mockNext } = require("./mocks/express-mocks") // Libraries under test -let rateLimitMiddleware = require("../../src/middleware/route-ratelimit") +const RateLimits = require("../../src/middleware/route-ratelimit") +const rateLimits = new RateLimits() +let rateLimitMiddleware = rateLimits.routeRateLimit + const controlRoute = require("../../src/routes/v3/full-node/control") const jwtAuth = require("../../src/middleware/jwt-auth") @@ -74,6 +77,7 @@ describe("#route-ratelimits & jwt-auth", () => { }) describe("#routeRateLimit", () => { + rateLimitMiddleware = new RateLimits() let routeRateLimit = rateLimitMiddleware.routeRateLimit const getInfo = controlRoute.testableComponents.getInfo @@ -112,10 +116,11 @@ describe("#route-ratelimits & jwt-auth", () => { it("should NOT trigger rate-limit for free-tier at 5 RPM", async () => { // Clear the require cache before running this test. - delete require.cache[ - require.resolve("../../src/middleware/route-ratelimit") - ] - rateLimitMiddleware = require("../../src/middleware/route-ratelimit") + // delete require.cache[ + // require.resolve("../../src/middleware/route-ratelimit") + // ] + // rateLimitMiddleware = require("../../src/middleware/route-ratelimit") + rateLimitMiddleware = new RateLimits() routeRateLimit = rateLimitMiddleware.routeRateLimit req.baseUrl = "/v3" @@ -167,10 +172,11 @@ describe("#route-ratelimits & jwt-auth", () => { it("should NOT trigger rate-limit handler for pro-tier at 25 RPM", async () => { // Clear the require cache before running this test. - delete require.cache[ - require.resolve("../../src/middleware/route-ratelimit") - ] - rateLimitMiddleware = require("../../src/middleware/route-ratelimit") + // delete require.cache[ + // require.resolve("../../src/middleware/route-ratelimit") + // ] + // rateLimitMiddleware = require("../../src/middleware/route-ratelimit") + rateLimitMiddleware = new RateLimits() routeRateLimit = rateLimitMiddleware.routeRateLimit req.baseUrl = "/v3" @@ -199,10 +205,11 @@ describe("#route-ratelimits & jwt-auth", () => { it("rate-limiting should still kick in at a higher RPM for pro-tier", async () => { // Clear the require cache before running this test. - delete require.cache[ - require.resolve("../../src/middleware/route-ratelimit") - ] - rateLimitMiddleware = require("../../src/middleware/route-ratelimit") + // delete require.cache[ + // require.resolve("../../src/middleware/route-ratelimit") + // ] + // rateLimitMiddleware = require("../../src/middleware/route-ratelimit") + rateLimitMiddleware = new RateLimits() routeRateLimit = rateLimitMiddleware.routeRateLimit req.baseUrl = "/v3" From dcf8455162810f9078cc80b9c5b3ad270c5b5af2 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 4 Feb 2020 17:11:19 -0800 Subject: [PATCH 03/30] Decoded JWT token --- package-lock.json | 125 +++++++++++++++++++++++++++++- package.json | 2 + src/app.js | 3 +- src/middleware/route-ratelimit.js | 45 +++++++++++ 4 files changed, 170 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9608aa0..d278d6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -731,11 +731,27 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@types/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", + "requires": { + "@types/node": "*" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, + "@types/elliptic": { + "version": "6.4.12", + "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.12.tgz", + "integrity": "sha512-gP1KsqoouLJGH6IJa28x7PXb3cRqh83X8HCLezd2dF+XcAIMKYv53KV+9Zn6QA561E120uOqZBQ+Jy/cl+fviw==", + "requires": { + "@types/bn.js": "*" + } + }, "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", @@ -1158,6 +1174,17 @@ "safer-buffer": "~2.1.0" } }, + "asn1.js": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.3.0.tgz", + "integrity": "sha512-WHnQJFcOrIWT1RLOkFFBQkFVvyt9BPOOrH+Dp152Zk4R993rSzXUGPmkybIcUFhHE2d/iHH+nCaOWVCDbO8fgA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "assert": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz", @@ -1870,6 +1897,11 @@ "ieee754": "^1.1.4" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-equals": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz", @@ -3241,6 +3273,14 @@ "safer-buffer": "^2.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ecurve": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/ecurve/-/ecurve-1.0.6.tgz", @@ -6270,6 +6310,30 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -6297,6 +6361,25 @@ "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", "dev": true }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "keccak": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", @@ -6308,6 +6391,17 @@ "safe-buffer": "^5.1.0" } }, + "key-encoder": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/key-encoder/-/key-encoder-2.0.3.tgz", + "integrity": "sha512-fgBtpAGIr/Fy5/+ZLQZIPPhsZEcbSlYu/Wu96tNDFNSjSACw5lEIOFeaVdQ/iwrb8oxjlWi6wmWdH76hV6GZjg==", + "requires": { + "@types/elliptic": "^6.4.9", + "asn1.js": "^5.0.1", + "bn.js": "^4.11.8", + "elliptic": "^6.4.1" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -6526,23 +6620,46 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, "lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", "dev": true }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", - "dev": true + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" }, "lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", - "dev": true + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, "lodash.set": { "version": "4.3.2", diff --git a/package.json b/package.json index 7ea61fb..e093f99 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "express-rate-limit": "^5.0.0", "helmet": "^3.21.2", "ioredis": "^4.14.1", + "jsonwebtoken": "^8.5.1", + "key-encoder": "^2.0.3", "level": "^6.0.0", "mkdirp": "^1.0.0", "mocha": "^7.0.1", diff --git a/src/app.js b/src/app.js index 1a972f0..778c3cc 100644 --- a/src/app.js +++ b/src/app.js @@ -86,7 +86,8 @@ app.use(`/${v3prefix}/`, auth.mw()) // Rate limit on all v3 routes // Establish and enforce rate limits. -app.use(`/${v3prefix}/`, rateLimits.routeRateLimit) +// app.use(`/${v3prefix}/`, rateLimits.routeRateLimit) +app.use(`/${v3prefix}/`, rateLimits.newRateLimit) app.use(`/${v3prefix}/` + `health-check`, healthCheckV3) app.use(`/${v3prefix}/` + `blockchain`, blockchainV3.router) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index f8f3935..71dbe31 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -21,6 +21,10 @@ const express = require("express") const RateLimit = require("express-rate-limit") const axios = require("axios") +const jwt = require("jsonwebtoken") +const KeyEncoder = require("key-encoder").default +const keyEncoder = new KeyEncoder("secp256k1") + // Set max requests per minute const maxRequests = process.env.RATE_LIMIT_MAX_REQUESTS ? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) @@ -203,6 +207,47 @@ class RateLimits { return retObj } + + newRateLimit(req, res, next) { + try { + // Create a res.locals object if not passed in. + if (!req.locals) { + req.locals = { + // default values + jwtToken: "", + proLimit: false, + apiLevel: 0 + } + } + + if (req.locals.jwtToken) { + // Hexadecimal + const publicKey = + "03e6c358092a459f7da9420de770eef3e16cf3c9c54a3d3d14ac2d7f0b82af4d7d" + + const jwtOptions = { + algorithms: ["ES256"] + } + + const pemPublicKey = keyEncoder.encodePublic(publicKey, "raw", "pem") + + // Validate the JWT token. + const decoded = jwt.verify( + req.locals.jwtToken, + pemPublicKey, + jwtOptions + ) + console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`) + } else { + console.log(`No JWT token found!`) + } + } catch (err) { + console.error(`Error in route-ratelimit.js/newRateLimit(): `, err) + // throw err + } + + next() + } } module.exports = RateLimits From 88e22de3ae11bb6a2c6d17db009c2a78acbb1e5b Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 5 Feb 2020 16:33:35 -0800 Subject: [PATCH 04/30] New rate limit working with tiers --- src/middleware/route-ratelimit.js | 36 ++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index 71dbe31..d950c1b 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -25,6 +25,18 @@ const jwt = require("jsonwebtoken") const KeyEncoder = require("key-encoder").default const keyEncoder = new KeyEncoder("secp256k1") +// Redis +const Redis = require("ioredis") +const redisClient = new Redis({ enableOfflineQueue: false }) + +// Rate limiter middleware lib. +const { RateLimiterRedis } = require("rate-limiter-flexible") +const rateLimitOptions = { + storeClient: redisClient, + points: 100, // Number of points + duration: 1 // Per second +} + // Set max requests per minute const maxRequests = process.env.RATE_LIMIT_MAX_REQUESTS ? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) @@ -41,6 +53,8 @@ let _this class RateLimits { constructor() { _this = this + + this.rateLimiter = new RateLimiterRedis(rateLimitOptions) } async routeRateLimit(req, res, next) { @@ -208,8 +222,10 @@ class RateLimits { return retObj } - newRateLimit(req, res, next) { + async newRateLimit(req, res, next) { try { + let userId + // Create a res.locals object if not passed in. if (!req.locals) { req.locals = { @@ -238,9 +254,27 @@ class RateLimits { jwtOptions ) console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`) + + userId = decoded.id } else { console.log(`No JWT token found!`) } + + try { + // https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users + const key = userId ? userId : req.ip + const pointsToConsume = userId ? 1 : 30 + + console.log(`User ${userId} consuming ${pointsToConsume} points.`) + + await this.rateLimiter.consume(key, pointToConsume) + } catch (err) { + // Rate limited was triggered + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Too many requests. Your limits are currently ${maxRequests} requests per minute. Increase rate limits at https://account.bchjs.cash` + }) + } } catch (err) { console.error(`Error in route-ratelimit.js/newRateLimit(): `, err) // throw err From c7f7c68fac4d85b72391b1b26069285a5d7f075c Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 6 Feb 2020 13:19:38 -0800 Subject: [PATCH 05/30] feat(resource rate-limiting): point-based consumption based on resource usage --- src/middleware/route-ratelimit.js | 134 +++++++++++++++++++++++------- 1 file changed, 106 insertions(+), 28 deletions(-) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index d950c1b..c29c896 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -1,20 +1,3 @@ -/* - This file controls the request-per-minute (RPM) rate limits. - - It is assumed that this middleware is run AFTER the jwt-auth.js and auth.js - middleware. - - Current rate limiting rules in requests-per-minute: - - anonymous access: 3 - - free access: 10, apiLevel = 0 - - any paid tier: 100, apiLevel > 0 - - If a person signs up for full node access but not indexer access, then the - apiLevel will be 10. If they call an endpoint that uses an indexer, the apiLevel - will be downgraded to 0 on-the-fly. Indexer endpoints will effectively be - downgraded to the anonymous access tier. -*/ - "use strict" const express = require("express") @@ -57,6 +40,23 @@ class RateLimits { this.rateLimiter = new RateLimiterRedis(rateLimitOptions) } + /* + This function controls the tierd request-per-minute (RPM) rate limits. + This is an older implementation that is currently not used. + + It is assumed that this middleware is run AFTER the jwt-auth.js and auth.js + middleware. + + Current rate limiting rules in requests-per-minute: + - anonymous access: 3 + - free access: 10, apiLevel = 0 + - any paid tier: 100, apiLevel > 0 + + If a person signs up for full node access but not indexer access, then the + apiLevel will be 10. If they call an endpoint that uses an indexer, the apiLevel + will be downgraded to 0 on-the-fly. Indexer endpoints will effectively be + downgraded to the anonymous access tier. + */ async routeRateLimit(req, res, next) { // Disable rate limiting if 0 passed from RATE_LIMIT_MAX_REQUESTS if (maxRequests === 0) return next() @@ -222,9 +222,16 @@ class RateLimits { return retObj } + /* + This is the new rate limit function that uses the rate-limiter-flexible npm + library. For the moment, it only distinguishes between anonymous usage + and registered user access. anon usage allows up to 3 RPM, whereas registered + users (with JWT tokens) have 100 RPM. + */ async newRateLimit(req, res, next) { try { let userId + let decoded // Create a res.locals object if not passed in. if (!req.locals) { @@ -248,27 +255,36 @@ class RateLimits { const pemPublicKey = keyEncoder.encodePublic(publicKey, "raw", "pem") // Validate the JWT token. - const decoded = jwt.verify( - req.locals.jwtToken, - pemPublicKey, - jwtOptions - ) - console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`) + decoded = jwt.verify(req.locals.jwtToken, pemPublicKey, jwtOptions) + // console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`) userId = decoded.id } else { console.log(`No JWT token found!`) } + // Code here for the rate limiter is adapted from this example: + // https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users try { - // https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users - const key = userId ? userId : req.ip - const pointsToConsume = userId ? 1 : 30 + // The resource being consumed: full node, indexer, SLPDB, etc. + const resource = _this.getResource(req.url) + console.log(`resource: ${resource}`) - console.log(`User ${userId} consuming ${pointsToConsume} points.`) + let key = userId ? userId : req.ip + key = `${key}-${resource}` - await this.rateLimiter.consume(key, pointToConsume) + // const pointsToConsume = userId ? 1 : 30 + decoded.resource = resource + const pointsToConsume = _this.calcPoints(decoded) + + console.log( + `User ${userId} consuming ${pointsToConsume} point for resource ${resource}.` + ) + + await _this.rateLimiter.consume(key, pointsToConsume) } catch (err) { + console.log(`err: `, err) + // Rate limited was triggered res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 return res.json({ @@ -282,6 +298,68 @@ class RateLimits { next() } + + // Calculates the points consumed, based on the jwt information and the route + // requested. + calcPoints(jwtInfo) { + let retVal = 30 // By default, use anonymous tier. + + try { + // console.log(`jwtInfo: ${JSON.stringify(jwtInfo, null, 2)}`) + + const apiLevel = jwtInfo.apiLevel + const resource = jwtInfo.resource + + const level20Routes = ["insight", "bitcore", "blockbook"] + const level30Routes = ["slp"] + + // Only evaluate if user is using a JWT token. + if (jwtInfo.id) { + // SLP indexer routes + if (level30Routes.includes(resource)) { + if (apiLevel >= 30) retVal = 1 + else retVal = 30 + } + + // Normal indexer routes + else if (level20Routes.includes(resource)) { + if (apiLevel >= 20) retVal = 1 + else retVal = 30 + } + + // Free tier, full node only. + else { + retVal = 1 + } + } + + return retVal + } catch (err) { + console.error(`Error in route-ratelimit.js/calcPoints()`) + // throw err + retVal = 30 + } + + return retVal + } + + // This function parses the req.url property to identify what resource + // the user is requesting. + // This was created as a function so that it can be unit tested. Not sure + // what kind of variations will be seen in production. + getResource(url) { + try { + console.log(`url: ${JSON.stringify(url, null, 2)}`) + + const splitUrl = url.split("/") + const resource = splitUrl[1] + + return resource + } catch (err) { + console.error(`Error in getResource().`) + throw err + } + } } module.exports = RateLimits From 86951eccacb43bb6348800fa9839067b44700e36 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 6 Feb 2020 14:32:59 -0800 Subject: [PATCH 06/30] Disconnecting from Redis after unit tests complete --- src/middleware/route-ratelimit.js | 4 ++++ test/v3/rate-limits.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index c29c896..7554acd 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -40,6 +40,10 @@ class RateLimits { this.rateLimiter = new RateLimiterRedis(rateLimitOptions) } + closeRedis() { + redisClient.disconnect() + } + /* This function controls the tierd request-per-minute (RPM) rate limits. This is an older implementation that is currently not used. diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js index 3ded94a..79f4fb1 100644 --- a/test/v3/rate-limits.js +++ b/test/v3/rate-limits.js @@ -52,6 +52,10 @@ describe("#route-ratelimits & jwt-auth", () => { req.query = {} }) + after(() => { + rateLimits.closeRedis() + }) + describe("#jwt-auth.js", () => { describe("#getTokenFromHeaders", () => { it(`should populate the req.locals object correctly`, () => { From 8a558db690ba8176e9fb1b7d7959b3e28ce01923 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 6 Feb 2020 14:51:09 -0800 Subject: [PATCH 07/30] unit test for getResource() --- src/middleware/route-ratelimit.js | 8 ++++---- test/v3/rate-limits.js | 12 ++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index 7554acd..961ea82 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -40,6 +40,8 @@ class RateLimits { this.rateLimiter = new RateLimiterRedis(rateLimitOptions) } + // Used to disconnect from the Redis DB. + // Called by unit tests so that node.js thread doesn't live forever. closeRedis() { redisClient.disconnect() } @@ -228,9 +230,7 @@ class RateLimits { /* This is the new rate limit function that uses the rate-limiter-flexible npm - library. For the moment, it only distinguishes between anonymous usage - and registered user access. anon usage allows up to 3 RPM, whereas registered - users (with JWT tokens) have 100 RPM. + library. */ async newRateLimit(req, res, next) { try { @@ -353,7 +353,7 @@ class RateLimits { // what kind of variations will be seen in production. getResource(url) { try { - console.log(`url: ${JSON.stringify(url, null, 2)}`) + // console.log(`url: ${JSON.stringify(url, null, 2)}`) const splitUrl = url.split("/") const resource = splitUrl[1] diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js index 79f4fb1..db237cb 100644 --- a/test/v3/rate-limits.js +++ b/test/v3/rate-limits.js @@ -240,6 +240,18 @@ describe("#route-ratelimits & jwt-auth", () => { ) }) }) + + describe("#getResource", () => { + it("should decode a blockchain request", () => { + const url = + "/blockchain/getTxOut/62a3ea958a463a372bc0caf2c374a7f60be9c624be63a0db8db78f05809df6d8/0?include_mempool=true" + + const result = rateLimits.getResource(url) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result, "blockchain") + }) + }) }) // Generates a Basic authorization header. From 4c80776754247db7f6832d7fba3eafac84fa23cb Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 00:05:40 -0800 Subject: [PATCH 08/30] unit tests for calcPoints() --- src/middleware/route-ratelimit.js | 17 ++-- test/v3/rate-limits.js | 124 ++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 6 deletions(-) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index 961ea82..9373b86 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -235,7 +235,7 @@ class RateLimits { async newRateLimit(req, res, next) { try { let userId - let decoded + let decoded = {} // Create a res.locals object if not passed in. if (!req.locals) { @@ -275,16 +275,18 @@ class RateLimits { console.log(`resource: ${resource}`) let key = userId ? userId : req.ip - key = `${key}-${resource}` // const pointsToConsume = userId ? 1 : 30 decoded.resource = resource const pointsToConsume = _this.calcPoints(decoded) console.log( - `User ${userId} consuming ${pointsToConsume} point for resource ${resource}.` + `User ${key} consuming ${pointsToConsume} point for resource ${resource}.` ) + // Update the key so that rate limits track both the user and the resource. + key = `${key}-${resource}` + await _this.rateLimiter.consume(key, pointsToConsume) } catch (err) { console.log(`err: `, err) @@ -317,18 +319,21 @@ class RateLimits { const level20Routes = ["insight", "bitcore", "blockbook"] const level30Routes = ["slp"] + // console.log(`apiLevel: ${apiLevel}`) + // Only evaluate if user is using a JWT token. if (jwtInfo.id) { // SLP indexer routes if (level30Routes.includes(resource)) { if (apiLevel >= 30) retVal = 1 - else retVal = 30 + // else if (apiLevel >= 10) retVal = 10 + else retVal = 10 } // Normal indexer routes else if (level20Routes.includes(resource)) { if (apiLevel >= 20) retVal = 1 - else retVal = 30 + else retVal = 10 } // Free tier, full node only. @@ -353,7 +358,7 @@ class RateLimits { // what kind of variations will be seen in production. getResource(url) { try { - // console.log(`url: ${JSON.stringify(url, null, 2)}`) + console.log(`url: ${JSON.stringify(url, null, 2)}`) const splitUrl = url.split("/") const resource = splitUrl[1] diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js index db237cb..892d732 100644 --- a/test/v3/rate-limits.js +++ b/test/v3/rate-limits.js @@ -252,6 +252,130 @@ describe("#route-ratelimits & jwt-auth", () => { assert.equal(result, "blockchain") }) }) + + describe("#calcPoints", () => { + it("should return 30 points for anonymous user", () => { + const result = rateLimits.calcPoints() + // console.log(`result: ${result}`) + + assert.equal(result, 30) + }) + + it("should return 1 point for free tier requesting full node access", () => { + const jwtInfo = { + apiLevel: 10, + resource: "blockchain", + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 1) + }) + + it("should return 10 points for free tier requesting indexer access", () => { + const jwtInfo = { + apiLevel: 10, + resource: "blockbook", + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 10) + }) + + it("should return 10 points for free tier requesting SLPDB access", () => { + const jwtInfo = { + apiLevel: 10, + resource: "slp", + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 10) + }) + + it("should return 1 point for indexer tier requesting full node access", () => { + const jwtInfo = { + apiLevel: 20, + resource: "blockchain", + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 1) + }) + + it("should return 1 points for indexer tier requesting indexer access", () => { + const jwtInfo = { + apiLevel: 20, + resource: "blockbook", + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 1) + }) + + it("should return 10 points for indexer tier requesting SLPDB access", () => { + const jwtInfo = { + apiLevel: 20, + resource: "slp", + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 10) + }) + + it("should return 1 point for SLP tier requesting full node access", () => { + const jwtInfo = { + apiLevel: 30, + resource: "blockchain", + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 1) + }) + + it("should return 1 points for SLP tier requesting indexer access", () => { + const jwtInfo = { + apiLevel: 30, + resource: "blockbook", + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 1) + }) + + it("should return 1 points for SLP tier requesting SLPDB access", () => { + const jwtInfo = { + apiLevel: 30, + resource: "slp", + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 1) + }) + }) + + describe("#newRateLimit", () => { + it("should pass through rate-limit middleware", async () => { + req.baseUrl = "/v3" + req.path = "/control/getNetworkInfo" + req.url = req.path + req.method = "GET" + + // Call the route twice to trigger the rate handling. + await rateLimits.newRateLimit(req, res, next) + await rateLimits.newRateLimit(req, res, next) + + // next() will be called if rate-limit is not triggered + assert.equal(next.called, true) + }) + }) }) // Generates a Basic authorization header. From 97e49180e3165cee2df80567cc49317ab693f2b5 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 00:29:39 -0800 Subject: [PATCH 09/30] Need to fix slp tier as 40 --- src/middleware/route-ratelimit.js | 16 ++++- test/v3/rate-limits.js | 106 +++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 6 deletions(-) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index 9373b86..bdfadf5 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -37,6 +37,7 @@ class RateLimits { constructor() { _this = this + this.jwt = jwt this.rateLimiter = new RateLimiterRedis(rateLimitOptions) } @@ -259,7 +260,11 @@ class RateLimits { const pemPublicKey = keyEncoder.encodePublic(publicKey, "raw", "pem") // Validate the JWT token. - decoded = jwt.verify(req.locals.jwtToken, pemPublicKey, jwtOptions) + decoded = _this.jwt.verify( + req.locals.jwtToken, + pemPublicKey, + jwtOptions + ) // console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`) userId = decoded.id @@ -289,7 +294,7 @@ class RateLimits { await _this.rateLimiter.consume(key, pointsToConsume) } catch (err) { - console.log(`err: `, err) + // console.log(`err: `, err) // Rate limited was triggered res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 @@ -336,9 +341,14 @@ class RateLimits { else retVal = 10 } + // Full node tier + else if (apiLevel >= 10) { + retVal = 1 + } + // Free tier, full node only. else { - retVal = 1 + retVal = 10 } } diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js index 892d732..889972d 100644 --- a/test/v3/rate-limits.js +++ b/test/v3/rate-limits.js @@ -3,6 +3,7 @@ const chai = require("chai") const assert = chai.assert const nock = require("nock") // HTTP mocking +const sinon = require("sinon") // Used for debugging. const util = require("util") @@ -13,7 +14,7 @@ const { mockReq, mockRes, mockNext } = require("./mocks/express-mocks") // Libraries under test const RateLimits = require("../../src/middleware/route-ratelimit") -const rateLimits = new RateLimits() +let rateLimits = new RateLimits() let rateLimitMiddleware = rateLimits.routeRateLimit const controlRoute = require("../../src/routes/v3/full-node/control") @@ -26,6 +27,8 @@ let originalEnvVars // Used during transition from integration to unit tests. const jwt = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYWRlM2Y1NzM5ZTZjMGZmMDM0YjlhMSIsImlhdCI6MTU3MTY3NzQ1MCwiZXhwIjoxNTc0MjY5NDUwfQ.SSz7F7ETyBB3eoNG2VKCzPOhddtB-vrtmEoj7PxicrQ` describe("#route-ratelimits & jwt-auth", () => { + let sandbox + before(() => { // Save existing environment variables. originalEnvVars = { @@ -50,6 +53,12 @@ describe("#route-ratelimits & jwt-auth", () => { req.params = {} req.body = {} req.query = {} + + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() }) after(() => { @@ -261,7 +270,7 @@ describe("#route-ratelimits & jwt-auth", () => { assert.equal(result, 30) }) - it("should return 1 point for free tier requesting full node access", () => { + it("should return 10 points for free tier requesting full node access", () => { const jwtInfo = { apiLevel: 10, resource: "blockchain", @@ -269,7 +278,7 @@ describe("#route-ratelimits & jwt-auth", () => { } const result = rateLimits.calcPoints(jwtInfo) - assert.equal(result, 1) + assert.equal(result, 10) }) it("should return 10 points for free tier requesting indexer access", () => { @@ -375,6 +384,97 @@ describe("#route-ratelimits & jwt-auth", () => { // next() will be called if rate-limit is not triggered assert.equal(next.called, true) }) + + it("should trigger rate-limit handler if rate limits exceeds 5 request per minute", async () => { + req.baseUrl = "/v3" + req.path = "/control/getNetworkInfo" + req.url = req.path + req.method = "GET" + + for (let i = 0; i < 5; i++) { + next.reset() // reset the stubbed next() function. + + await rateLimits.newRateLimit(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + false, + `next should not be called if rate limit was triggered.` + ) + }) + + it("should NOT trigger rate-limit for free-tier at 5 RPM", async () => { + // Create a new instance of the rate limit so we start with zeroed tracking. + rateLimits = new RateLimits() + + req.baseUrl = "/v3" + req.path = "/control/getNetworkInfo" + req.url = req.path + req.method = "GET" + + req.locals.jwtToken = "some-token" + + const jwtInfo = { + apiLevel: 10, + id: "5e3a0415eb29a962da2708b4" + } + + // Mock the call to the jwt library. + sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo) + + for (let i = 0; i < 5; i++) { + next.reset() // reset the stubbed next() function. + + await rateLimits.newRateLimit(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + //console.log(`req.locals after test: ${util.inspect(req.locals)}`) + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + true, + `next should be called if rate limit was not triggered.` + ) + }) + + it("should trigger rate-limit for free tier after 10 RPM", async () => { + // Create a new instance of the rate limit so we start with zeroed tracking. + rateLimits = new RateLimits() + + req.baseUrl = "/v3" + req.path = "/control/getNetworkInfo" + req.url = req.path + req.method = "GET" + + req.locals.jwtToken = "some-token" + + const jwtInfo = { + apiLevel: 10, + id: "5e3a0415eb29a962da2708b4" + } + + // Mock the call to the jwt library. + sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo) + + for (let i = 0; i < 12; i++) { + next.reset() // reset the stubbed next() function. + + await rateLimits.newRateLimit(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + false, + `next should not be called if rate limit was triggered.` + ) + }) }) }) From c70bbd46c589d6fbe1b34e9cebad593f6de422e5 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 16:20:37 -0800 Subject: [PATCH 10/30] Getting ready to refactor another tier --- src/app.js | 2 +- src/middleware/route-ratelimit.js | 2 +- test/v3/rate-limits.js | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app.js b/src/app.js index 778c3cc..82014dc 100644 --- a/src/app.js +++ b/src/app.js @@ -87,7 +87,7 @@ app.use(`/${v3prefix}/`, auth.mw()) // Rate limit on all v3 routes // Establish and enforce rate limits. // app.use(`/${v3prefix}/`, rateLimits.routeRateLimit) -app.use(`/${v3prefix}/`, rateLimits.newRateLimit) +app.use(`/${v3prefix}/`, rateLimits.rateLimitByResource) app.use(`/${v3prefix}/` + `health-check`, healthCheckV3) app.use(`/${v3prefix}/` + `blockchain`, blockchainV3.router) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index bdfadf5..ffa76a6 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -233,7 +233,7 @@ class RateLimits { This is the new rate limit function that uses the rate-limiter-flexible npm library. */ - async newRateLimit(req, res, next) { + async rateLimitByResource(req, res, next) { try { let userId let decoded = {} diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js index 889972d..98e7ff9 100644 --- a/test/v3/rate-limits.js +++ b/test/v3/rate-limits.js @@ -370,7 +370,7 @@ describe("#route-ratelimits & jwt-auth", () => { }) }) - describe("#newRateLimit", () => { + describe("#rateLimitByResource", () => { it("should pass through rate-limit middleware", async () => { req.baseUrl = "/v3" req.path = "/control/getNetworkInfo" @@ -378,8 +378,8 @@ describe("#route-ratelimits & jwt-auth", () => { req.method = "GET" // Call the route twice to trigger the rate handling. - await rateLimits.newRateLimit(req, res, next) - await rateLimits.newRateLimit(req, res, next) + await rateLimits.rateLimitByResource(req, res, next) + await rateLimits.rateLimitByResource(req, res, next) // next() will be called if rate-limit is not triggered assert.equal(next.called, true) @@ -394,7 +394,7 @@ describe("#route-ratelimits & jwt-auth", () => { for (let i = 0; i < 5; i++) { next.reset() // reset the stubbed next() function. - await rateLimits.newRateLimit(req, res, next) + await rateLimits.rateLimitByResource(req, res, next) //console.log(`next() called: ${next.called}`) } @@ -428,7 +428,7 @@ describe("#route-ratelimits & jwt-auth", () => { for (let i = 0; i < 5; i++) { next.reset() // reset the stubbed next() function. - await rateLimits.newRateLimit(req, res, next) + await rateLimits.rateLimitByResource(req, res, next) //console.log(`next() called: ${next.called}`) } @@ -464,7 +464,7 @@ describe("#route-ratelimits & jwt-auth", () => { for (let i = 0; i < 12; i++) { next.reset() // reset the stubbed next() function. - await rateLimits.newRateLimit(req, res, next) + await rateLimits.rateLimitByResource(req, res, next) //console.log(`next() called: ${next.called}`) } From f518ad4a3e0b5957f4094530c7e2c63e4679d807 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 16:30:20 -0800 Subject: [PATCH 11/30] Refactored for four tiers --- src/middleware/route-ratelimit.js | 14 ++++----- test/v3/rate-limits.js | 47 ++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index ffa76a6..cbd47fd 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -321,28 +321,28 @@ class RateLimits { const apiLevel = jwtInfo.apiLevel const resource = jwtInfo.resource - const level20Routes = ["insight", "bitcore", "blockbook"] - const level30Routes = ["slp"] + const level30Routes = ["insight", "bitcore", "blockbook"] + const level40Routes = ["slp"] // console.log(`apiLevel: ${apiLevel}`) // Only evaluate if user is using a JWT token. if (jwtInfo.id) { // SLP indexer routes - if (level30Routes.includes(resource)) { - if (apiLevel >= 30) retVal = 1 + if (level40Routes.includes(resource)) { + if (apiLevel >= 40) retVal = 1 // else if (apiLevel >= 10) retVal = 10 else retVal = 10 } // Normal indexer routes - else if (level20Routes.includes(resource)) { - if (apiLevel >= 20) retVal = 1 + else if (level30Routes.includes(resource)) { + if (apiLevel >= 30) retVal = 1 else retVal = 10 } // Full node tier - else if (apiLevel >= 10) { + else if (apiLevel >= 20) { retVal = 1 } diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js index 98e7ff9..fdf021a 100644 --- a/test/v3/rate-limits.js +++ b/test/v3/rate-limits.js @@ -303,7 +303,7 @@ describe("#route-ratelimits & jwt-auth", () => { assert.equal(result, 10) }) - it("should return 1 point for indexer tier requesting full node access", () => { + it("should return 1 point for full node tier requesting full node access", () => { const jwtInfo = { apiLevel: 20, resource: "blockchain", @@ -314,7 +314,7 @@ describe("#route-ratelimits & jwt-auth", () => { assert.equal(result, 1) }) - it("should return 1 points for indexer tier requesting indexer access", () => { + it("should return 10 points for full-node tier requesting indexer access", () => { const jwtInfo = { apiLevel: 20, resource: "blockbook", @@ -322,10 +322,10 @@ describe("#route-ratelimits & jwt-auth", () => { } const result = rateLimits.calcPoints(jwtInfo) - assert.equal(result, 1) + assert.equal(result, 10) }) - it("should return 10 points for indexer tier requesting SLPDB access", () => { + it("should return 10 points for full node tier requesting SLPDB access", () => { const jwtInfo = { apiLevel: 20, resource: "slp", @@ -336,7 +336,7 @@ describe("#route-ratelimits & jwt-auth", () => { assert.equal(result, 10) }) - it("should return 1 point for SLP tier requesting full node access", () => { + it("should return 1 point for indexer tier requesting full node access", () => { const jwtInfo = { apiLevel: 30, resource: "blockchain", @@ -347,7 +347,7 @@ describe("#route-ratelimits & jwt-auth", () => { assert.equal(result, 1) }) - it("should return 1 points for SLP tier requesting indexer access", () => { + it("should return 1 points for indexer tier requesting indexer access", () => { const jwtInfo = { apiLevel: 30, resource: "blockbook", @@ -358,13 +358,46 @@ describe("#route-ratelimits & jwt-auth", () => { assert.equal(result, 1) }) - it("should return 1 points for SLP tier requesting SLPDB access", () => { + it("should return 10 points for indexer tier requesting SLPDB access", () => { const jwtInfo = { apiLevel: 30, resource: "slp", id: "5e3a0415eb29a962da2708b4" } + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 10) + }) + + it("should return 1 point for SLP tier requesting full node access", () => { + const jwtInfo = { + apiLevel: 40, + resource: "blockchain", + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 1) + }) + + it("should return 1 points for SLP tier requesting indexer access", () => { + const jwtInfo = { + apiLevel: 40, + resource: "blockbook", + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 1) + }) + + it("should return 1 points for SLP tier requesting SLPDB access", () => { + const jwtInfo = { + apiLevel: 40, + resource: "slp", + id: "5e3a0415eb29a962da2708b4" + } + const result = rateLimits.calcPoints(jwtInfo) assert.equal(result, 1) }) From b83bef09d8d0daef34a62dfd1a453b9a49f9e6f4 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 16:43:59 -0800 Subject: [PATCH 12/30] Added logging to new rate limiter --- src/middleware/route-ratelimit.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index cbd47fd..725c9f6 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -4,6 +4,8 @@ const express = require("express") const RateLimit = require("express-rate-limit") const axios = require("axios") +const wlogger = require("../util/winston-logging") + const jwt = require("jsonwebtoken") const KeyEncoder = require("key-encoder").default const keyEncoder = new KeyEncoder("secp256k1") @@ -64,6 +66,7 @@ class RateLimits { will be downgraded to 0 on-the-fly. Indexer endpoints will effectively be downgraded to the anonymous access tier. */ + // CT 2/7/20: Older rate-limiting code that does not scale well. async routeRateLimit(req, res, next) { // Disable rate limiting if 0 passed from RATE_LIMIT_MAX_REQUESTS if (maxRequests === 0) return next() @@ -195,6 +198,7 @@ class RateLimits { // It does fine-grane analysis on the data coming from the auth servers and // uses its output to adjust rate limits on-the-fly based on the users // permission level. + // CT 2/7/20: I believe this is older code that is only used by routeRateLimit. evalUserPermissioins(req, authData) { // console.log(`authData: ${JSON.stringify(authData, null, 2)}`) @@ -269,7 +273,7 @@ class RateLimits { userId = decoded.id } else { - console.log(`No JWT token found!`) + wlogger.debug(`No JWT token found!`) } // Code here for the rate limiter is adapted from this example: @@ -277,7 +281,7 @@ class RateLimits { try { // The resource being consumed: full node, indexer, SLPDB, etc. const resource = _this.getResource(req.url) - console.log(`resource: ${resource}`) + wlogger.debug(`resource: ${resource}`) let key = userId ? userId : req.ip @@ -285,7 +289,7 @@ class RateLimits { decoded.resource = resource const pointsToConsume = _this.calcPoints(decoded) - console.log( + wlogger.info( `User ${key} consuming ${pointsToConsume} point for resource ${resource}.` ) @@ -303,7 +307,7 @@ class RateLimits { }) } } catch (err) { - console.error(`Error in route-ratelimit.js/newRateLimit(): `, err) + wlogger.error(`Error in route-ratelimit.js/newRateLimit(): `, err) // throw err } @@ -324,7 +328,7 @@ class RateLimits { const level30Routes = ["insight", "bitcore", "blockbook"] const level40Routes = ["slp"] - // console.log(`apiLevel: ${apiLevel}`) + wlogger.debug(`apiLevel: ${apiLevel}`) // Only evaluate if user is using a JWT token. if (jwtInfo.id) { @@ -354,7 +358,7 @@ class RateLimits { return retVal } catch (err) { - console.error(`Error in route-ratelimit.js/calcPoints()`) + wlogger.error(`Error in route-ratelimit.js/calcPoints()`) // throw err retVal = 30 } @@ -368,14 +372,14 @@ class RateLimits { // what kind of variations will be seen in production. getResource(url) { try { - console.log(`url: ${JSON.stringify(url, null, 2)}`) + wlogger.debug(`url: ${JSON.stringify(url, null, 2)}`) const splitUrl = url.split("/") const resource = splitUrl[1] return resource } catch (err) { - console.error(`Error in getResource().`) + wlogger.error(`Error in getResource().`) throw err } } From bae3bb9de964ff336547d9c8f157a14b28bdec6a Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 16:56:15 -0800 Subject: [PATCH 13/30] Adding redis install to travis --- .travis.yml | 3 +++ install-redis.sh | 12 ++++++++++++ 2 files changed, 15 insertions(+) create mode 100755 install-redis.sh diff --git a/.travis.yml b/.travis.yml index 551328c..c4b3256 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,9 @@ sudo: required services: - docker +before_install: + - ./install-redis.sh + script: "npm run test" after_success: diff --git a/install-redis.sh b/install-redis.sh new file mode 100755 index 0000000..f3d6caf --- /dev/null +++ b/install-redis.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Adapted from: +# https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-18-04 + +# Install Redis. +sudo apt install redis-server + +# Converting to systemd managed process +#echo "supervised systemd" >> /etc/redis/redis.conf + +#sudo systemctl restart redis.service From 142ad8f3f5b0e2b858ccd5370f8a12cba035aeed Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 17:01:01 -0800 Subject: [PATCH 14/30] trying redis install again --- install-redis.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install-redis.sh b/install-redis.sh index f3d6caf..b934819 100755 --- a/install-redis.sh +++ b/install-redis.sh @@ -7,6 +7,6 @@ sudo apt install redis-server # Converting to systemd managed process -#echo "supervised systemd" >> /etc/redis/redis.conf +echo "supervised systemd" >> /etc/redis/redis.conf -#sudo systemctl restart redis.service +sudo systemctl restart redis.service From 759b5a371934d58d711b0effab052024cb32f013 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 17:03:11 -0800 Subject: [PATCH 15/30] trying redis again --- install-redis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install-redis.sh b/install-redis.sh index b934819..3270ab7 100755 --- a/install-redis.sh +++ b/install-redis.sh @@ -7,6 +7,6 @@ sudo apt install redis-server # Converting to systemd managed process -echo "supervised systemd" >> /etc/redis/redis.conf +sudo echo "supervised systemd" >> /etc/redis/redis.conf sudo systemctl restart redis.service From bae5aea85b8d671e9f4bfbc4552807f4caad614f Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 17:12:18 -0800 Subject: [PATCH 16/30] trying redis on travis again --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c4b3256..3415e75 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,9 +9,10 @@ sudo: required services: - docker + - redis-server -before_install: - - ./install-redis.sh +#before_install: +# - ./install-redis.sh script: "npm run test" From 23ea6bd7d38e9768fb4779f6d817c45f228be165 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 17:14:30 -0800 Subject: [PATCH 17/30] Removing redis script --- install-redis.sh | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100755 install-redis.sh diff --git a/install-redis.sh b/install-redis.sh deleted file mode 100755 index 3270ab7..0000000 --- a/install-redis.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# Adapted from: -# https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-18-04 - -# Install Redis. -sudo apt install redis-server - -# Converting to systemd managed process -sudo echo "supervised systemd" >> /etc/redis/redis.conf - -sudo systemctl restart redis.service From 6b2a890dda95c2e45aa2e3df474949da1870ebf6 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 17:55:30 -0800 Subject: [PATCH 18/30] fix(redis): Adding env vars for connecting --- docker/mainnet/Dockerfile | 3 +++ docker/mainnet/docker-compose.yml | 9 +++++++-- docker/mainnet/start-local-mainnet.sh | 4 ++++ src/middleware/route-ratelimit.js | 6 +++++- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docker/mainnet/Dockerfile b/docker/mainnet/Dockerfile index 936092d..43c7e45 100644 --- a/docker/mainnet/Dockerfile +++ b/docker/mainnet/Dockerfile @@ -11,6 +11,9 @@ USER safeuser # Prep 'sudo' commands. #RUN echo 'abcd8765' | sudo -S pwd +# Install Redis. TODO: Make this a separate container. + + # Clone the repository WORKDIR /home/safeuser RUN git clone https://github.com/christroutner/bch-api diff --git a/docker/mainnet/docker-compose.yml b/docker/mainnet/docker-compose.yml index c3dad5d..66b0a6c 100644 --- a/docker/mainnet/docker-compose.yml +++ b/docker/mainnet/docker-compose.yml @@ -3,8 +3,13 @@ bch-api: dockerfile: Dockerfile #image: bitcore container_name: bch-api-main - #links: - # - mongodb + links: + - redis ports: - "12400:3000" restart: always + +redis: + image: redis:latest + container_name: redis-bch-api-main + restart: always diff --git a/docker/mainnet/start-local-mainnet.sh b/docker/mainnet/start-local-mainnet.sh index 6ceae1e..e96afe4 100755 --- a/docker/mainnet/start-local-mainnet.sh +++ b/docker/mainnet/start-local-mainnet.sh @@ -17,4 +17,8 @@ export NODE_TLS_REJECT_UNAUTHORIZED=0 export JWT_AUTH_SERVER=http://localhost:5001/ +# Redis DB +export REDIS_PORT=6379 +export REDIS_HOST=172.17.0.1 + npm start diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index 725c9f6..af524d5 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -12,7 +12,11 @@ const keyEncoder = new KeyEncoder("secp256k1") // Redis const Redis = require("ioredis") -const redisClient = new Redis({ enableOfflineQueue: false }) +const redisClient = new Redis({ + enableOfflineQueue: false, + port: process.env.REDIS_PORT ? process.env.REDIS_PORT : 6379, + host: process.env.REDIS_HOST ? process.env.REDIS_HOST : "127.0.0.1" +}) // Rate limiter middleware lib. const { RateLimiterRedis } = require("rate-limiter-flexible") From f732bdb6a7f183ae965a383628d5ba4c29b56235 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 18:42:31 -0800 Subject: [PATCH 19/30] debugging Docker redis --- docker/mainnet/docker-compose.yml | 2 ++ src/middleware/route-ratelimit.js | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docker/mainnet/docker-compose.yml b/docker/mainnet/docker-compose.yml index 66b0a6c..4ff6c40 100644 --- a/docker/mainnet/docker-compose.yml +++ b/docker/mainnet/docker-compose.yml @@ -13,3 +13,5 @@ redis: image: redis:latest container_name: redis-bch-api-main restart: always + ports: + - "6379:6379" diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index af524d5..fe759a1 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -11,12 +11,15 @@ const KeyEncoder = require("key-encoder").default const keyEncoder = new KeyEncoder("secp256k1") // Redis -const Redis = require("ioredis") -const redisClient = new Redis({ +const redisOptions = { enableOfflineQueue: false, port: process.env.REDIS_PORT ? process.env.REDIS_PORT : 6379, host: process.env.REDIS_HOST ? process.env.REDIS_HOST : "127.0.0.1" -}) +} +console.log(`redisOptions: ${JSON.stringify(redisOptions, null, 2)}`) + +const Redis = require("ioredis") +const redisClient = new Redis(redisOptions) // Rate limiter middleware lib. const { RateLimiterRedis } = require("rate-limiter-flexible") From 18c384ae08ce67f52b37c376f4947d889c1246c9 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 19:31:55 -0800 Subject: [PATCH 20/30] fix(docker): Updating docker containers to use redis --- docker/mainnet/docker-compose.yml | 8 ++++---- docker/testnet/docker-compose.yml | 15 +++++++++++---- docker/testnet/start-local-testnet.sh | 4 ++++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docker/mainnet/docker-compose.yml b/docker/mainnet/docker-compose.yml index 4ff6c40..56db9e1 100644 --- a/docker/mainnet/docker-compose.yml +++ b/docker/mainnet/docker-compose.yml @@ -1,17 +1,17 @@ -bch-api: +bch-api-main: build: . dockerfile: Dockerfile #image: bitcore container_name: bch-api-main links: - - redis + - redis-main ports: - "12400:3000" restart: always -redis: +redis-main: image: redis:latest - container_name: redis-bch-api-main + container_name: redis-main restart: always ports: - "6379:6379" diff --git a/docker/testnet/docker-compose.yml b/docker/testnet/docker-compose.yml index edc28ee..9d97c2f 100644 --- a/docker/testnet/docker-compose.yml +++ b/docker/testnet/docker-compose.yml @@ -1,10 +1,17 @@ -bch-api: +bch-api-test: build: . dockerfile: Dockerfile #image: bitcore - container_name: bch-api - #links: - # - mongodb + container_name: bch-api-test + links: + - redis-test ports: - "13400:3000" restart: always + +redis-test: + image: redis:latest + container_name: redis-test + restart: always + ports: + - "6380:6379" diff --git a/docker/testnet/start-local-testnet.sh b/docker/testnet/start-local-testnet.sh index 3a2e033..edb8619 100755 --- a/docker/testnet/start-local-testnet.sh +++ b/docker/testnet/start-local-testnet.sh @@ -17,4 +17,8 @@ export BLOCKBOOK_URL=https://172.17.0.1:19131/ # Allow node.js to make network calls to https using self-signed certificate. export NODE_TLS_REJECT_UNAUTHORIZED=0 +# Redis DB +export REDIS_PORT=6380 +export REDIS_HOST=172.17.0.1 + npm start From a0d1f55894b8945fd01e57f4ffdee063acf24382 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 7 Feb 2020 19:44:36 -0800 Subject: [PATCH 21/30] fix(deploy): Also deploying testnet for production --- deploy-production.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy-production.sh b/deploy-production.sh index 52be2fc..6064898 100755 --- a/deploy-production.sh +++ b/deploy-production.sh @@ -10,5 +10,6 @@ export DATA="{\"ref\":\"$DEPLOY_SECRET\"}" #echo $DATA curl -X POST http://fullstack.cash:9000/hooks/bch-api-mainnet -H "Content-Type: application/json" -d $DATA +curl -X POST http://fullstack.cash:9000/hooks/bch-api-testnet -H "Content-Type: application/json" -d $DATA echo "...Finished deploying to production." From e83aaad1e309928ac3407569020ee2586e69531a Mon Sep 17 00:00:00 2001 From: danielhumgon Date: Sat, 8 Feb 2020 22:31:56 -0400 Subject: [PATCH 22/30] refactor(blockchain.js): refactoring blockchain.js library [WIP] --- src/routes/v3/full-node/blockchain2.js | 748 +++++++++++++++++++++++++ 1 file changed, 748 insertions(+) diff --git a/src/routes/v3/full-node/blockchain2.js b/src/routes/v3/full-node/blockchain2.js index a7c103f..a84c70e 100644 --- a/src/routes/v3/full-node/blockchain2.js +++ b/src/routes/v3/full-node/blockchain2.js @@ -34,6 +34,21 @@ class Blockchain { this.router.get("/", this.root) this.router.get("/getBestBlockHash", this.getBestBlockHash) this.router.get("/getBlockchainInfo", this.getBlockchainInfo) + this.router.get("/getBlockCount", this.getBlockCount) + this.router.get("/getBlockHeader/:hash", this.getBlockHeaderSingle) + this.router.post("/getBlockHeader", getBlockHeaderBulk) + this.router.get("/getChainTips", getChainTips) + this.router.get("/getDifficulty", getDifficulty) + this.router.get("/getMempoolEntry/:txid", getMempoolEntrySingle) + this.router.post("/getMempoolEntry", getMempoolEntryBulk) + this.router.get("/getMempoolAncestors/:txid", getMempoolAncestorsSingle) + this.router.get("/getMempoolInfo", getMempoolInfo) + this.router.get("/getRawMempool", getRawMempool) + this.router.get("/getTxOut/:txid/:n", getTxOut) + this.router.get("/getTxOutProof/:txid", getTxOutProofSingle) + this.router.post("/getTxOutProof", getTxOutProofBulk) + this.router.get("/verifyTxOutProof/:proof", verifyTxOutProofSingle) + this.router.post("/verifyTxOutProof", verifyTxOutProofBulk) } root(req, res, next) { @@ -128,6 +143,739 @@ class Blockchain { return this.errorHandler(err, res) } } + + /** + * @api {get} /blockchain/getBlockCount Get Block Count + * @apiName GetBlockCount + * @apiGroup Blockchain + * @apiDescription Returns the number of blocks in the longest blockchain. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getBlockCount" -H "accept: application/json" + * + * @apiSuccess {Number} bestBlockCount 587665 + */ + async getBlockCount(req, res, next) { + try { + // Axios options + const options = this.routeUtils.getAxiosOptions() + options.data.id = "getblockcount" + options.data.method = "getblockcount" + options.data.params = [] + + const response = await this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getBlockCount().`, err) + + return this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getBlockHeader/:hash Get single block header + * @apiName GetSingleBlockHeader + * @apiGroup Blockchain + * @apiDescription If verbose is false (default), returns a string that is + * serialized, hex-encoded data for blockheader 'hash'. If verbose is true, + * returns an Object with information about blockheader hash. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getBlockHeader/000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201?verbose=true" -H "accept: application/json" + * + * @apiParam {String} hash block hash + * @apiParam {Boolean} verbose Return verbose data + * + * @apiSuccess {Object} object Object containing data + * @apiSuccess {String} object.hash "000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201" + * @apiSuccess {Number} object.confirmations 61839 + * @apiSuccess {Number} object.height 500000 + * @apiSuccess {Number} object.version 536870912 + * @apiSuccess {String} object.versionHex "20000000" + * @apiSuccess {String} object.merkleroot "4af279645e1b337e655ae3286fc2ca09f58eb01efa6ab27adedd1e9e6ec19091" + * @apiSuccess {Number} object.time 1509343584 + * @apiSuccess {Number} object.mediantime 1509336533 + * @apiSuccess {Number} object.nonce 3604508752 + * @apiSuccess {String} object.bits "1809b91a" + * @apiSuccess {Number} object.difficulty 113081236211.4533 + * @apiSuccess {String} object.chainwork "0000000000000000000000000000000000000000007ae48aca46e3b449ad9714" + * @apiSuccess {String} object.previousblockhash "0000000000000000043831d6ebb013716f0580287ee5e5687e27d0ed72e6e523" + * @apiSuccess {String} object.nextblockhash "00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3" + */ + async getBlockHeaderSingle(req, res, next) { + try { + let verbose = false + if (req.query.verbose && req.query.verbose.toString() === "true") + verbose = true + + const hash = req.params.hash + if (!hash || hash === "") { + res.status(400) + return res.json({ error: "hash can not be empty" }) + } + + // Axios options + const options = this.routeUtils.getAxiosOptions() + options.data.id = "getblockheader" + options.data.method = "getblockheader" + options.data.params = [hash, verbose] + + const response = await this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getBlockHeaderSingle().`, err) + + return this.errorHandler(err, res) + } + } + + /** + * @api {post} /blockchain/getBlockHeader Get multiple block headers + * @apiName GetBulkBlockHeader + * @apiGroup Blockchain + * @apiDescription If verbose is false (default), returns a string that is + * serialized, hex-encoded data for blockheader 'hash'. If verbose is true, + * returns an Object with information about blockheader hash. + * + * @apiExample Example usage: + * curl -X POST "https://rest.bitcoin.com/v3/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}" + * + * @apiParam {String} hash block hash + * @apiParam {Boolean} verbose Return verbose data + * + * @apiSuccess {Array} array array containing objects + * @apiSuccess {String} object.hash "000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201" + * @apiSuccess {Number} object.confirmations 61839 + * @apiSuccess {Number} object.height 500000 + * @apiSuccess {Number} object.version 536870912 + * @apiSuccess {String} object.versionHex "20000000" + * @apiSuccess {String} object.merkleroot "4af279645e1b337e655ae3286fc2ca09f58eb01efa6ab27adedd1e9e6ec19091" + * @apiSuccess {Number} object.time 1509343584 + * @apiSuccess {Number} object.mediantime 1509336533 + * @apiSuccess {Number} object.nonce 3604508752 + * @apiSuccess {String} object.bits "1809b91a" + * @apiSuccess {Number} object.difficulty 113081236211.4533 + * @apiSuccess {String} object.chainwork "0000000000000000000000000000000000000000007ae48aca46e3b449ad9714" + * @apiSuccess {String} object.previousblockhash "0000000000000000043831d6ebb013716f0580287ee5e5687e27d0ed72e6e523" + * @apiSuccess {String} object.nextblockhash "00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3" + */ + async getBlockHeaderBulk(req, res, next) { + try { + const hashes = req.body.hashes + const verbose = req.body.verbose ? req.body.verbose : false + + if (!Array.isArray(hashes)) { + res.status(400) + return res.json({ + error: "hashes needs to be an array. Use GET for single hash." + }) + } + + // 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.` + }) + } + + wlogger.debug( + `Executing blockchain/getBlockHeaderBulk with these hashes: `, + hashes + ) + + // Validate each hash in the array. + for (let i = 0; i < hashes.length; i++) { + const hash = hashes[i] + + if (hash.length !== 64) { + res.status(400) + return res.json({ error: `This is not a hash: ${hash}` }) + } + } + + // Axios options + const options = this.routeUtils.getAxiosOptions() + + // Loop through each hash and creates an array of requests to call in parallel + const promises = hashes.map(async hash => { + options.data.id = "getblockheader" + options.data.method = "getblockheader" + options.data.params = [hash, verbose] + + return await this.axios.request(options) + }) + + const axiosResult = await this.axios.all(promises) + + // Extract the data component from the axios response. + const result = axiosResult.map(x => x.data.result) + + res.status(200) + return res.json(result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getBlockHeaderBulk().`, err) + + return this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getChainTips Get Chain Tips + * @apiName getChainTips + * @apiGroup Blockchain + * @apiDescription Return information about all known tips in the block tree, + * including the main chain as well as orphaned branches. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getChainTips" -H "accept: application/json" + * + */ + async getChainTips(req, res, next) { + try { + // Axios options + const options = this.routeUtils.getAxiosOptions() + + options.data.id = "getchaintips" + options.data.method = "getchaintips" + options.data.params = [] + + const response = await this.axios.request(options) + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getChainTips().`, err) + + return this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getDifficulty Get difficulty + * @apiName getDifficulty + * @apiGroup Blockchain + * @apiDescription Get the current difficulty value, used to regulate mining + * power on the network. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getDifficulty" -H "accept: application/json" + * + */ + async getDifficulty(req, res, next) { + try { + // Axios options + const options = this.routeUtils.getAxiosOptions() + + options.data.id = "getdifficulty" + options.data.method = "getdifficulty" + options.data.params = [] + + const response = await this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getDifficulty().`, err) + + return this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getMempoolEntry/:txid Get single mempool entry + * @apiName getMempoolEntry + * @apiGroup Blockchain + * @apiDescription Returns mempool data for given transaction. TXID must be in + * mempool (unconfirmed) + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getMempoolEntry/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" + * + */ + async getMempoolEntrySingle(req, res, next) { + try { + // Validate input parameter + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + // Axios options + const options = this.routeUtils.getAxiosOptions() + + options.data.id = "getmempoolentry" + options.data.method = "getmempoolentry" + options.data.params = [txid] + + const response = await this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getMempoolEntrySingle().`, err) + + return this.errorHandler(err, res) + } + } + + /** + * @api {post} /blockchain/getMempoolEntry Get bulk mempool entry + * @apiName getMempoolEntryBulk + * @apiGroup Blockchain + * @apiDescription Returns mempool data for multiple transactions + * + * @apiExample Example usage: + * curl -X POST https://mainnet.bchjs.cash/v3/blockchain/getMempoolEntry -H "Content-Type: application/json" -d "{\"txids\":[\"a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1\",\"5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e\"]}" + */ + async getMempoolEntryBulk(req, res, next) { + try { + const txids = req.body.txids + + if (!Array.isArray(txids)) { + res.status(400) + return res.json({ + error: "txids needs to be an array. Use GET for single txid." + }) + } + + // 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 blockchain/getMempoolEntry with these txids: `, + txids + ) + + // Validate each element in the array + for (let i = 0; i < txids.length; i++) { + const txid = txids[i] + + if (txid.length !== 64) { + res.status(400) + return res.json({ error: "This is not a txid" }) + } + } + + // Axios options + const options = this.routeUtils.getAxiosOptions() + + // Loop through each txid and creates an array of requests to call in parallel + const promises = txids.map(async txid => { + options.data.id = "getmempoolentry" + options.data.method = "getmempoolentry" + options.data.params = [txid] + + return await this.axios.request(options) + }) + + const axiosResult = await this.axios.all(promises) + + // Extract the data component from the axios response. + const result = axiosResult.map(x => x.data.result) + + res.status(200) + return res.json(result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getMempoolEntryBulk().`, err) + + return this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getMempoolAncestors/:txid Get Mempool Ancestors + * @apiName getMempoolAncestors + * @apiGroup Blockchain + * @apiDescription Returns mempool ancestors data for given TXID. It must be in + * mempool (unconfirmed). This call is handy to tell if a UTXO is bumping up + * against the 25 ancestor chain-limit. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getMempoolAncestors/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" + * + */ + async getMempoolAncestorsSingle(req, res, next) { + try { + // Validate input parameter + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + let verbose = req.params.verbose + if (verbose === undefined) verbose = false + + // Axios options + const options = this.routeUtils.getAxiosOptions() + + options.data.id = "getmempoolancestors" + options.data.method = "getmempoolancestors" + options.data.params = [txid, verbose] + + const response = await this.axios.request(requestConfig) + // console.log(`response: ${util.inspect(response)}`) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getMempoolAncestorsSingle().`, err) + + return this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getMempoolInfo Get mempool info + * @apiName getMempoolInfo + * @apiGroup Blockchain + * @apiDescription Returns details on the active state of the TX memory pool. + * + * @apiExample Example usage: + * curl -X GET https://mainnet.bchjs.cash/v3/getMempoolInfo -H "accept: application/json" + * + */ + async getMempoolInfo(req, res, next) { + try { + // Axios options + const options = this.routeUtils.getAxiosOptions() + + options.data.id = "getmempoolinfo" + options.data.method = "getmempoolinfo" + options.data.params = [] + + const response = await this.axios.request(options) + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getMempoolInfo().`, err) + + return this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getRawMempool Get mempool info + * @apiName getMempoolInfo + * @apiGroup Blockchain + * @apiDescription Returns details on the active state of the TX memory pool. + * + * @apiExample Example usage: + * curl -X GET https://mainnet.bchjs.cash/v3/getMempoolInfo -H "accept: application/json" + * + */ + /** + * @api {get} /blockchain/getRawMempool/?verbose= Get raw mempool + * @apiName getRawMempool + * @apiGroup Blockchain + * @apiDescription Returns all transaction ids in memory pool as a json array + * of string transaction ids. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/getRawMempool/?verbose=true" -H "accept: application/json" + * + * @apiParam {Boolean} verbose Return verbose data + * + */ + async getRawMempool(req, res, next) { + try { + // Axios options + const options = this.routeUtils.getAxiosOptions() + + let verbose = false + if (req.query.verbose && req.query.verbose === "true") verbose = true + + options.data.id = "getrawmempool" + options.data.method = "getrawmempool" + options.data.params = [verbose] + + const response = await this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getRawMempool().`, err) + + return this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getTxOut/:txid/:n?mempool= Get Tx Out + * @apiName getTxOut + * @apiGroup Blockchain + * @apiDescription Returns details about an unspent transaction output. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getTxOut/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33/0?mempool=false" -H "accept: application/json" + * + * @apiParam {String} txid Transaction id (required) + * @apiParam {Number} n Output number (required) + * @apiParam {Boolean} mempool Check mempool or not (optional) + * + */ + // Returns details about an unspent transaction output. + async getTxOut(req, res, next) { + try { + // Validate input parameter + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + let n = req.params.n + if (n === undefined || n === "") { + res.status(400) + return res.json({ error: "n can not be empty" }) + } + n = parseInt(n) + + let include_mempool = false + if (req.query.include_mempool && req.query.include_mempool === "true") + include_mempool = true + + // Axios options + const options = this.routeUtils.getAxiosOptions() + + options.data.id = "gettxout" + options.data.method = "gettxout" + options.data.params = [txid, n, include_mempool] + + // console.log(`requestConfig: ${JSON.stringify(requestConfig, null, 2)}`) + + const response = await this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getTxOut().`, err) + + return this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getTxOutProofSingle/:txid Get Tx Out Proof + * @apiName getTxOutProofSingle + * @apiGroup Blockchain + * @apiDescription Returns a hex-encoded proof that 'txid' was included in a block. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getTxOutProofSingle/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" + * + * @apiParam {String} txid Transaction id (required) + * + */ + async getTxOutProofSingle(req, res, next) { + try { + // Validate input parameter + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + // Axios options + const options = this.routeUtils.getAxiosOptions() + + options.data.id = "gettxoutproof" + options.data.method = "gettxoutproof" + options.data.params = [[txid]] + + const response = await this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getTxOutProofSingle().`, err) + + return this.errorHandler(err, res) + } + } + + // Returns a hex-encoded proof that 'txid' was included in a block. + async getTxOutProofBulk(req, res, next) { + try { + const txids = req.body.txids + + // Reject if txids is not an array. + if (!Array.isArray(txids)) { + res.status(400) + return res.json({ + error: "txids needs to be an array. Use GET for single txid." + }) + } + + // 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.` + }) + } + + // Axios options + const options = this.routeUtils.getAxiosOptions() + + // Validate each element in the array. + for (let i = 0; i < txids.length; i++) { + const txid = txids[i] + + if (txid.length !== 64) { + res.status(400) + return res.json({ + error: `Invalid txid. Double check your txid is valid: ${txid}` + }) + } + } + + wlogger.debug( + `Executing blockchain/getTxOutProof with these txids: `, + txids + ) + + // Loop through each txid and creates an array of requests to call in parallel + const promises = txids.map(async txid => { + options.data.id = "gettxoutproof" + options.data.method = "gettxoutproof" + options.data.params = [[txid]] + + return await this.axios.request(options) + }) + + // Wait for all parallel promisses to resolve. + const axiosResult = await this.axios.all(promises) + + // Extract the data component from the axios response. + const result = axiosResult.map(x => x.data.result) + + res.status(200) + return res.json(result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/getTxOutProofBulk().`, err) + + return this.errorHandler(err, res) + } + } + + async verifyTxOutProofSingle(req, res, next) { + try { + // Validate input parameter + const proof = req.params.proof + if (!proof || proof === "") { + res.status(400) + return res.json({ error: "proof can not be empty" }) + } + + // Axios options + const options = this.routeUtils.getAxiosOptions() + + options.data.id = "verifytxoutproof" + options.data.method = "verifytxoutproof" + options.data.params = [req.params.proof] + + const response = await this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/verifyTxOutProofSingle().`, err) + + return this.errorHandler(err, res) + } + } + + async verifyTxOutProofBulk(req, res, next) { + try { + const proofs = req.body.proofs + + // Reject if proofs is not an array. + if (!Array.isArray(proofs)) { + res.status(400) + return res.json({ + error: "proofs needs to be an array. Use GET for single proof." + }) + } + + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, proofs)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Array too large.` + }) + } + + // Axios options + const options = this.routeUtils.getAxiosOptions() + + // Validate each element in the array. + for (let i = 0; i < proofs.length; i++) { + const proof = proofs[i] + + if (!proof || proof === "") { + res.status(400) + return res.json({ error: `proof can not be empty: ${proof}` }) + } + } + + wlogger.debug( + `Executing blockchain/verifyTxOutProof with these proofs: `, + proofs + ) + + // Loop through each proof and creates an array of requests to call in parallel + const promises = proofs.map(async proof => { + options.data.id = "verifytxoutproof" + options.data.method = "verifytxoutproof" + options.data.params = [proof] + + return await this.axios.request(options) + }) + + // Wait for all parallel promisses to resolve. + const axiosResult = await this.axios.all(promises) + + // Extract the data component from the axios response. + const result = axiosResult.map(x => x.data.result[0]) + + res.status(200) + return res.json(result) + } catch (err) { + // Write out error to error log. + //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error(`Error in blockchain.ts/verifyTxOutProofBulk().`, err) + + return this.errorHandler(err, res) + } + } } module.exports = Blockchain From edef471a793ab6fb91b8a2edc9d6360e9d0b9e96 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" <23040076+greenkeeper[bot]@users.noreply.github.com> Date: Sun, 9 Feb 2020 06:17:48 +0000 Subject: [PATCH 23/30] fix(package): update rate-limiter-flexible to version 2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e093f99..9aa4a60 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "passport-http": "^0.3.0", "pg": "^7.11.0", "pg-hstore": "^2.3.2", - "rate-limiter-flexible": "^1.3.2", + "rate-limiter-flexible": "^2.0.0", "strftime": "^0.10.0", "winston": "^3.2.1", "winston-daily-rotate-file": "^4.0.0" From 02c8e3b10db3ff82275b46b6f5b24edbf429f235 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" <23040076+greenkeeper[bot]@users.noreply.github.com> Date: Sun, 9 Feb 2020 06:17:52 +0000 Subject: [PATCH 24/30] chore(package): update lockfile package-lock.json --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d278d6c..dd3088f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12319,9 +12319,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "rate-limiter-flexible": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-1.3.2.tgz", - "integrity": "sha512-f+xNvGn+52G4nZVok9VB3LTE1kfDmqbnWKRayX5n2k/LEQ7doWrYvzmzFVh7ltmLgwEdEzoYwxGaXlfCjFg4Ag==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.0.0.tgz", + "integrity": "sha512-DjLeci3BuHWNr9LVVm+YPJ+Lrki/J9iDb9cEJFZELpa/ZBhXmfnfR+eCI5jzmsPJtpYGYhw/vyRfjjQjTzVtRg==" }, "raw-body": { "version": "2.4.0", From 6a7904b6af53f1a81effa8a6bf711bb770ad2265 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Mon, 10 Feb 2020 09:08:17 -0800 Subject: [PATCH 25/30] Finished unit tests for new rateLimitByResource() --- src/middleware/route-ratelimit.js | 2 + test/v3/rate-limits.js | 78 +++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index fe759a1..9695370 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -206,6 +206,7 @@ class RateLimits { // uses its output to adjust rate limits on-the-fly based on the users // permission level. // CT 2/7/20: I believe this is older code that is only used by routeRateLimit. + // It will probably be removed in the future. evalUserPermissioins(req, authData) { // console.log(`authData: ${JSON.stringify(authData, null, 2)}`) @@ -259,6 +260,7 @@ class RateLimits { } } + // Decode the JWT token if one exists. if (req.locals.jwtToken) { // Hexadecimal const publicKey = diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js index fdf021a..2838e83 100644 --- a/test/v3/rate-limits.js +++ b/test/v3/rate-limits.js @@ -160,7 +160,7 @@ describe("#route-ratelimits & jwt-auth", () => { ) }) - it("should trigger rate-limit for free tier 10 RPM", async () => { + it("should trigger rate-limit for free tier after 10 RPM", async () => { req.baseUrl = "/v3" req.path = "/control/getNetworkInfo" req.method = "GET" @@ -452,7 +452,7 @@ describe("#route-ratelimits & jwt-auth", () => { const jwtInfo = { apiLevel: 10, - id: "5e3a0415eb29a962da2708b4" + id: "5e3a0415eb29a962da2708b1" } // Mock the call to the jwt library. @@ -488,7 +488,7 @@ describe("#route-ratelimits & jwt-auth", () => { const jwtInfo = { apiLevel: 10, - id: "5e3a0415eb29a962da2708b4" + id: "5e3a0415eb29a962da2708b2" } // Mock the call to the jwt library. @@ -508,6 +508,78 @@ describe("#route-ratelimits & jwt-auth", () => { `next should not be called if rate limit was triggered.` ) }) + + it("should NOT trigger rate-limit handler for indexer-tier at 25 RPM", async () => { + // Create a new instance of the rate limit so we start with zeroed tracking. + rateLimits = new RateLimits() + + req.baseUrl = "/v3" + req.path = "/control/getNetworkInfo" + req.url = req.path + req.method = "GET" + + req.locals.jwtToken = "some-token" + + const jwtInfo = { + apiLevel: 20, + id: "5e3a0415eb29a962da2708b3" + } + + // Mock the call to the jwt library. + sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo) + + for (let i = 0; i < 25; i++) { + next.reset() // reset the stubbed next() function. + + await rateLimits.rateLimitByResource(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + //console.log(`req.locals after test: ${util.inspect(req.locals)}`) + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + true, + `next should be called if rate limit was not triggered.` + ) + }) + + it("should still rate-limit at a higher RPM for pro-tier", async () => { + // Create a new instance of the rate limit so we start with zeroed tracking. + rateLimits = new RateLimits() + + req.baseUrl = "/v3" + req.path = "/control/getNetworkInfo" + req.url = req.path + req.method = "GET" + + req.locals.jwtToken = "some-token" + + const jwtInfo = { + apiLevel: 20, + id: "5e3a0415eb29a962da2708b5" + } + + // Mock the call to the jwt library. + sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo) + + for (let i = 0; i < 150; i++) { + next.reset() // reset the stubbed next() function. + + await rateLimits.rateLimitByResource(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + //console.log(`req.locals after test: ${util.inspect(req.locals)}`) + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + false, + `next should NOT be called if rate limit was triggered.` + ) + }) }) }) From dc1b4443098caa8987cb01868de9c0cb01a084a8 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Mon, 10 Feb 2020 17:43:15 -0800 Subject: [PATCH 26/30] fix(rateLimitSimple): Created simpler rate-limit middleware for rest.bitcoin.com --- src/middleware/route-ratelimit.js | 117 +++++++++++++++-- test/v3/rate-limits.js | 209 ++++++++++++++++++++++++++++++ 2 files changed, 315 insertions(+), 11 deletions(-) diff --git a/src/middleware/route-ratelimit.js b/src/middleware/route-ratelimit.js index 9695370..1d71a6c 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -29,6 +29,11 @@ const rateLimitOptions = { duration: 1 // Per second } +// This hard-coded value is temporary. It will be swapped out with an environment +// variable when moved to production. +const publicKey = + "03e6c358092a459f7da9420de770eef3e16cf3c9c54a3d3d14ac2d7f0b82af4d7d" + // Set max requests per minute const maxRequests = process.env.RATE_LIMIT_MAX_REQUESTS ? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) @@ -56,6 +61,7 @@ class RateLimits { redisClient.disconnect() } + // CT 2/7/20: Older rate-limiting code that does not scale well. /* This function controls the tierd request-per-minute (RPM) rate limits. This is an older implementation that is currently not used. @@ -73,7 +79,6 @@ class RateLimits { will be downgraded to 0 on-the-fly. Indexer endpoints will effectively be downgraded to the anonymous access tier. */ - // CT 2/7/20: Older rate-limiting code that does not scale well. async routeRateLimit(req, res, next) { // Disable rate limiting if 0 passed from RATE_LIMIT_MAX_REQUESTS if (maxRequests === 0) return next() @@ -201,12 +206,12 @@ class RateLimits { uniqueRateLimits[route](req, res, next) } + // CT 2/7/20: I believe this is older code that is only used by routeRateLimit. + // It will probably be removed in the future. // This function returns an object with proLimit and apiLevel properties. // It does fine-grane analysis on the data coming from the auth servers and // uses its output to adjust rate limits on-the-fly based on the users // permission level. - // CT 2/7/20: I believe this is older code that is only used by routeRateLimit. - // It will probably be removed in the future. evalUserPermissioins(req, authData) { // console.log(`authData: ${JSON.stringify(authData, null, 2)}`) @@ -241,10 +246,9 @@ class RateLimits { return retObj } - /* - This is the new rate limit function that uses the rate-limiter-flexible npm - library. - */ + // This is the new rate limit function that uses the rate-limiter-flexible npm + // library. It uses fine-grain rate limiting based on the resources being + // consumed. async rateLimitByResource(req, res, next) { try { let userId @@ -262,10 +266,6 @@ class RateLimits { // Decode the JWT token if one exists. if (req.locals.jwtToken) { - // Hexadecimal - const publicKey = - "03e6c358092a459f7da9420de770eef3e16cf3c9c54a3d3d14ac2d7f0b82af4d7d" - const jwtOptions = { algorithms: ["ES256"] } @@ -392,6 +392,101 @@ class RateLimits { throw err } } + + // This is a variation of rateLimitByResource() function. This version will + // potentially be used by Bitcoin.com. + // Rather than using apiLevel, the rateLimit is explicitly recorded in the + // JWT token. + async rateLimitSimple(req, res, next) { + try { + let userId + let decoded = {} + + // Create a res.locals object if not passed in. + if (!req.locals) { + req.locals = { + // default values + jwtToken: "", + proLimit: false, + rateLimit: 3 + } + } + + // Decode the JWT token if one exists. + if (req.locals.jwtToken) { + const jwtOptions = { + algorithms: ["ES256"] + } + + const pemPublicKey = keyEncoder.encodePublic(publicKey, "raw", "pem") + + // Validate the JWT token. + decoded = _this.jwt.verify( + req.locals.jwtToken, + pemPublicKey, + jwtOptions + ) + // console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`) + + userId = decoded.id + } else { + wlogger.debug(`No JWT token found!`) + } + + // Code here for the rate limiter is adapted from this example: + // https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users + try { + // Key for Redis key/value pair. + const key = userId ? userId : req.ip + + const pointsToConsume = _this.calcPoints2(decoded) + + wlogger.debug(`User ${key} consuming ${pointsToConsume}.`) + + await _this.rateLimiter.consume(key, pointsToConsume) + } catch (err) { + // console.log(`err: `, err) + + // Rate limited was triggered + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: `Too many requests. Your limits are currently ${maxRequests} requests per minute. Increase rate limits at https://account.bchjs.cash` + }) + } + } catch (err) { + wlogger.error(`Error in route-ratelimit.js/rateLimitSimple(): `, err) + // throw err + } + + next() + } + + // Calculates the points consumed, based on the explicit rateLimit defined + // in the JWT token. + calcPoints2(jwtInfo) { + let retVal = 30 // By default, use anonymous tier. + + try { + // console.log(`jwtInfo: ${JSON.stringify(jwtInfo, null, 2)}`) + + const MAX_RATE_LIMIT = 100 + + const rateLimit = jwtInfo.rateLimit + + // Only evaluate if user is using a JWT token. + if (jwtInfo.id) { + const points = Math.floor(MAX_RATE_LIMIT / rateLimit) + + retVal = points + } + } catch (err) { + wlogger.error(`Error in route-ratelimit.js/calcPoints2()`) + // throw err + retVal = 30 + } + + return retVal + } } module.exports = RateLimits diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js index 2838e83..2b832a1 100644 --- a/test/v3/rate-limits.js +++ b/test/v3/rate-limits.js @@ -53,6 +53,7 @@ describe("#route-ratelimits & jwt-auth", () => { req.params = {} req.body = {} req.query = {} + req.locals = {} sandbox = sinon.createSandbox() }) @@ -403,6 +404,35 @@ describe("#route-ratelimits & jwt-auth", () => { }) }) + describe("#calcPoints2", () => { + it("should return 30 points for anonymous user", () => { + const result = rateLimits.calcPoints2({}) + // console.log(`result: ${result}`) + + assert.equal(result, 30) + }) + + it("should return 10 point for free tier", () => { + const jwtInfo = { + rateLimit: 10, + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints2(jwtInfo) + assert.equal(result, 10) + }) + + it("should return 1 point for pro tier", () => { + const jwtInfo = { + rateLimit: 100, + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints2(jwtInfo) + assert.equal(result, 1) + }) + }) + describe("#rateLimitByResource", () => { it("should pass through rate-limit middleware", async () => { req.baseUrl = "/v3" @@ -581,6 +611,185 @@ describe("#route-ratelimits & jwt-auth", () => { ) }) }) + + describe("#rateLimitSimple", () => { + it("should pass through rate-limit middleware", async () => { + req.baseUrl = "/v3" + req.path = "/control/getNetworkInfo" + req.url = req.path + req.method = "GET" + + // Call the route twice to trigger the rate handling. + await rateLimits.rateLimitSimple(req, res, next) + await rateLimits.rateLimitSimple(req, res, next) + + // next() will be called if rate-limit is not triggered + assert.equal(next.called, true) + }) + + it("should trigger rate-limit handler if rate limits exceeds 5 request per minute", async () => { + req.baseUrl = "/v3" + req.path = "/control/getNetworkInfo" + req.url = req.path + req.method = "GET" + + for (let i = 0; i < 5; i++) { + next.reset() // reset the stubbed next() function. + + await rateLimits.rateLimitSimple(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + false, + `next should not be called if rate limit was triggered.` + ) + }) + + it("should NOT trigger rate-limit for free-tier at 5 RPM", async () => { + // Create a new instance of the rate limit so we start with zeroed tracking. + rateLimits = new RateLimits() + + req.baseUrl = "/v3" + req.path = "/control/getNetworkInfo" + req.url = req.path + req.method = "GET" + + req.locals.jwtToken = "some-token" + + const jwtInfo = { + rateLimit: 10, + id: "5e3a0415eb29a962da2708c1" + } + + // Mock the call to the jwt library. + sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo) + + for (let i = 0; i < 5; i++) { + next.reset() // reset the stubbed next() function. + + await rateLimits.rateLimitSimple(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + //console.log(`req.locals after test: ${util.inspect(req.locals)}`) + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + true, + `next should be called if rate limit was not triggered.` + ) + }) + + it("should trigger rate-limit for free tier after 10 RPM", async () => { + // Create a new instance of the rate limit so we start with zeroed tracking. + rateLimits = new RateLimits() + + req.baseUrl = "/v3" + req.path = "/control/getNetworkInfo" + req.url = req.path + req.method = "GET" + + req.locals.jwtToken = "some-token" + + const jwtInfo = { + rateLimit: 10, + id: "5e3a0415eb29a962da2708c2" + } + + // Mock the call to the jwt library. + sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo) + + for (let i = 0; i < 12; i++) { + next.reset() // reset the stubbed next() function. + + await rateLimits.rateLimitSimple(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + false, + `next should not be called if rate limit was triggered.` + ) + }) + + it("should NOT trigger rate-limit handler for pro-tier at 25 RPM", async () => { + // Create a new instance of the rate limit so we start with zeroed tracking. + rateLimits = new RateLimits() + + req.baseUrl = "/v3" + req.path = "/control/getNetworkInfo" + req.url = req.path + req.method = "GET" + + req.locals.jwtToken = "some-token" + + const jwtInfo = { + rateLimit: 100, + id: "5e3a0415eb29a962da2708c3" + } + + // Mock the call to the jwt library. + sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo) + + for (let i = 0; i < 25; i++) { + next.reset() // reset the stubbed next() function. + + await rateLimits.rateLimitSimple(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + //console.log(`req.locals after test: ${util.inspect(req.locals)}`) + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + true, + `next should be called if rate limit was not triggered.` + ) + }) + + it("should still rate-limit at a higher RPM for pro-tier", async () => { + // Create a new instance of the rate limit so we start with zeroed tracking. + rateLimits = new RateLimits() + + req.baseUrl = "/v3" + req.path = "/control/getNetworkInfo" + req.url = req.path + req.method = "GET" + + req.locals.jwtToken = "some-token" + + const jwtInfo = { + rateLimit: 100, + id: "5e3a0415eb29a962da2708c4" + } + + // Mock the call to the jwt library. + sandbox.stub(rateLimits.jwt, "verify").returns(jwtInfo) + + for (let i = 0; i < 150; i++) { + next.reset() // reset the stubbed next() function. + + await rateLimits.rateLimitSimple(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + //console.log(`req.locals after test: ${util.inspect(req.locals)}`) + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + false, + `next should NOT be called if rate limit was triggered.` + ) + }) + }) }) // Generates a Basic authorization header. From 380acbbfa4e078ddb1006360f19da023ce29023c Mon Sep 17 00:00:00 2001 From: danielhumgon Date: Fri, 14 Feb 2020 13:02:58 -0400 Subject: [PATCH 27/30] refactor(blockchain.js): Refactored blockchain.js library [WIP] --- src/routes/v3/full-node/blockchain2.js | 31 +- test/v3/blockchain2.js | 2519 ++++++++++++++---------- 2 files changed, 1478 insertions(+), 1072 deletions(-) diff --git a/src/routes/v3/full-node/blockchain2.js b/src/routes/v3/full-node/blockchain2.js index a84c70e..906707e 100644 --- a/src/routes/v3/full-node/blockchain2.js +++ b/src/routes/v3/full-node/blockchain2.js @@ -36,19 +36,22 @@ class Blockchain { this.router.get("/getBlockchainInfo", this.getBlockchainInfo) this.router.get("/getBlockCount", this.getBlockCount) this.router.get("/getBlockHeader/:hash", this.getBlockHeaderSingle) - this.router.post("/getBlockHeader", getBlockHeaderBulk) - this.router.get("/getChainTips", getChainTips) - this.router.get("/getDifficulty", getDifficulty) - this.router.get("/getMempoolEntry/:txid", getMempoolEntrySingle) - this.router.post("/getMempoolEntry", getMempoolEntryBulk) - this.router.get("/getMempoolAncestors/:txid", getMempoolAncestorsSingle) - this.router.get("/getMempoolInfo", getMempoolInfo) - this.router.get("/getRawMempool", getRawMempool) - this.router.get("/getTxOut/:txid/:n", getTxOut) - this.router.get("/getTxOutProof/:txid", getTxOutProofSingle) - this.router.post("/getTxOutProof", getTxOutProofBulk) - this.router.get("/verifyTxOutProof/:proof", verifyTxOutProofSingle) - this.router.post("/verifyTxOutProof", verifyTxOutProofBulk) + this.router.post("/getBlockHeader", this.getBlockHeaderBulk) + this.router.get("/getChainTips", this.getChainTips) + this.router.get("/getDifficulty", this.getDifficulty) + this.router.get("/getMempoolEntry/:txid", this.getMempoolEntrySingle) + this.router.post("/getMempoolEntry", this.getMempoolEntryBulk) + this.router.get( + "/getMempoolAncestors/:txid", + this.getMempoolAncestorsSingle + ) + this.router.get("/getMempoolInfo", this.getMempoolInfo) + this.router.get("/getRawMempool", this.getRawMempool) + this.router.get("/getTxOut/:txid/:n", this.getTxOut) + this.router.get("/getTxOutProof/:txid", this.getTxOutProofSingle) + this.router.post("/getTxOutProof", this.getTxOutProofBulk) + this.router.get("/verifyTxOutProof/:proof", this.verifyTxOutProofSingle) + this.router.post("/verifyTxOutProof", this.verifyTxOutProofBulk) } root(req, res, next) { @@ -532,7 +535,7 @@ class Blockchain { options.data.method = "getmempoolancestors" options.data.params = [txid, verbose] - const response = await this.axios.request(requestConfig) + const response = await this.axios.request(options) // console.log(`response: ${util.inspect(response)}`) return res.json(response.data.result) diff --git a/test/v3/blockchain2.js b/test/v3/blockchain2.js index debaa2d..d74918d 100644 --- a/test/v3/blockchain2.js +++ b/test/v3/blockchain2.js @@ -10,9 +10,7 @@ const chai = require("chai") const assert = chai.assert -const nock = require("nock") // HTTP mocking const sinon = require("sinon") -// const blockchainRoute = require("../../src/routes/v3/full-node/blockchain") const Blockchain = require("../../src/routes/v3/full-node/blockchain2") const uut = new Blockchain() @@ -62,18 +60,11 @@ describe("#BlockchainRouter2", () => { // Explicitly reset the parmas and body. req.params = {} req.body = {} - - // Activate nock if it's inactive. - if (!nock.isActive()) nock.activate() - sandbox = sinon.createSandbox() }) afterEach(() => { - // Clean up HTTP mocks. - nock.cleanAll() // clear interceptor list. - nock.restore() - + // Restore Sandbox sandbox.restore() }) @@ -97,9 +88,6 @@ describe("#BlockchainRouter2", () => { }) describe("getBestBlockHash()", () => { - // block route handler. - // const getBestBlockHash = blockchainRoute.testableComponents.getBestBlockHash - it("should throw 503 when network issues", async () => { // Save the existing RPC URL. const savedUrl2 = process.env.RPC_BASEURL @@ -165,12 +153,743 @@ describe("#BlockchainRouter2", () => { assert.isString(result) assert.equal(result.length, 64, "Hash string is fixed length") }) + + describe("getBlockchainInfo()", () => { + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await uut.getBlockchainInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getBestBlockHash(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getBestBlockHash(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getBlockchainInfo", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockBlockchainInfo } }) + } + + const result = await uut.getBlockchainInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "chain", + "blocks", + "headers", + "bestblockhash", + "difficulty", + "mediantime", + "verificationprogress", + "chainwork", + "pruned", + "softforks", + "bip9_softforks" + ]) + }) + }) + + describe("getBlockCount()", () => { + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await uut.getBlockCount(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getBlockCount(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getBlockCount(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getBlockCount", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: 126769 } }) + } + + const result = await uut.getBlockCount(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isNumber(result) + }) + }) }) - describe("getBlockchainInfo()", () => { - // block route handler. - // const getBlockchainInfo = - // blockchainRoute.testableComponents.getBlockchainInfo + describe("getBlockHeaderSingle()", async () => { + it("should throw 400 error if hash is missing", async () => { + const result = await uut.getBlockHeaderSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "hash can not be empty") + }) + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.hash = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await uut.getBlockHeaderSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Network error: Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.params.hash = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await uut.getBlockHeaderSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.params.hash = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await uut.getBlockHeaderSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("should GET block header", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox.stub(uut.axios, "request").resolves({ + data: { + result: + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c" + } + }) + } + + req.params.hash = + "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" + + const result = await uut.getBlockHeaderSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isString(result) + assert.equal( + result, + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c" + ) + }) + + it("should GET verbose block header", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockBlockHeader } }) + } + + req.query.verbose = true + req.params.hash = + "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" + + const result = await uut.getBlockHeaderSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, [ + "hash", + "confirmations", + "height", + "version", + "versionHex", + "merkleroot", + "time", + "mediantime", + "nonce", + "bits", + "difficulty", + "chainwork", + "previousblockhash", + "nextblockhash" + ]) + }) + }) + + // + + describe("#getBlockHeaderBulk", () => { + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await uut.getBlockHeaderBulk(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 hash", async () => { + req.body.hashes = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await uut.getBlockHeaderBulk(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 uut.getBlockHeaderBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw a 400 error for an invalid hash", async () => { + req.body.hashes = ["badHash"] + + await uut.getBlockHeaderBulk(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.hashes = [ + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + ] + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await uut.getBlockHeaderBulk(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("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.body.hashes = [ + "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" + ] + + const result = await uut.getBlockHeaderBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.body.hashes = [ + "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" + ] + + const result = await uut.getBlockHeaderBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should get concise block header for a single hash", async () => { + req.body.hashes = [ + "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" + ] + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockBlockHeaderConcise } }) + } + + // Call the details API. + const result = await uut.getBlockHeaderBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.isArray(result) + assert.equal( + result[0], + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c" + ) + }) + + it("should get verbose block header for a single hash", async () => { + req.body = { + hashes: [ + "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" + ], + verbose: true + } + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockBlockHeader } }) + } + + // Call the details API. + const result = await uut.getBlockHeaderBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "hash", + "confirmations", + "height", + "version", + "versionHex", + "merkleroot", + "time", + "mediantime", + "nonce", + "bits", + "difficulty", + "chainwork", + "previousblockhash", + "nextblockhash" + ]) + }) + + it("should get details for multiple block heights", async () => { + req.body.hashes = [ + "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0", + "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" + ] + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockBlockHeaderConcise } }) + } + + // Call the details API. + const result = await uut.getBlockHeaderBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "2 outputs for 2 inputs") + }) + }) + describe("getChainTips()", () => { + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await uut.getChainTips(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getChainTips(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getChainTips(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getChainTips", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockChainTips } }) + } + + const result = await uut.getChainTips(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], ["height", "hash", "branchlen", "status"]) + }) + }) + describe("getDifficulty()", () => { + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await uut.getDifficulty(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getDifficulty(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getDifficulty(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getDifficulty", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: 4049809.205246544 } }) + } + + const result = await uut.getDifficulty(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isNumber(result) + }) + }) + describe("getMempoolInfo()", () => { + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await uut.getMempoolInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getMempoolInfo(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getMempoolInfo(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getMempoolInfo", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockMempoolInfo } }) + } + + const result = await uut.getMempoolInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "result", + "bytes", + "usage", + "maxmempool", + "mempoolminfree" + ]) + }) + }) + describe("getRawMempool()", () => { + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await uut.getRawMempool(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getRawMempool(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getRawMempool(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getMempoolInfo", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockRawMempool } }) + } + + const result = await uut.getRawMempool(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + // Not sure what other assertions should be made here. + }) + }) + describe("getMempoolEntrySingle()", () => { + it("should throw 400 if txid is empty", async () => { + const result = await uut.getMempoolEntrySingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) it("should throw 503 when network issues", async () => { // Save the existing RPC URL. @@ -179,7 +898,288 @@ describe("#BlockchainRouter2", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - const result = await uut.getBlockchainInfo(req, res) + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await uut.getMempoolEntrySingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await uut.getMempoolEntrySingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await uut.getMempoolEntrySingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getMempoolEntry", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox.stub(uut.axios, "request").resolves({ + data: { result: { error: "Transaction not in mempool" } } + }) + } + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await uut.getMempoolEntrySingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.isString(result.error) + assert.equal(result.error, "Transaction not in mempool") + }) + }) + describe("#getMempoolEntryBulk", () => { + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await uut.getMempoolEntryBulk(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 = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await uut.getMempoolEntryBulk(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 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.txids = testArray + + const result = await uut.getMempoolEntryBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.body.txids = [ + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + ] + + const result = await uut.getMempoolEntryBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.body.txids = [ + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + ] + + const result = await uut.getMempoolEntryBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + // Only execute on integration tests. + if (process.env.TEST !== "unit") { + // Dev-note: This test passes because it expects an error. TXIDs do not + // stay in the mempool for long, so it does not work well for a unit or + // integration test. + it("should retrieve single mempool entry", async () => { + req.body.txids = [ + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + ] + + const result = await uut.getMempoolEntryBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.isString(result.error) + assert.equal(result.error, "Transaction not in mempool") + }) + + // Dev-note: This test passes because it expects an error. TXIDs do not + // stay in the mempool for long, so it does not work well for a unit or + // integration test. + it("should retrieve multiple mempool entries", async () => { + req.body.txids = [ + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde`, + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + ] + + const result = await uut.getMempoolEntryBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.isString(result.error) + assert.equal(result.error, "Transaction not in mempool") + }) + } + }) + describe("getMempoolAncestorsSingle()", () => { + it("should throw 400 if txid is empty", async () => { + const result = await uut.getMempoolAncestorsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await uut.getMempoolAncestorsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await uut.getMempoolAncestorsSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await uut.getMempoolAncestorsSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("should GET /getMempoolAncestorsSingle", async () => { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockAncestors } }) + + req.params.txid = `bb0d349892d351da2767f8c45f6f7949713ff09bd12838d53e76158ddee3ce93` + + const result = await uut.getMempoolAncestorsSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + }) + }) + describe("getTxOut()", () => { + it("should throw 400 if txid is empty", async () => { + const result = await uut.getTxOut(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should throw 400 if n is empty", async () => { + req.params.txid = `sometxid` + const result = await uut.getTxOut(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "n can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.n = 0 + + const result = await uut.getTxOut(req, res) //console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. @@ -197,7 +1197,11 @@ describe("#BlockchainRouter2", () => { // Mock the timeout error. sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - const result = await uut.getBestBlockHash(req, res) + req.params.txid = `197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d` + req.params.n = 0 + req.query.include_mempool = "true" + + const result = await uut.getTxOut(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") @@ -207,12 +1211,115 @@ describe("#BlockchainRouter2", () => { "Error message expected" ) }) - it("returns proper error when downstream service is down", async () => { // Mock the timeout error. sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - const result = await uut.getBestBlockHash(req, res) + req.params.txid = `197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d` + req.params.n = 0 + req.query.include_mempool = "true" + + const result = await uut.getTxOut(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + // This test can only run for unit tests. See TODO at the top of this file. + it("should GET /getTxOut", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockTxOut } }) + } + + req.params.txid = `197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d` + req.params.n = 0 + req.query.include_mempool = "true" + + const result = await uut.getTxOut(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, [ + "bestblock", + "confirmations", + "value", + "scriptPubKey", + "coinbase" + ]) + assert.hasAllKeys(result.scriptPubKey, [ + "asm", + "hex", + "reqSigs", + "type", + "addresses" + ]) + assert.isArray(result.scriptPubKey.addresses) + }) + }) + describe("getTxOutProofSingle()", () => { + it("should throw 400 if txid is empty", async () => { + const result = await uut.getTxOutProofSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await uut.getTxOutProofSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.params.txid = `197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d` + req.params.n = 0 + req.query.include_mempool = "true" + + const result = await uut.getTxOutProofSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.params.txid = `197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d` + req.params.n = 0 + req.query.include_mempool = "true" + + const result = await uut.getTxOutProofSingle(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") @@ -223,1049 +1330,345 @@ describe("#BlockchainRouter2", () => { ) }) - it("should GET /getBlockchainInfo", async () => { + it("should GET /getTxOutProof", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: mockData.mockBlockchainInfo }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockTxOutProof } }) } - const result = await uut.getBlockchainInfo(req, res) - //console.log(`result: ${util.inspect(result)}`) + req.params.txid = `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` - assert.hasAnyKeys(result, [ - "chain", - "blocks", - "headers", - "bestblockhash", - "difficulty", - "mediantime", - "verificationprogress", - "chainwork", - "pruned", - "softforks", - "bip9_softforks" - ]) + const result = await uut.getTxOutProofSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isString(result) }) }) + describe("#getTxOutProofBulk", () => { + it("should throw an error for an empty body", async () => { + req.body = {} - // describe("getBlockCount()", () => { - // // block route handler. - // const getBlockCount = blockchainRoute.testableComponents.getBlockCount - // - // it("should throw 503 when network issues", async () => { - // // Save the existing RPC URL. - // const savedUrl2 = process.env.RPC_BASEURL - // - // // Manipulate the URL to cause a 500 network error. - // process.env.RPC_BASEURL = "http://fakeurl/api/" - // - // const result = await getBlockCount(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // // Restore the saved URL. - // process.env.RPC_BASEURL = savedUrl2 - // - // assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - // assert.include( - // result.error, - // "Could not communicate with full node", - // "Error message expected" - // ) - // }) - // - // it("should GET /getBlockCount", async () => { - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: 126769 }) - // } - // - // const result = await getBlockCount(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.isNumber(result) - // }) - // }) - // - // describe("getBlockHeaderSingle()", async () => { - // const getBlockHeader = - // blockchainRoute.testableComponents.getBlockHeaderSingle - // - // it("should throw 400 error if hash is missing", async () => { - // const result = await getBlockHeader(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.include(result.error, "hash can not be empty") - // }) - // - // it("should throw 503 when network issues", async () => { - // // Save the existing RPC URL. - // const savedUrl2 = process.env.RPC_BASEURL - // - // // Manipulate the URL to cause a 500 network error. - // process.env.RPC_BASEURL = "http://fakeurl/api/" - // - // req.params.hash = - // "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" - // - // const result = await getBlockHeader(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // // Restore the saved URL. - // process.env.RPC_BASEURL = savedUrl2 - // - // assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - // assert.include( - // result.error, - // "Network error: Could not communicate with full node", - // "Error message expected" - // ) - // }) - // - // it("should GET block header", async () => { - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { - // result: - // "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c" - // }) - // } - // - // req.params.hash = - // "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - // - // const result = await getBlockHeader(req, res) - // // console.log(`result: ${util.inspect(result)}`) - // - // assert.isString(result) - // assert.equal( - // result, - // "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c" - // ) - // }) - // - // it("should GET verbose block header", async () => { - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: mockData.mockBlockHeader }) - // } - // - // req.query.verbose = true - // req.params.hash = - // "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - // - // const result = await getBlockHeader(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, [ - // "hash", - // "confirmations", - // "height", - // "version", - // "versionHex", - // "merkleroot", - // "time", - // "mediantime", - // "nonce", - // "bits", - // "difficulty", - // "chainwork", - // "previousblockhash", - // "nextblockhash" - // ]) - // }) - // }) - // - // describe("#getBlockHeaderBulk", () => { - // // route handler. - // const getBlockHeaderBulk = - // blockchainRoute.testableComponents.getBlockHeaderBulk - // - // it("should throw an error for an empty body", async () => { - // req.body = {} - // - // const result = await getBlockHeaderBulk(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 hash", async () => { - // req.body.hashes = - // "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" - // - // const result = await getBlockHeaderBulk(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 getBlockHeaderBulk(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.include(result.error, "Array too large") - // }) - // - // it("should throw a 400 error for an invalid hash", async () => { - // req.body.hashes = ["badHash"] - // - // await getBlockHeaderBulk(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.hashes = [ - // "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" - // ] - // - // // Switch the Insight URL to something that will error out. - // process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" - // - // const result = await getBlockHeaderBulk(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 concise block header for a single hash", async () => { - // req.body.hashes = [ - // "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - // ] - // - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: mockData.mockBlockHeaderConcise }) - // } - // - // // Call the details API. - // const result = await getBlockHeaderBulk(req, res) - // // console.log(`result: ${util.inspect(result)}`) - // - // // Assert that required fields exist in the returned object. - // assert.isArray(result) - // assert.equal( - // result[0], - // "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c" - // ) - // }) - // - // it("should get verbose block header for a single hash", async () => { - // req.body = { - // hashes: [ - // "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - // ], - // verbose: true - // } - // - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: mockData.mockBlockHeader }) - // } - // - // // Call the details API. - // const result = await getBlockHeaderBulk(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // // Assert that required fields exist in the returned object. - // assert.isArray(result) - // assert.hasAllKeys(result[0], [ - // "hash", - // "confirmations", - // "height", - // "version", - // "versionHex", - // "merkleroot", - // "time", - // "mediantime", - // "nonce", - // "bits", - // "difficulty", - // "chainwork", - // "previousblockhash", - // "nextblockhash" - // ]) - // }) - // - // it("should get details for multiple block heights", async () => { - // req.body.hashes = [ - // "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0", - // "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - // ] - // - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .times(2) - // .reply(200, { result: mockData.mockBlockHeaderConcise }) - // } - // - // // Call the details API. - // const result = await getBlockHeaderBulk(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.isArray(result) - // assert.equal(result.length, 2, "2 outputs for 2 inputs") - // }) - // }) - // - // describe("getChainTips()", () => { - // // block route handler. - // const getChainTips = blockchainRoute.testableComponents.getChainTips - // - // it("should throw 503 when network issues", async () => { - // // Save the existing RPC URL. - // const savedUrl2 = process.env.RPC_BASEURL - // - // // Manipulate the URL to cause a 500 network error. - // process.env.RPC_BASEURL = "http://fakeurl/api/" - // - // const result = await getChainTips(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // // Restore the saved URL. - // process.env.RPC_BASEURL = savedUrl2 - // - // assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - // assert.include( - // result.error, - // "Could not communicate with full node", - // "Error message expected" - // ) - // }) - // - // it("should GET /getChainTips", async () => { - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: mockData.mockChainTips }) - // } - // - // const result = await getChainTips(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.isArray(result) - // assert.hasAnyKeys(result[0], ["height", "hash", "branchlen", "status"]) - // }) - // }) - // - // describe("getDifficulty()", () => { - // // block route handler. - // const getDifficulty = blockchainRoute.testableComponents.getDifficulty - // - // it("should throw 503 when network issues", async () => { - // // Save the existing RPC URL. - // const savedUrl2 = process.env.RPC_BASEURL - // - // // Manipulate the URL to cause a 500 network error. - // process.env.RPC_BASEURL = "http://fakeurl/api/" - // - // const result = await getDifficulty(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // // Restore the saved URL. - // process.env.RPC_BASEURL = savedUrl2 - // - // assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - // assert.include( - // result.error, - // "Could not communicate with full node", - // "Error message expected" - // ) - // }) - // - // it("should GET /getDifficulty", async () => { - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: 4049809.205246544 }) - // } - // - // const result = await getDifficulty(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.isNumber(result) - // }) - // }) - // - // describe("getMempoolInfo()", () => { - // // block route handler. - // const getMempoolInfo = blockchainRoute.testableComponents.getMempoolInfo - // - // it("should throw 503 when network issues", async () => { - // // Save the existing RPC URL. - // const savedUrl2 = process.env.RPC_BASEURL - // - // // Manipulate the URL to cause a 500 network error. - // process.env.RPC_BASEURL = "http://fakeurl/api/" - // - // const result = await getMempoolInfo(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // // Restore the saved URL. - // process.env.RPC_BASEURL = savedUrl2 - // - // assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - // assert.include( - // result.error, - // "Could not communicate with full node", - // "Error message expected" - // ) - // }) - // - // it("should GET /getMempoolInfo", async () => { - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: mockData.mockMempoolInfo }) - // } - // - // const result = await getMempoolInfo(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAnyKeys(result, [ - // "result", - // "bytes", - // "usage", - // "maxmempool", - // "mempoolminfree" - // ]) - // }) - // }) - // - // describe("getRawMempool()", () => { - // // block route handler. - // const getRawMempool = blockchainRoute.testableComponents.getRawMempool - // - // it("should throw 503 when network issues", async () => { - // // Save the existing RPC URL. - // const savedUrl2 = process.env.RPC_BASEURL - // - // // Manipulate the URL to cause a 500 network error. - // process.env.RPC_BASEURL = "http://fakeurl/api/" - // - // const result = await getRawMempool(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // // Restore the saved URL. - // process.env.RPC_BASEURL = savedUrl2 - // - // assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - // assert.include( - // result.error, - // "Could not communicate with full node", - // "Error message expected" - // ) - // }) - // - // it("should GET /getMempoolInfo", async () => { - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: mockData.mockRawMempool }) - // } - // - // const result = await getRawMempool(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.isArray(result) - // // Not sure what other assertions should be made here. - // }) - // }) - // - // describe("getMempoolEntry()", () => { - // // block route handler. - // const getMempoolEntry = - // blockchainRoute.testableComponents.getMempoolEntrySingle - // - // it("should throw 400 if txid is empty", async () => { - // const result = await getMempoolEntry(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.include(result.error, "txid can not be empty") - // }) - // - // it("should throw 503 when network issues", async () => { - // // Save the existing RPC URL. - // const savedUrl2 = process.env.RPC_BASEURL - // - // // Manipulate the URL to cause a 500 network error. - // process.env.RPC_BASEURL = "http://fakeurl/api/" - // - // req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` - // - // const result = await getMempoolEntry(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // // Restore the saved URL. - // process.env.RPC_BASEURL = savedUrl2 - // - // assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - // assert.include( - // result.error, - // "Could not communicate with full node", - // "Error message expected" - // ) - // }) - // - // it("should GET /getMempoolEntry", async () => { - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: { error: "Transaction not in mempool" } }) - // } - // - // req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` - // - // const result = await getMempoolEntry(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.isString(result.error) - // assert.equal(result.error, "Transaction not in mempool") - // }) - // }) - // - // describe("#getMempoolEntryBulk", () => { - // // route handler. - // const getMempoolEntryBulk = - // blockchainRoute.testableComponents.getMempoolEntryBulk - // - // it("should throw an error for an empty body", async () => { - // req.body = {} - // - // const result = await getMempoolEntryBulk(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 = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` - // - // const result = await getMempoolEntryBulk(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 400 error if addresses array is too large", async () => { - // const testArray = [] - // for (var i = 0; i < 25; i++) testArray.push("") - // - // req.body.txids = testArray - // - // const result = await getMempoolEntryBulk(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.include(result.error, "Array too large") - // }) - // - // // Only execute on integration tests. - // if (process.env.TEST !== "unit") { - // // Dev-note: This test passes because it expects an error. TXIDs do not - // // stay in the mempool for long, so it does not work well for a unit or - // // integration test. - // it("should retrieve single mempool entry", async () => { - // req.body.txids = [ - // `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` - // ] - // - // const result = await getMempoolEntryBulk(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.isString(result.error) - // assert.equal(result.error, "Transaction not in mempool") - // }) - // - // // Dev-note: This test passes because it expects an error. TXIDs do not - // // stay in the mempool for long, so it does not work well for a unit or - // // integration test. - // it("should retrieve multiple mempool entries", async () => { - // req.body.txids = [ - // `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde`, - // `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` - // ] - // - // const result = await getMempoolEntryBulk(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.isString(result.error) - // assert.equal(result.error, "Transaction not in mempool") - // }) - // } - // }) - // - // describe("getMempoolAncestorsSingle()", () => { - // // block route handler. - // const getMempoolAncestorsSingle = - // blockchainRoute.testableComponents.getMempoolAncestorsSingle - // - // it("should throw 400 if txid is empty", async () => { - // const result = await getMempoolAncestorsSingle(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.include(result.error, "txid can not be empty") - // }) - // - // it("should throw 503 when network issues", async () => { - // // Save the existing RPC URL. - // const savedUrl2 = process.env.RPC_BASEURL - // - // // Manipulate the URL to cause a 500 network error. - // process.env.RPC_BASEURL = "http://fakeurl/api/" - // - // req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` - // - // const result = await getMempoolAncestorsSingle(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // // Restore the saved URL. - // process.env.RPC_BASEURL = savedUrl2 - // - // assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - // assert.include( - // result.error, - // "Could not communicate with full node", - // "Error message expected" - // ) - // }) - // - // it("should GET /getMempoolAncestorsSingle", async () => { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: mockData.mockAncestors }) - // - // req.params.txid = `bb0d349892d351da2767f8c45f6f7949713ff09bd12838d53e76158ddee3ce93` - // - // const result = await getMempoolAncestorsSingle(req, res) - // // console.log(`result: ${util.inspect(result)}`) - // - // assert.isArray(result) - // }) - // }) - // - // describe("getTxOut()", () => { - // // block route handler. - // const getTxOut = blockchainRoute.testableComponents.getTxOut - // - // it("should throw 400 if txid is empty", async () => { - // const result = await getTxOut(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.include(result.error, "txid can not be empty") - // }) - // - // it("should throw 400 if n is empty", async () => { - // req.params.txid = `sometxid` - // const result = await getTxOut(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.include(result.error, "n can not be empty") - // }) - // - // it("should throw 503 when network issues", async () => { - // // Save the existing RPC URL. - // const savedUrl2 = process.env.RPC_BASEURL - // - // // Manipulate the URL to cause a 500 network error. - // process.env.RPC_BASEURL = "http://fakeurl/api/" - // - // req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` - // req.params.n = 0 - // - // const result = await getTxOut(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // // Restore the saved URL. - // process.env.RPC_BASEURL = savedUrl2 - // - // assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - // assert.include( - // result.error, - // "Could not communicate with full node", - // "Error message expected" - // ) - // }) - // - // // This test can only run for unit tests. See TODO at the top of this file. - // it("should GET /getTxOut", async () => { - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: mockData.mockTxOut }) - // } - // - // req.params.txid = `197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d` - // req.params.n = 0 - // req.query.include_mempool = "true" - // - // const result = await getTxOut(req, res) - // // console.log(`result: ${JSON.stringify(result, null, 2)}`) - // - // assert.hasAllKeys(result, [ - // "bestblock", - // "confirmations", - // "value", - // "scriptPubKey", - // "coinbase" - // ]) - // assert.hasAllKeys(result.scriptPubKey, [ - // "asm", - // "hex", - // "reqSigs", - // "type", - // "addresses" - // ]) - // assert.isArray(result.scriptPubKey.addresses) - // }) - // }) - // - // describe("getTxOutProof()", () => { - // const getTxOutProof = blockchainRoute.testableComponents.getTxOutProofSingle - // - // it("should throw 400 if txid is empty", async () => { - // const result = await getTxOutProof(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.include(result.error, "txid can not be empty") - // }) - // - // it("should throw 503 when network issues", async () => { - // // Save the existing RPC URL. - // const savedUrl2 = process.env.RPC_BASEURL - // - // // Manipulate the URL to cause a 500 network error. - // process.env.RPC_BASEURL = "http://fakeurl/api/" - // - // req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` - // - // const result = await getTxOutProof(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // // Restore the saved URL. - // process.env.RPC_BASEURL = savedUrl2 - // - // assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - // assert.include( - // result.error, - // "Could not communicate with full node", - // "Error message expected" - // ) - // }) - // - // it("should GET /getTxOutProof", async () => { - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: mockData.mockTxOutProof }) - // } - // - // req.params.txid = `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` - // - // const result = await getTxOutProof(req, res) - // // console.log(`result: ${JSON.stringify(result, null, 2)}`) - // - // assert.isString(result) - // }) - // }) - // - // describe("#getTxOutProofBulk", () => { - // // route handler. - // const getTxOutProofBulk = - // blockchainRoute.testableComponents.getTxOutProofBulk - // - // it("should throw an error for an empty body", async () => { - // req.body = {} - // - // const result = await getTxOutProofBulk(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 = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` - // - // const result = await getTxOutProofBulk(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 400 error if addresses array is too large", async () => { - // const testArray = [] - // for (var i = 0; i < 25; i++) testArray.push("") - // - // req.body.txids = testArray - // - // const result = await getTxOutProofBulk(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.include(result.error, "Array too large") - // }) - // - // it("should GET proof for single txid", async () => { - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: mockData.mockTxOutProof }) - // } - // - // req.body.txids = [ - // `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` - // ] - // - // const result = await getTxOutProofBulk(req, res) - // //console.log(`result: ${JSON.stringify(result, null, 2)}`) - // - // assert.isArray(result) - // assert.isString(result[0]) - // }) - // - // it("should GET proof for multiple txids", async () => { - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .times(2) - // .reply(200, { result: mockData.mockTxOutProof }) - // } - // - // req.body.txids = [ - // `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266`, - // `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` - // ] - // - // const result = await getTxOutProofBulk(req, res) - // //console.log(`result: ${JSON.stringify(result, null, 2)}`) - // - // assert.isArray(result) - // assert.equal(result.length, 2, "Correct length of returned array") - // }) - // }) - // - // describe("verifyTxOutProof()", () => { - // const verifyTxOutProof = - // blockchainRoute.testableComponents.verifyTxOutProofSingle - // - // it("should throw 400 if proof is empty", async () => { - // const result = await verifyTxOutProof(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.include(result.error, "proof can not be empty") - // }) - // - // it("should throw 503 when network issues", async () => { - // // Save the existing RPC URL. - // const savedUrl2 = process.env.RPC_BASEURL - // - // // Manipulate the URL to cause a 500 network error. - // process.env.RPC_BASEURL = "http://fakeurl/api/" - // - // req.params.proof = mockData.mockTxOutProof - // - // const result = await verifyTxOutProof(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // // Restore the saved URL. - // process.env.RPC_BASEURL = savedUrl2 - // - // assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - // assert.include( - // result.error, - // "Could not communicate with full node", - // "Error message expected" - // ) - // }) - // - // it("should GET /verifyTxOutProof", async () => { - // const expected = - // "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" - // - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: [expected] }) - // } - // - // req.params.proof = - // "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" - // - // const result = await verifyTxOutProof(req, res) - // //console.log(`result: ${JSON.stringify(result, null, 2)}`) - // - // assert.isArray(result) - // assert.isString(result[0]) - // assert.equal(result[0], expected) - // }) - // }) - // - // describe("#verifyTxOutProofBulk", () => { - // // route handler. - // const verifyTxOutProofBulk = - // blockchainRoute.testableComponents.verifyTxOutProofBulk - // - // it("should throw an error for an empty body", async () => { - // req.body = {} - // - // const result = await verifyTxOutProofBulk(req, res) - // - // assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") - // assert.include( - // result.error, - // "proofs needs to be an array", - // "Proper error message" - // ) - // }) - // - // it("should error on non-array single txid", async () => { - // req.body.proofs = mockData.mockTxOutProof - // - // const result = await verifyTxOutProofBulk(req, res) - // - // assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") - // assert.include( - // result.error, - // "proofs 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.proofs = testArray - // - // const result = await verifyTxOutProofBulk(req, res) - // //console.log(`result: ${util.inspect(result)}`) - // - // assert.hasAllKeys(result, ["error"]) - // assert.include(result.error, "Array too large") - // }) - // - // it("should get single proof", async () => { - // const expected = - // "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" - // - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .reply(200, { result: [expected] }) - // } - // - // req.body.proofs = [ - // "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" - // ] - // - // const result = await verifyTxOutProofBulk(req, res) - // //console.log(`result: ${JSON.stringify(result, null, 2)}`) - // - // assert.isArray(result) - // assert.isString(result[0]) - // assert.equal(result[0], expected) - // }) - // - // it("should get multiple proofs", async () => { - // const expected = - // "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" - // - // // Mock the RPC call for unit tests. - // if (process.env.TEST === "unit") { - // nock(`${process.env.RPC_BASEURL}`) - // .post(uri => uri.includes("/")) - // .times(2) - // .reply(200, { result: [expected] }) - // } - // - // req.body.proofs = [ - // "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700", - // "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" - // ] - // - // const result = await verifyTxOutProofBulk(req, res) - // //console.log(`result: ${JSON.stringify(result, null, 2)}`) - // - // assert.isArray(result) - // assert.isString(result[0]) - // assert.equal(result[0], expected) - // assert.equal(result.length, 2) - // }) - // }) + const result = await uut.getTxOutProofBulk(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 = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await uut.getTxOutProofBulk(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 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.txids = testArray + + const result = await uut.getTxOutProofBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.body.txids = [ + `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` + ] + + const result = await uut.getTxOutProofBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.body.txids = [ + `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` + ] + + const result = await uut.getTxOutProofBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET proof for single txid", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockTxOutProof } }) + } + + req.body.txids = [ + `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` + ] + + const result = await uut.getTxOutProofBulk(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isString(result[0]) + }) + + it("should GET proof for multiple txids", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockTxOutProof } }) + } + + req.body.txids = [ + `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266`, + `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` + ] + + const result = await uut.getTxOutProofBulk(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "Correct length of returned array") + }) + }) + describe("verifyTxOutProofSingle()", () => { + it("should throw 400 if proof is empty", async () => { + const result = await uut.verifyTxOutProofSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "proof can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.proof = mockData.mockTxOutProof + + const result = await uut.verifyTxOutProofSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.params.proof = + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" + + const result = await uut.verifyTxOutProofSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.params.proof = + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" + + const result = await uut.verifyTxOutProofSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("should GET /verifyTxOutProof", async () => { + const expected = + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: [expected] } }) + } + + req.params.proof = + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" + + const result = await uut.verifyTxOutProofSingle(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isString(result[0]) + assert.equal(result[0], expected) + }) + }) + describe("#verifyTxOutProofBulk", () => { + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await uut.verifyTxOutProofBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "proofs needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single txid", async () => { + req.body.proofs = mockData.mockTxOutProof + + const result = await uut.verifyTxOutProofBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "proofs 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.proofs = testArray + + const result = await uut.verifyTxOutProofBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.body.proofs = [ + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" + ] + const result = await uut.verifyTxOutProofBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.body.proofs = [ + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" + ] + const result = await uut.verifyTxOutProofBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should get single proof", async () => { + const expected = + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: [expected] } }) + } + + req.body.proofs = [ + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" + ] + + const result = await uut.verifyTxOutProofBulk(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isString(result[0]) + assert.equal(result[0], expected) + }) + + it("should get multiple proofs", async () => { + const expected = + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: [expected] } }) + } + + req.body.proofs = [ + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700", + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" + ] + + const result = await uut.verifyTxOutProofBulk(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isString(result[0]) + assert.equal(result[0], expected) + assert.equal(result.length, 2) + }) + }) }) From cd8724b4b2a8e307906788b3240a59f781f7e39e Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sat, 15 Feb 2020 19:01:11 -0800 Subject: [PATCH 28/30] linting and formatting fix --- src/routes/v3/full-node/blockchain2.js | 80 ++--- test/v3/blockchain2.js | 401 +++++++++++++------------ 2 files changed, 250 insertions(+), 231 deletions(-) diff --git a/src/routes/v3/full-node/blockchain2.js b/src/routes/v3/full-node/blockchain2.js index 906707e..97a0d8f 100644 --- a/src/routes/v3/full-node/blockchain2.js +++ b/src/routes/v3/full-node/blockchain2.js @@ -97,7 +97,7 @@ class Blockchain { return res.json(response.data.result) } catch (err) { // Write out error to error log. - wlogger.error(`Error in blockchain.ts/getBestBlockHash().`, err) + wlogger.error("Error in blockchain.ts/getBestBlockHash().", err) return this.errorHandler(err, res) } @@ -141,7 +141,7 @@ class Blockchain { return res.json(response.data.result) } catch (err) { // Write out error to error log. - wlogger.error(`Error in blockchain.ts/getBlockchainInfo().`, err) + wlogger.error("Error in blockchain.ts/getBlockchainInfo().", err) return this.errorHandler(err, res) } @@ -171,8 +171,8 @@ class Blockchain { return res.json(response.data.result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getBlockCount().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getBlockCount().", err) return this.errorHandler(err, res) } @@ -231,8 +231,8 @@ class Blockchain { return res.json(response.data.result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getBlockHeaderSingle().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getBlockHeaderSingle().", err) return this.errorHandler(err, res) } @@ -284,12 +284,12 @@ class Blockchain { 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.` + error: "Array too large." }) } wlogger.debug( - `Executing blockchain/getBlockHeaderBulk with these hashes: `, + "Executing blockchain/getBlockHeaderBulk with these hashes: ", hashes ) @@ -324,8 +324,8 @@ class Blockchain { return res.json(result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getBlockHeaderBulk().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getBlockHeaderBulk().", err) return this.errorHandler(err, res) } @@ -355,8 +355,8 @@ class Blockchain { return res.json(response.data.result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getChainTips().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getChainTips().", err) return this.errorHandler(err, res) } @@ -387,8 +387,8 @@ class Blockchain { return res.json(response.data.result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getDifficulty().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getDifficulty().", err) return this.errorHandler(err, res) } @@ -426,8 +426,8 @@ class Blockchain { return res.json(response.data.result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getMempoolEntrySingle().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getMempoolEntrySingle().", err) return this.errorHandler(err, res) } @@ -457,12 +457,12 @@ class Blockchain { 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.` + error: "Array too large." }) } wlogger.debug( - `Executing blockchain/getMempoolEntry with these txids: `, + "Executing blockchain/getMempoolEntry with these txids: ", txids ) @@ -497,8 +497,8 @@ class Blockchain { return res.json(result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getMempoolEntryBulk().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getMempoolEntryBulk().", err) return this.errorHandler(err, res) } @@ -541,8 +541,8 @@ class Blockchain { return res.json(response.data.result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getMempoolAncestorsSingle().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getMempoolAncestorsSingle().", err) return this.errorHandler(err, res) } @@ -571,8 +571,8 @@ class Blockchain { return res.json(response.data.result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getMempoolInfo().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getMempoolInfo().", err) return this.errorHandler(err, res) } @@ -618,8 +618,8 @@ class Blockchain { return res.json(response.data.result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getRawMempool().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getRawMempool().", err) return this.errorHandler(err, res) } @@ -674,8 +674,8 @@ class Blockchain { return res.json(response.data.result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getTxOut().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getTxOut().", err) return this.errorHandler(err, res) } @@ -714,8 +714,8 @@ class Blockchain { return res.json(response.data.result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getTxOutProofSingle().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getTxOutProofSingle().", err) return this.errorHandler(err, res) } @@ -738,7 +738,7 @@ class Blockchain { 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.` + error: "Array too large." }) } @@ -758,7 +758,7 @@ class Blockchain { } wlogger.debug( - `Executing blockchain/getTxOutProof with these txids: `, + "Executing blockchain/getTxOutProof with these txids: ", txids ) @@ -781,8 +781,8 @@ class Blockchain { return res.json(result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getTxOutProofBulk().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getTxOutProofBulk().", err) return this.errorHandler(err, res) } @@ -809,8 +809,8 @@ class Blockchain { return res.json(response.data.result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/verifyTxOutProofSingle().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/verifyTxOutProofSingle().", err) return this.errorHandler(err, res) } @@ -832,7 +832,7 @@ class Blockchain { if (!routeUtils.validateArraySize(req, proofs)) { res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 return res.json({ - error: `Array too large.` + error: "Array too large." }) } @@ -850,7 +850,7 @@ class Blockchain { } wlogger.debug( - `Executing blockchain/verifyTxOutProof with these proofs: `, + "Executing blockchain/verifyTxOutProof with these proofs: ", proofs ) @@ -873,8 +873,8 @@ class Blockchain { return res.json(result) } catch (err) { // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/verifyTxOutProofBulk().`, err) + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/verifyTxOutProofBulk().", err) return this.errorHandler(err, res) } diff --git a/test/v3/blockchain2.js b/test/v3/blockchain2.js index d74918d..956de95 100644 --- a/test/v3/blockchain2.js +++ b/test/v3/blockchain2.js @@ -31,7 +31,7 @@ describe("#BlockchainRouter2", () => { let sandbox // local node will be started in regtest mode on the port 48332 - //before(panda.runLocalNode) + // before(panda.runLocalNode) before(() => { // Save existing environment variables. @@ -70,7 +70,7 @@ describe("#BlockchainRouter2", () => { after(() => { // otherwise the panda will run forever - //process.exit() + // process.exit() // Restore any pre-existing environment variables. process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL @@ -153,149 +153,150 @@ describe("#BlockchainRouter2", () => { assert.isString(result) assert.equal(result.length, 64, "Hash string is fixed length") }) + }) - describe("getBlockchainInfo()", () => { - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL + describe("getBlockchainInfo()", () => { + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" - const result = await uut.getBlockchainInfo(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getBlockchainInfo(req, res) + // console.log(`result: ${util.inspect(result)}`) - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - const result = await uut.getBestBlockHash(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - const result = await uut.getBestBlockHash(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("should GET /getBlockchainInfo", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockBlockchainInfo } }) - } - - const result = await uut.getBlockchainInfo(req, res) - //console.log(`result: ${util.inspect(result)}`) - - assert.hasAnyKeys(result, [ - "chain", - "blocks", - "headers", - "bestblockhash", - "difficulty", - "mediantime", - "verificationprogress", - "chainwork", - "pruned", - "softforks", - "bip9_softforks" - ]) - }) + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) }) - describe("getBlockCount()", () => { - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" + const result = await uut.getBestBlockHash(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) - const result = await uut.getBlockCount(req, res) - //console.log(`result: ${util.inspect(result)}`) + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + const result = await uut.getBestBlockHash(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) - const result = await uut.getBlockCount(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + it("should GET /getBlockchainInfo", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockBlockchainInfo } }) + } - const result = await uut.getBlockCount(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) + const result = await uut.getBlockchainInfo(req, res) + // console.log(`result: ${util.inspect(result)}`) - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) + assert.hasAnyKeys(result, [ + "chain", + "blocks", + "headers", + "bestblockhash", + "difficulty", + "mediantime", + "verificationprogress", + "chainwork", + "pruned", + "softforks", + "bip9_softforks" + ]) + }) + }) - it("should GET /getBlockCount", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: 126769 } }) - } + describe("getBlockCount()", () => { + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL - const result = await uut.getBlockCount(req, res) - //console.log(`result: ${util.inspect(result)}`) + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" - assert.isNumber(result) - }) + const result = await uut.getBlockCount(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getBlockCount(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getBlockCount(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getBlockCount", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: 126769 } }) + } + + const result = await uut.getBlockCount(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isNumber(result) }) }) @@ -318,7 +319,7 @@ describe("#BlockchainRouter2", () => { "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" const result = await uut.getBlockHeaderSingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -401,7 +402,7 @@ describe("#BlockchainRouter2", () => { "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" const result = await uut.getBlockHeaderSingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, [ "hash", @@ -459,7 +460,7 @@ describe("#BlockchainRouter2", () => { req.body.hashes = testArray const result = await uut.getBlockHeaderBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "Array too large") @@ -575,7 +576,7 @@ describe("#BlockchainRouter2", () => { // Call the details API. const result = await uut.getBlockHeaderBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) // Assert that required fields exist in the returned object. assert.isArray(result) @@ -612,7 +613,7 @@ describe("#BlockchainRouter2", () => { // Call the details API. const result = await uut.getBlockHeaderBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.isArray(result) assert.equal(result.length, 2, "2 outputs for 2 inputs") @@ -627,7 +628,7 @@ describe("#BlockchainRouter2", () => { process.env.RPC_BASEURL = "http://fakeurl/api/" const result = await uut.getChainTips(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -677,7 +678,7 @@ describe("#BlockchainRouter2", () => { } const result = await uut.getChainTips(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.isArray(result) assert.hasAnyKeys(result[0], ["height", "hash", "branchlen", "status"]) @@ -692,7 +693,7 @@ describe("#BlockchainRouter2", () => { process.env.RPC_BASEURL = "http://fakeurl/api/" const result = await uut.getDifficulty(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -742,7 +743,7 @@ describe("#BlockchainRouter2", () => { } const result = await uut.getDifficulty(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.isNumber(result) }) @@ -756,7 +757,7 @@ describe("#BlockchainRouter2", () => { process.env.RPC_BASEURL = "http://fakeurl/api/" const result = await uut.getMempoolInfo(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -806,7 +807,7 @@ describe("#BlockchainRouter2", () => { } const result = await uut.getMempoolInfo(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAnyKeys(result, [ "result", @@ -826,7 +827,7 @@ describe("#BlockchainRouter2", () => { process.env.RPC_BASEURL = "http://fakeurl/api/" const result = await uut.getRawMempool(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -876,7 +877,7 @@ describe("#BlockchainRouter2", () => { } const result = await uut.getRawMempool(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.isArray(result) // Not sure what other assertions should be made here. @@ -885,7 +886,7 @@ describe("#BlockchainRouter2", () => { describe("getMempoolEntrySingle()", () => { it("should throw 400 if txid is empty", async () => { const result = await uut.getMempoolEntrySingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "txid can not be empty") @@ -898,10 +899,11 @@ describe("#BlockchainRouter2", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" const result = await uut.getMempoolEntrySingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -917,7 +919,8 @@ describe("#BlockchainRouter2", () => { // Mock the timeout error. sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" const result = await uut.getMempoolEntrySingle(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) @@ -933,7 +936,8 @@ describe("#BlockchainRouter2", () => { // Mock the timeout error. sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" const result = await uut.getMempoolEntrySingle(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) @@ -954,10 +958,11 @@ describe("#BlockchainRouter2", () => { }) } - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" const result = await uut.getMempoolEntrySingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.isString(result.error) @@ -979,7 +984,8 @@ describe("#BlockchainRouter2", () => { }) it("should error on non-array single txid", async () => { - req.body.txids = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.body.txids = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" const result = await uut.getMempoolEntryBulk(req, res) @@ -998,7 +1004,7 @@ describe("#BlockchainRouter2", () => { req.body.txids = testArray const result = await uut.getMempoolEntryBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "Array too large") @@ -1008,7 +1014,7 @@ describe("#BlockchainRouter2", () => { sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) req.body.txids = [ - `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" ] const result = await uut.getMempoolEntryBulk(req, res) @@ -1026,7 +1032,7 @@ describe("#BlockchainRouter2", () => { sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) req.body.txids = [ - `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" ] const result = await uut.getMempoolEntryBulk(req, res) @@ -1046,11 +1052,11 @@ describe("#BlockchainRouter2", () => { // integration test. it("should retrieve single mempool entry", async () => { req.body.txids = [ - `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" ] const result = await uut.getMempoolEntryBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.isString(result.error) @@ -1062,12 +1068,12 @@ describe("#BlockchainRouter2", () => { // integration test. it("should retrieve multiple mempool entries", async () => { req.body.txids = [ - `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde`, - `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde", + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" ] const result = await uut.getMempoolEntryBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.isString(result.error) @@ -1078,7 +1084,7 @@ describe("#BlockchainRouter2", () => { describe("getMempoolAncestorsSingle()", () => { it("should throw 400 if txid is empty", async () => { const result = await uut.getMempoolAncestorsSingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "txid can not be empty") @@ -1091,10 +1097,11 @@ describe("#BlockchainRouter2", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" const result = await uut.getMempoolAncestorsSingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -1110,7 +1117,8 @@ describe("#BlockchainRouter2", () => { // Mock the timeout error. sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" const result = await uut.getMempoolAncestorsSingle(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) @@ -1126,7 +1134,8 @@ describe("#BlockchainRouter2", () => { // Mock the timeout error. sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" const result = await uut.getMempoolAncestorsSingle(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) @@ -1143,7 +1152,8 @@ describe("#BlockchainRouter2", () => { .stub(uut.axios, "request") .resolves({ data: { result: mockData.mockAncestors } }) - req.params.txid = `bb0d349892d351da2767f8c45f6f7949713ff09bd12838d53e76158ddee3ce93` + req.params.txid = + "bb0d349892d351da2767f8c45f6f7949713ff09bd12838d53e76158ddee3ce93" const result = await uut.getMempoolAncestorsSingle(req, res) // console.log(`result: ${util.inspect(result)}`) @@ -1154,16 +1164,16 @@ describe("#BlockchainRouter2", () => { describe("getTxOut()", () => { it("should throw 400 if txid is empty", async () => { const result = await uut.getTxOut(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "txid can not be empty") }) it("should throw 400 if n is empty", async () => { - req.params.txid = `sometxid` + req.params.txid = "sometxid" const result = await uut.getTxOut(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "n can not be empty") @@ -1176,11 +1186,12 @@ describe("#BlockchainRouter2", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" req.params.n = 0 const result = await uut.getTxOut(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -1197,7 +1208,8 @@ describe("#BlockchainRouter2", () => { // Mock the timeout error. sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - req.params.txid = `197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d` + req.params.txid = + "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" req.params.n = 0 req.query.include_mempool = "true" @@ -1215,7 +1227,8 @@ describe("#BlockchainRouter2", () => { // Mock the timeout error. sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - req.params.txid = `197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d` + req.params.txid = + "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" req.params.n = 0 req.query.include_mempool = "true" @@ -1238,7 +1251,8 @@ describe("#BlockchainRouter2", () => { .resolves({ data: { result: mockData.mockTxOut } }) } - req.params.txid = `197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d` + req.params.txid = + "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" req.params.n = 0 req.query.include_mempool = "true" @@ -1265,7 +1279,7 @@ describe("#BlockchainRouter2", () => { describe("getTxOutProofSingle()", () => { it("should throw 400 if txid is empty", async () => { const result = await uut.getTxOutProofSingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "txid can not be empty") @@ -1278,10 +1292,11 @@ describe("#BlockchainRouter2", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" const result = await uut.getTxOutProofSingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -1297,7 +1312,8 @@ describe("#BlockchainRouter2", () => { // Mock the timeout error. sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - req.params.txid = `197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d` + req.params.txid = + "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" req.params.n = 0 req.query.include_mempool = "true" @@ -1315,7 +1331,8 @@ describe("#BlockchainRouter2", () => { // Mock the timeout error. sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - req.params.txid = `197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d` + req.params.txid = + "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" req.params.n = 0 req.query.include_mempool = "true" @@ -1338,7 +1355,8 @@ describe("#BlockchainRouter2", () => { .resolves({ data: { result: mockData.mockTxOutProof } }) } - req.params.txid = `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` + req.params.txid = + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" const result = await uut.getTxOutProofSingle(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) @@ -1361,7 +1379,8 @@ describe("#BlockchainRouter2", () => { }) it("should error on non-array single txid", async () => { - req.body.txids = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.body.txids = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" const result = await uut.getTxOutProofBulk(req, res) @@ -1380,7 +1399,7 @@ describe("#BlockchainRouter2", () => { req.body.txids = testArray const result = await uut.getTxOutProofBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "Array too large") @@ -1390,7 +1409,7 @@ describe("#BlockchainRouter2", () => { sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) req.body.txids = [ - `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" ] const result = await uut.getTxOutProofBulk(req, res) @@ -1408,7 +1427,7 @@ describe("#BlockchainRouter2", () => { sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) req.body.txids = [ - `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" ] const result = await uut.getTxOutProofBulk(req, res) @@ -1431,11 +1450,11 @@ describe("#BlockchainRouter2", () => { } req.body.txids = [ - `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" ] const result = await uut.getTxOutProofBulk(req, res) - //console.log(`result: ${JSON.stringify(result, null, 2)}`) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) assert.isString(result[0]) @@ -1450,12 +1469,12 @@ describe("#BlockchainRouter2", () => { } req.body.txids = [ - `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266`, - `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266", + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" ] const result = await uut.getTxOutProofBulk(req, res) - //console.log(`result: ${JSON.stringify(result, null, 2)}`) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) assert.equal(result.length, 2, "Correct length of returned array") @@ -1464,7 +1483,7 @@ describe("#BlockchainRouter2", () => { describe("verifyTxOutProofSingle()", () => { it("should throw 400 if proof is empty", async () => { const result = await uut.verifyTxOutProofSingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "proof can not be empty") @@ -1480,7 +1499,7 @@ describe("#BlockchainRouter2", () => { req.params.proof = mockData.mockTxOutProof const result = await uut.verifyTxOutProofSingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -1541,7 +1560,7 @@ describe("#BlockchainRouter2", () => { "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" const result = await uut.verifyTxOutProofSingle(req, res) - //console.log(`result: ${JSON.stringify(result, null, 2)}`) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) assert.isString(result[0]) @@ -1582,7 +1601,7 @@ describe("#BlockchainRouter2", () => { req.body.proofs = testArray const result = await uut.verifyTxOutProofBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "Array too large") @@ -1639,7 +1658,7 @@ describe("#BlockchainRouter2", () => { ] const result = await uut.verifyTxOutProofBulk(req, res) - //console.log(`result: ${JSON.stringify(result, null, 2)}`) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) assert.isString(result[0]) @@ -1663,7 +1682,7 @@ describe("#BlockchainRouter2", () => { ] const result = await uut.verifyTxOutProofBulk(req, res) - //console.log(`result: ${JSON.stringify(result, null, 2)}`) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) assert.isString(result[0]) From 3b02e779d9e719c16ab007ceaf56c95ade699f26 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 16 Feb 2020 05:20:30 -0800 Subject: [PATCH 29/30] fix(blockchain2): Got integration tests passing --- src/app.js | 5 +- src/routes/v3/full-node/blockchain2.js | 113 +++++++++++++------------ 2 files changed, 61 insertions(+), 57 deletions(-) diff --git a/src/app.js b/src/app.js index 82014dc..37c11b3 100644 --- a/src/app.js +++ b/src/app.js @@ -24,7 +24,7 @@ const jwtAuth = require("./middleware/jwt-auth") // v3 const healthCheckV3 = require("./routes/v3/health-check") -const blockchainV3 = require("./routes/v3/full-node/blockchain") +const BlockchainV3 = require("./routes/v3/full-node/blockchain2") const controlV3 = require("./routes/v3/full-node/control") const miningV3 = require("./routes/v3/full-node/mining") const networkV3 = require("./routes/v3/full-node/network") @@ -37,6 +37,9 @@ const Ninsight = require("./routes/v3/ninsight") require("dotenv").config() +// Instantiate route libraries. +const blockchainV3 = new BlockchainV3() + const app = express() app.locals.env = process.env diff --git a/src/routes/v3/full-node/blockchain2.js b/src/routes/v3/full-node/blockchain2.js index 97a0d8f..8c34ff6 100644 --- a/src/routes/v3/full-node/blockchain2.js +++ b/src/routes/v3/full-node/blockchain2.js @@ -61,7 +61,7 @@ class Blockchain { // DRY error handler. errorHandler(err, res) { // Attempt to decode the error message. - const { msg, status } = this.routeUtils.decodeError(err) + const { msg, status } = _this.routeUtils.decodeError(err) if (msg) { res.status(status) return res.json({ error: msg }) @@ -86,12 +86,12 @@ class Blockchain { async getBestBlockHash(req, res, next) { try { // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() options.data.id = "getbestblockhash" options.data.method = "getbestblockhash" options.data.params = [] - const response = await this.axios.request(options) + const response = await _this.axios.request(options) // console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`) return res.json(response.data.result) @@ -99,7 +99,7 @@ class Blockchain { // Write out error to error log. wlogger.error("Error in blockchain.ts/getBestBlockHash().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -131,19 +131,19 @@ class Blockchain { async getBlockchainInfo(req, res, next) { try { // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() options.data.id = "getblockchaininfo" options.data.method = "getblockchaininfo" options.data.params = [] - const response = await this.axios.request(options) + 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) + return _this.errorHandler(err, res) } } @@ -161,12 +161,12 @@ class Blockchain { async getBlockCount(req, res, next) { try { // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() options.data.id = "getblockcount" options.data.method = "getblockcount" options.data.params = [] - const response = await this.axios.request(options) + const response = await _this.axios.request(options) return res.json(response.data.result) } catch (err) { @@ -174,7 +174,7 @@ class Blockchain { // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/getBlockCount().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -221,12 +221,12 @@ class Blockchain { } // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() options.data.id = "getblockheader" options.data.method = "getblockheader" options.data.params = [hash, verbose] - const response = await this.axios.request(options) + const response = await _this.axios.request(options) return res.json(response.data.result) } catch (err) { @@ -234,7 +234,7 @@ class Blockchain { // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/getBlockHeaderSingle().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -304,7 +304,7 @@ class Blockchain { } // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() // Loop through each hash and creates an array of requests to call in parallel const promises = hashes.map(async hash => { @@ -312,10 +312,10 @@ class Blockchain { options.data.method = "getblockheader" options.data.params = [hash, verbose] - return await this.axios.request(options) + return await _this.axios.request(options) }) - const axiosResult = await this.axios.all(promises) + const axiosResult = await _this.axios.all(promises) // Extract the data component from the axios response. const result = axiosResult.map(x => x.data.result) @@ -327,7 +327,7 @@ class Blockchain { // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/getBlockHeaderBulk().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -345,20 +345,20 @@ class Blockchain { async getChainTips(req, res, next) { try { // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() options.data.id = "getchaintips" options.data.method = "getchaintips" options.data.params = [] - const response = await this.axios.request(options) + const response = await _this.axios.request(options) return res.json(response.data.result) } catch (err) { // Write out error to error log. // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/getChainTips().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -376,13 +376,13 @@ class Blockchain { async getDifficulty(req, res, next) { try { // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() options.data.id = "getdifficulty" options.data.method = "getdifficulty" options.data.params = [] - const response = await this.axios.request(options) + const response = await _this.axios.request(options) return res.json(response.data.result) } catch (err) { @@ -390,7 +390,7 @@ class Blockchain { // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/getDifficulty().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -415,13 +415,13 @@ class Blockchain { } // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() options.data.id = "getmempoolentry" options.data.method = "getmempoolentry" options.data.params = [txid] - const response = await this.axios.request(options) + const response = await _this.axios.request(options) return res.json(response.data.result) } catch (err) { @@ -429,7 +429,7 @@ class Blockchain { // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/getMempoolEntrySingle().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -477,7 +477,7 @@ class Blockchain { } // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() // Loop through each txid and creates an array of requests to call in parallel const promises = txids.map(async txid => { @@ -485,10 +485,10 @@ class Blockchain { options.data.method = "getmempoolentry" options.data.params = [txid] - return await this.axios.request(options) + return await _this.axios.request(options) }) - const axiosResult = await this.axios.all(promises) + const axiosResult = await _this.axios.all(promises) // Extract the data component from the axios response. const result = axiosResult.map(x => x.data.result) @@ -500,7 +500,7 @@ class Blockchain { // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/getMempoolEntryBulk().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -529,13 +529,13 @@ class Blockchain { if (verbose === undefined) verbose = false // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() options.data.id = "getmempoolancestors" options.data.method = "getmempoolancestors" options.data.params = [txid, verbose] - const response = await this.axios.request(options) + const response = await _this.axios.request(options) // console.log(`response: ${util.inspect(response)}`) return res.json(response.data.result) @@ -544,7 +544,7 @@ class Blockchain { // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/getMempoolAncestorsSingle().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -561,20 +561,20 @@ class Blockchain { async getMempoolInfo(req, res, next) { try { // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() options.data.id = "getmempoolinfo" options.data.method = "getmempoolinfo" options.data.params = [] - const response = await this.axios.request(options) + const response = await _this.axios.request(options) return res.json(response.data.result) } catch (err) { // Write out error to error log. // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/getMempoolInfo().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -588,6 +588,7 @@ class Blockchain { * curl -X GET https://mainnet.bchjs.cash/v3/getMempoolInfo -H "accept: application/json" * */ + /** * @api {get} /blockchain/getRawMempool/?verbose= Get raw mempool * @apiName getRawMempool @@ -604,7 +605,7 @@ class Blockchain { async getRawMempool(req, res, next) { try { // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() let verbose = false if (req.query.verbose && req.query.verbose === "true") verbose = true @@ -613,7 +614,7 @@ class Blockchain { options.data.method = "getrawmempool" options.data.params = [verbose] - const response = await this.axios.request(options) + const response = await _this.axios.request(options) return res.json(response.data.result) } catch (err) { @@ -621,7 +622,7 @@ class Blockchain { // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/getRawMempool().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -661,7 +662,7 @@ class Blockchain { include_mempool = true // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() options.data.id = "gettxout" options.data.method = "gettxout" @@ -669,7 +670,7 @@ class Blockchain { // console.log(`requestConfig: ${JSON.stringify(requestConfig, null, 2)}`) - const response = await this.axios.request(options) + const response = await _this.axios.request(options) return res.json(response.data.result) } catch (err) { @@ -677,7 +678,7 @@ class Blockchain { // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/getTxOut().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -703,13 +704,13 @@ class Blockchain { } // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() options.data.id = "gettxoutproof" options.data.method = "gettxoutproof" options.data.params = [[txid]] - const response = await this.axios.request(options) + const response = await _this.axios.request(options) return res.json(response.data.result) } catch (err) { @@ -717,7 +718,7 @@ class Blockchain { // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/getTxOutProofSingle().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -743,7 +744,7 @@ class Blockchain { } // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() // Validate each element in the array. for (let i = 0; i < txids.length; i++) { @@ -768,11 +769,11 @@ class Blockchain { options.data.method = "gettxoutproof" options.data.params = [[txid]] - return await this.axios.request(options) + return await _this.axios.request(options) }) // Wait for all parallel promisses to resolve. - const axiosResult = await this.axios.all(promises) + const axiosResult = await _this.axios.all(promises) // Extract the data component from the axios response. const result = axiosResult.map(x => x.data.result) @@ -784,7 +785,7 @@ class Blockchain { // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/getTxOutProofBulk().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -798,13 +799,13 @@ class Blockchain { } // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() options.data.id = "verifytxoutproof" options.data.method = "verifytxoutproof" options.data.params = [req.params.proof] - const response = await this.axios.request(options) + const response = await _this.axios.request(options) return res.json(response.data.result) } catch (err) { @@ -812,7 +813,7 @@ class Blockchain { // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/verifyTxOutProofSingle().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -837,7 +838,7 @@ class Blockchain { } // Axios options - const options = this.routeUtils.getAxiosOptions() + const options = _this.routeUtils.getAxiosOptions() // Validate each element in the array. for (let i = 0; i < proofs.length; i++) { @@ -860,11 +861,11 @@ class Blockchain { options.data.method = "verifytxoutproof" options.data.params = [proof] - return await this.axios.request(options) + return await _this.axios.request(options) }) // Wait for all parallel promisses to resolve. - const axiosResult = await this.axios.all(promises) + const axiosResult = await _this.axios.all(promises) // Extract the data component from the axios response. const result = axiosResult.map(x => x.data.result[0]) @@ -876,7 +877,7 @@ class Blockchain { // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) wlogger.error("Error in blockchain.ts/verifyTxOutProofBulk().", err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } } From c5338b230828774c3740ae38fa8d1d1e40693adb Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 16 Feb 2020 05:22:59 -0800 Subject: [PATCH 30/30] Removed old blockchain route --- src/app.js | 2 +- src/routes/v3/full-node/blockchain.js | 1921 ++++++++++-------------- src/routes/v3/full-node/blockchain2.js | 885 ----------- test/v3/blockchain.js | 1030 +++++++++---- test/v3/blockchain2.js | 1693 --------------------- 5 files changed, 1585 insertions(+), 3946 deletions(-) delete mode 100644 src/routes/v3/full-node/blockchain2.js delete mode 100644 test/v3/blockchain2.js diff --git a/src/app.js b/src/app.js index 37c11b3..3e7fad5 100644 --- a/src/app.js +++ b/src/app.js @@ -24,7 +24,7 @@ const jwtAuth = require("./middleware/jwt-auth") // v3 const healthCheckV3 = require("./routes/v3/health-check") -const BlockchainV3 = require("./routes/v3/full-node/blockchain2") +const BlockchainV3 = require("./routes/v3/full-node/blockchain") const controlV3 = require("./routes/v3/full-node/control") const miningV3 = require("./routes/v3/full-node/mining") const networkV3 = require("./routes/v3/full-node/network") diff --git a/src/routes/v3/full-node/blockchain.js b/src/routes/v3/full-node/blockchain.js index 201244a..8c34ff6 100644 --- a/src/routes/v3/full-node/blockchain.js +++ b/src/routes/v3/full-node/blockchain.js @@ -1,1152 +1,885 @@ /* - TODO - - Add blockhash functionality back into getTxOutProof + A library for interacting with the Full Node */ "use strict" const express = require("express") const router = express.Router() -const axios = require("axios") -const routeUtils = require("../route-utils") +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 } -// Define routes. -router.get("/", root) -router.get("/getBestBlockHash", getBestBlockHash) -// Dev Note: getBlock/:hash ommited because its the same as block/detailsByHash -//router.get("/getBlock/:hash", getBlock) -router.get("/getBlockchainInfo", getBlockchainInfo) -router.get("/getBlockCount", getBlockCount) -router.get("/getBlockHeader/:hash", getBlockHeaderSingle) -router.post("/getBlockHeader", getBlockHeaderBulk) -router.get("/getChainTips", getChainTips) -router.get("/getDifficulty", getDifficulty) -router.get("/getMempoolEntry/:txid", getMempoolEntrySingle) -router.post("/getMempoolEntry", getMempoolEntryBulk) -router.get("/getMempoolAncestors/:txid", getMempoolAncestorsSingle) -router.get("/getMempoolInfo", getMempoolInfo) -router.get("/getRawMempool", getRawMempool) -router.get("/getTxOut/:txid/:n", getTxOut) -router.get("/getTxOutProof/:txid", getTxOutProofSingle) -router.post("/getTxOutProof", getTxOutProofBulk) -router.get("/verifyTxOutProof/:proof", verifyTxOutProofSingle) -router.post("/verifyTxOutProof", verifyTxOutProofBulk) +const BCHJS = require("@chris.troutner/bch-js") +const bchjs = new BCHJS() -function root(req, res, next) { - return res.json({ status: "blockchain" }) -} +let _this -/** - * @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 function getBestBlockHash(req, res, next) { - try { - // Axios options - const options = routeUtils.getAxiosOptions() - options.data.id = "getbestblockhash" - options.data.method = "getbestblockhash" - options.data.params = [] +class Blockchain { + constructor() { + _this = this - const response = await axios.request(options) + this.bchjs = bchjs + this.axios = axios + this.routeUtils = routeUtils - return res.json(response.data.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 blockchain.ts/getBestBlockHash().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @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 function getBlockchainInfo(req, res, next) { - try { - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - requestConfig.data.id = "getblockchaininfo" - requestConfig.data.method = "getblockchaininfo" - requestConfig.data.params = [] - - const response = await BitboxHTTP(requestConfig) - - return res.json(response.data.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 blockchain.ts/getBlockchainInfo().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @api {get} /blockchain/getBlockCount Get Block Count - * @apiName GetBlockCount - * @apiGroup Blockchain - * @apiDescription Returns the number of blocks in the longest blockchain. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getBlockCount" -H "accept: application/json" - * - * @apiSuccess {Number} bestBlockCount 587665 - */ -async function getBlockCount(req, res, next) { - try { - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - requestConfig.data.id = "getblockcount" - requestConfig.data.method = "getblockcount" - requestConfig.data.params = [] - - const response = await BitboxHTTP(requestConfig) - return res.json(response.data.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 blockchain.ts/getBlockCount().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @api {get} /blockchain/getBlockHeader/:hash Get single block header - * @apiName GetSingleBlockHeader - * @apiGroup Blockchain - * @apiDescription If verbose is false (default), returns a string that is - * serialized, hex-encoded data for blockheader 'hash'. If verbose is true, - * returns an Object with information about blockheader hash. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getBlockHeader/000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201?verbose=true" -H "accept: application/json" - * - * @apiParam {String} hash block hash - * @apiParam {Boolean} verbose Return verbose data - * - * @apiSuccess {Object} object Object containing data - * @apiSuccess {String} object.hash "000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201" - * @apiSuccess {Number} object.confirmations 61839 - * @apiSuccess {Number} object.height 500000 - * @apiSuccess {Number} object.version 536870912 - * @apiSuccess {String} object.versionHex "20000000" - * @apiSuccess {String} object.merkleroot "4af279645e1b337e655ae3286fc2ca09f58eb01efa6ab27adedd1e9e6ec19091" - * @apiSuccess {Number} object.time 1509343584 - * @apiSuccess {Number} object.mediantime 1509336533 - * @apiSuccess {Number} object.nonce 3604508752 - * @apiSuccess {String} object.bits "1809b91a" - * @apiSuccess {Number} object.difficulty 113081236211.4533 - * @apiSuccess {String} object.chainwork "0000000000000000000000000000000000000000007ae48aca46e3b449ad9714" - * @apiSuccess {String} object.previousblockhash "0000000000000000043831d6ebb013716f0580287ee5e5687e27d0ed72e6e523" - * @apiSuccess {String} object.nextblockhash "00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3" - */ -async function getBlockHeaderSingle(req, res, next) { - try { - let verbose = false - if (req.query.verbose && req.query.verbose.toString() === "true") - verbose = true - - const hash = req.params.hash - if (!hash || hash === "") { - res.status(400) - return res.json({ error: "hash can not be empty" }) - } - - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - requestConfig.data.id = "getblockheader" - requestConfig.data.method = "getblockheader" - requestConfig.data.params = [hash, verbose] - - const response = await BitboxHTTP(requestConfig) - - return res.json(response.data.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 blockchain.ts/getBlockHeaderSingle().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @api {post} /blockchain/getBlockHeader Get multiple block headers - * @apiName GetBulkBlockHeader - * @apiGroup Blockchain - * @apiDescription If verbose is false (default), returns a string that is - * serialized, hex-encoded data for blockheader 'hash'. If verbose is true, - * returns an Object with information about blockheader hash. - * - * @apiExample Example usage: - * curl -X POST "https://rest.bitcoin.com/v3/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}" - * - * @apiParam {String} hash block hash - * @apiParam {Boolean} verbose Return verbose data - * - * @apiSuccess {Array} array array containing objects - * @apiSuccess {String} object.hash "000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201" - * @apiSuccess {Number} object.confirmations 61839 - * @apiSuccess {Number} object.height 500000 - * @apiSuccess {Number} object.version 536870912 - * @apiSuccess {String} object.versionHex "20000000" - * @apiSuccess {String} object.merkleroot "4af279645e1b337e655ae3286fc2ca09f58eb01efa6ab27adedd1e9e6ec19091" - * @apiSuccess {Number} object.time 1509343584 - * @apiSuccess {Number} object.mediantime 1509336533 - * @apiSuccess {Number} object.nonce 3604508752 - * @apiSuccess {String} object.bits "1809b91a" - * @apiSuccess {Number} object.difficulty 113081236211.4533 - * @apiSuccess {String} object.chainwork "0000000000000000000000000000000000000000007ae48aca46e3b449ad9714" - * @apiSuccess {String} object.previousblockhash "0000000000000000043831d6ebb013716f0580287ee5e5687e27d0ed72e6e523" - * @apiSuccess {String} object.nextblockhash "00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3" - */ -async function getBlockHeaderBulk(req, res, next) { - try { - const hashes = req.body.hashes - const verbose = req.body.verbose ? req.body.verbose : false - - if (!Array.isArray(hashes)) { - res.status(400) - return res.json({ - error: "hashes needs to be an array. Use GET for single hash." - }) - } - - // 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.` - }) - } - - wlogger.debug( - `Executing blockchain/getBlockHeaderBulk with these hashes: `, - hashes + this.router = router + this.router.get("/", this.root) + this.router.get("/getBestBlockHash", this.getBestBlockHash) + this.router.get("/getBlockchainInfo", this.getBlockchainInfo) + this.router.get("/getBlockCount", this.getBlockCount) + this.router.get("/getBlockHeader/:hash", this.getBlockHeaderSingle) + this.router.post("/getBlockHeader", this.getBlockHeaderBulk) + this.router.get("/getChainTips", this.getChainTips) + this.router.get("/getDifficulty", this.getDifficulty) + this.router.get("/getMempoolEntry/:txid", this.getMempoolEntrySingle) + this.router.post("/getMempoolEntry", this.getMempoolEntryBulk) + this.router.get( + "/getMempoolAncestors/:txid", + this.getMempoolAncestorsSingle ) + this.router.get("/getMempoolInfo", this.getMempoolInfo) + this.router.get("/getRawMempool", this.getRawMempool) + this.router.get("/getTxOut/:txid/:n", this.getTxOut) + this.router.get("/getTxOutProof/:txid", this.getTxOutProofSingle) + this.router.post("/getTxOutProof", this.getTxOutProofBulk) + this.router.get("/verifyTxOutProof/:proof", this.verifyTxOutProofSingle) + this.router.post("/verifyTxOutProof", this.verifyTxOutProofBulk) + } - // Validate each hash in the array. - for (let i = 0; i < hashes.length; i++) { - const hash = hashes[i] + root(req, res, next) { + return res.json({ status: "blockchain" }) + } - if (hash.length !== 64) { + // 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) + } + } + + /** + * @api {get} /blockchain/getBlockCount Get Block Count + * @apiName GetBlockCount + * @apiGroup Blockchain + * @apiDescription Returns the number of blocks in the longest blockchain. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getBlockCount" -H "accept: application/json" + * + * @apiSuccess {Number} bestBlockCount 587665 + */ + async getBlockCount(req, res, next) { + try { + // Axios options + const options = _this.routeUtils.getAxiosOptions() + options.data.id = "getblockcount" + options.data.method = "getblockcount" + options.data.params = [] + + const response = await _this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getBlockCount().", err) + + return _this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getBlockHeader/:hash Get single block header + * @apiName GetSingleBlockHeader + * @apiGroup Blockchain + * @apiDescription If verbose is false (default), returns a string that is + * serialized, hex-encoded data for blockheader 'hash'. If verbose is true, + * returns an Object with information about blockheader hash. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getBlockHeader/000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201?verbose=true" -H "accept: application/json" + * + * @apiParam {String} hash block hash + * @apiParam {Boolean} verbose Return verbose data + * + * @apiSuccess {Object} object Object containing data + * @apiSuccess {String} object.hash "000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201" + * @apiSuccess {Number} object.confirmations 61839 + * @apiSuccess {Number} object.height 500000 + * @apiSuccess {Number} object.version 536870912 + * @apiSuccess {String} object.versionHex "20000000" + * @apiSuccess {String} object.merkleroot "4af279645e1b337e655ae3286fc2ca09f58eb01efa6ab27adedd1e9e6ec19091" + * @apiSuccess {Number} object.time 1509343584 + * @apiSuccess {Number} object.mediantime 1509336533 + * @apiSuccess {Number} object.nonce 3604508752 + * @apiSuccess {String} object.bits "1809b91a" + * @apiSuccess {Number} object.difficulty 113081236211.4533 + * @apiSuccess {String} object.chainwork "0000000000000000000000000000000000000000007ae48aca46e3b449ad9714" + * @apiSuccess {String} object.previousblockhash "0000000000000000043831d6ebb013716f0580287ee5e5687e27d0ed72e6e523" + * @apiSuccess {String} object.nextblockhash "00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3" + */ + async getBlockHeaderSingle(req, res, next) { + try { + let verbose = false + if (req.query.verbose && req.query.verbose.toString() === "true") + verbose = true + + const hash = req.params.hash + if (!hash || hash === "") { res.status(400) - return res.json({ error: `This is not a hash: ${hash}` }) + return res.json({ error: "hash can not be empty" }) } + + // Axios options + const options = _this.routeUtils.getAxiosOptions() + options.data.id = "getblockheader" + options.data.method = "getblockheader" + options.data.params = [hash, verbose] + + const response = await _this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getBlockHeaderSingle().", err) + + return _this.errorHandler(err, res) } - - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - // Loop through each hash and creates an array of requests to call in parallel - const promises = hashes.map(async hash => { - requestConfig.data.id = "getblockheader" - requestConfig.data.method = "getblockheader" - requestConfig.data.params = [hash, verbose] - - return await BitboxHTTP(requestConfig) - }) - - const axiosResult = await axios.all(promises) - - // Extract the data component from the axios response. - const result = axiosResult.map(x => x.data.result) - - 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 blockchain.ts/getBlockHeaderBulk().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) } -} -/** - * @api {get} /blockchain/getChainTips Get Chain Tips - * @apiName getChainTips - * @apiGroup Blockchain - * @apiDescription Return information about all known tips in the block tree, - * including the main chain as well as orphaned branches. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getChainTips" -H "accept: application/json" - * - */ -async function getChainTips(req, res, next) { - try { - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - requestConfig.data.id = "getchaintips" - requestConfig.data.method = "getchaintips" - requestConfig.data.params = [] - - const response = await BitboxHTTP(requestConfig) - return res.json(response.data.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 blockchain.ts/getChainTips().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @api {get} /blockchain/getDifficulty Get difficulty - * @apiName getDifficulty - * @apiGroup Blockchain - * @apiDescription Get the current difficulty value, used to regulate mining - * power on the network. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getDifficulty" -H "accept: application/json" - * - */ -async function getDifficulty(req, res, next) { - try { - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - requestConfig.data.id = "getdifficulty" - requestConfig.data.method = "getdifficulty" - requestConfig.data.params = [] - - const response = await BitboxHTTP(requestConfig) - - return res.json(response.data.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 blockchain.ts/getDifficulty().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @api {get} /blockchain/getMempoolEntry/:txid Get single mempool entry - * @apiName getMempoolEntry - * @apiGroup Blockchain - * @apiDescription Returns mempool data for given transaction. TXID must be in - * mempool (unconfirmed) - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getMempoolEntry/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" - * - */ -async function getMempoolEntrySingle(req, res, next) { - try { - // Validate input parameter - const txid = req.params.txid - if (!txid || txid === "") { - res.status(400) - return res.json({ error: "txid can not be empty" }) - } - - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - requestConfig.data.id = "getmempoolentry" - requestConfig.data.method = "getmempoolentry" - requestConfig.data.params = [txid] - - const response = await BitboxHTTP(requestConfig) - - return res.json(response.data.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 blockchain.ts/getMempoolEntrySingle().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @api {post} /blockchain/getMempoolEntry Get bulk mempool entry - * @apiName getMempoolEntryBulk - * @apiGroup Blockchain - * @apiDescription Returns mempool data for multiple transactions - * - * @apiExample Example usage: - * curl -X POST https://mainnet.bchjs.cash/v3/blockchain/getMempoolEntry -H "Content-Type: application/json" -d "{\"txids\":[\"a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1\",\"5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e\"]}" - */ -async function getMempoolEntryBulk(req, res, next) { - try { - const txids = req.body.txids - - if (!Array.isArray(txids)) { - res.status(400) - return res.json({ - error: "txids needs to be an array. Use GET for single txid." - }) - } - - // 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 blockchain/getMempoolEntry with these txids: `, - txids - ) - - // Validate each element in the array - for (let i = 0; i < txids.length; i++) { - const txid = txids[i] - - if (txid.length !== 64) { - res.status(400) - return res.json({ error: "This is not a txid" }) - } - } - - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - // Loop through each txid and creates an array of requests to call in parallel - const promises = txids.map(async txid => { - requestConfig.data.id = "getmempoolentry" - requestConfig.data.method = "getmempoolentry" - requestConfig.data.params = [txid] - - return await BitboxHTTP(requestConfig) - }) - - const axiosResult = await axios.all(promises) - - // Extract the data component from the axios response. - const result = axiosResult.map(x => x.data.result) - - 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 blockchain.ts/getMempoolEntryBulk().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @api {get} /blockchain/getMempoolAncestors/:txid Get Mempool Ancestors - * @apiName getMempoolAncestors - * @apiGroup Blockchain - * @apiDescription Returns mempool ancestors data for given TXID. It must be in - * mempool (unconfirmed). This call is handy to tell if a UTXO is bumping up - * against the 25 ancestor chain-limit. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getMempoolAncestors/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" - * - */ -async function getMempoolAncestorsSingle(req, res, next) { - try { - // Validate input parameter - const txid = req.params.txid - if (!txid || txid === "") { - res.status(400) - return res.json({ error: "txid can not be empty" }) - } - - let verbose = req.params.verbose - if (verbose === undefined) verbose = false - - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - requestConfig.data.id = "getmempoolancestors" - requestConfig.data.method = "getmempoolancestors" - requestConfig.data.params = [txid, verbose] - - const response = await BitboxHTTP(requestConfig) - // console.log(`response: ${util.inspect(response)}`) - - return res.json(response.data.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 blockchain.ts/getMempoolAncestorsSingle().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @api {get} /blockchain/getMempoolInfo Get mempool info - * @apiName getMempoolInfo - * @apiGroup Blockchain - * @apiDescription Returns details on the active state of the TX memory pool. - * - * @apiExample Example usage: - * curl -X GET https://mainnet.bchjs.cash/v3/getMempoolInfo -H "accept: application/json" - * - */ -async function getMempoolInfo(req, res, next) { - try { - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - requestConfig.data.id = "getmempoolinfo" - requestConfig.data.method = "getmempoolinfo" - requestConfig.data.params = [] - - const response = await BitboxHTTP(requestConfig) - return res.json(response.data.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 blockchain.ts/getMempoolInfo().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @api {get} /blockchain/getRawMempool Get mempool info - * @apiName getMempoolInfo - * @apiGroup Blockchain - * @apiDescription Returns details on the active state of the TX memory pool. - * - * @apiExample Example usage: - * curl -X GET https://mainnet.bchjs.cash/v3/getMempoolInfo -H "accept: application/json" - * - */ -/** - * @api {get} /blockchain/getRawMempool/?verbose= Get raw mempool - * @apiName getRawMempool - * @apiGroup Blockchain - * @apiDescription Returns all transaction ids in memory pool as a json array - * of string transaction ids. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/getRawMempool/?verbose=true" -H "accept: application/json" - * - * @apiParam {Boolean} verbose Return verbose data - * - */ -async function getRawMempool(req, res, next) { - try { - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - let verbose = false - if (req.query.verbose && req.query.verbose === "true") verbose = true - - requestConfig.data.id = "getrawmempool" - requestConfig.data.method = "getrawmempool" - requestConfig.data.params = [verbose] - - const response = await BitboxHTTP(requestConfig) - - return res.json(response.data.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 blockchain.ts/getRawMempool().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @api {get} /blockchain/getTxOut/:txid/:n?mempool= Get Tx Out - * @apiName getTxOut - * @apiGroup Blockchain - * @apiDescription Returns details about an unspent transaction output. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getTxOut/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33/0?mempool=false" -H "accept: application/json" - * - * @apiParam {String} txid Transaction id (required) - * @apiParam {Number} n Output number (required) - * @apiParam {Boolean} mempool Check mempool or not (optional) - * - */ -// Returns details about an unspent transaction output. -async function getTxOut(req, res, next) { - try { - // Validate input parameter - const txid = req.params.txid - if (!txid || txid === "") { - res.status(400) - return res.json({ error: "txid can not be empty" }) - } - - let n = req.params.n - if (n === undefined || n === "") { - res.status(400) - return res.json({ error: "n can not be empty" }) - } - n = parseInt(n) - - let include_mempool = false - if (req.query.include_mempool && req.query.include_mempool === "true") - include_mempool = true - - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - requestConfig.data.id = "gettxout" - requestConfig.data.method = "gettxout" - requestConfig.data.params = [txid, n, include_mempool] - - // console.log(`requestConfig: ${JSON.stringify(requestConfig, null, 2)}`) - - const response = await BitboxHTTP(requestConfig) - - return res.json(response.data.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 blockchain.ts/getTxOut().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/** - * @api {get} /blockchain/getTxOutProofSingle/:txid Get Tx Out Proof - * @apiName getTxOutProofSingle - * @apiGroup Blockchain - * @apiDescription Returns a hex-encoded proof that 'txid' was included in a block. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getTxOutProofSingle/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" - * - * @apiParam {String} txid Transaction id (required) - * - */ -async function getTxOutProofSingle(req, res, next) { - try { - // Validate input parameter - const txid = req.params.txid - if (!txid || txid === "") { - res.status(400) - return res.json({ error: "txid can not be empty" }) - } - - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - requestConfig.data.id = "gettxoutproof" - requestConfig.data.method = "gettxoutproof" - requestConfig.data.params = [[txid]] - - const response = await BitboxHTTP(requestConfig) - - return res.json(response.data.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 blockchain.ts/getTxOutProofSingle().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -// Returns a hex-encoded proof that 'txid' was included in a block. -async function getTxOutProofBulk(req, res, next) { - try { - const txids = req.body.txids - - // Reject if txids is not an array. - if (!Array.isArray(txids)) { - res.status(400) - return res.json({ - error: "txids needs to be an array. Use GET for single txid." - }) - } - - // 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.` - }) - } - - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - // Validate each element in the array. - for (let i = 0; i < txids.length; i++) { - const txid = txids[i] - - if (txid.length !== 64) { + /** + * @api {post} /blockchain/getBlockHeader Get multiple block headers + * @apiName GetBulkBlockHeader + * @apiGroup Blockchain + * @apiDescription If verbose is false (default), returns a string that is + * serialized, hex-encoded data for blockheader 'hash'. If verbose is true, + * returns an Object with information about blockheader hash. + * + * @apiExample Example usage: + * curl -X POST "https://rest.bitcoin.com/v3/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}" + * + * @apiParam {String} hash block hash + * @apiParam {Boolean} verbose Return verbose data + * + * @apiSuccess {Array} array array containing objects + * @apiSuccess {String} object.hash "000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201" + * @apiSuccess {Number} object.confirmations 61839 + * @apiSuccess {Number} object.height 500000 + * @apiSuccess {Number} object.version 536870912 + * @apiSuccess {String} object.versionHex "20000000" + * @apiSuccess {String} object.merkleroot "4af279645e1b337e655ae3286fc2ca09f58eb01efa6ab27adedd1e9e6ec19091" + * @apiSuccess {Number} object.time 1509343584 + * @apiSuccess {Number} object.mediantime 1509336533 + * @apiSuccess {Number} object.nonce 3604508752 + * @apiSuccess {String} object.bits "1809b91a" + * @apiSuccess {Number} object.difficulty 113081236211.4533 + * @apiSuccess {String} object.chainwork "0000000000000000000000000000000000000000007ae48aca46e3b449ad9714" + * @apiSuccess {String} object.previousblockhash "0000000000000000043831d6ebb013716f0580287ee5e5687e27d0ed72e6e523" + * @apiSuccess {String} object.nextblockhash "00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3" + */ + async getBlockHeaderBulk(req, res, next) { + try { + const hashes = req.body.hashes + const verbose = req.body.verbose ? req.body.verbose : false + + if (!Array.isArray(hashes)) { res.status(400) return res.json({ - error: `Invalid txid. Double check your txid is valid: ${txid}` + error: "hashes needs to be an array. Use GET for single hash." }) } - } - wlogger.debug( - `Executing blockchain/getTxOutProof with these txids: `, - txids - ) + // 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." + }) + } - // Loop through each txid and creates an array of requests to call in parallel - const promises = txids.map(async txid => { - requestConfig.data.id = "gettxoutproof" - requestConfig.data.method = "gettxoutproof" - requestConfig.data.params = [[txid]] + wlogger.debug( + "Executing blockchain/getBlockHeaderBulk with these hashes: ", + hashes + ) - return await BitboxHTTP(requestConfig) - }) + // Validate each hash in the array. + for (let i = 0; i < hashes.length; i++) { + const hash = hashes[i] - // Wait for all parallel promisses to resolve. - const axiosResult = await axios.all(promises) + if (hash.length !== 64) { + res.status(400) + return res.json({ error: `This is not a hash: ${hash}` }) + } + } - // Extract the data component from the axios response. - const result = axiosResult.map(x => x.data.result) + // Axios options + const options = _this.routeUtils.getAxiosOptions() - 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 }) - } + // Loop through each hash and creates an array of requests to call in parallel + const promises = hashes.map(async hash => { + options.data.id = "getblockheader" + options.data.method = "getblockheader" + options.data.params = [hash, verbose] - // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/getTxOutProofBulk().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -/* -// -// router.get('/preciousBlock/:hash', async (req, res, next) => { -// BitboxHTTP({ -// method: 'post', -// auth: { -// username: username, -// password: password -// }, -// data: { -// jsonrpc: "1.0", -// id:"preciousblock", -// method: "preciousblock", -// params: [ -// req.params.hash -// ] -// } -// }) -// .then((response) => { -// res.json(JSON.stringify(response.data.result)); -// }) -// .catch((error) => { -// res.send(error.response.data.error.message); -// }); -// }); -// -// router.post('/pruneBlockchain/:height', async (req, res, next) => { -// BitboxHTTP({ -// method: 'post', -// auth: { -// username: username, -// password: password -// }, -// data: { -// jsonrpc: "1.0", -// id:"pruneblockchain", -// method: "pruneblockchain", -// params: [ -// req.params.height -// ] -// } -// }) -// .then((response) => { -// res.json(response.data.result); -// }) -// .catch((error) => { -// res.send(error.response.data.error.message); -// }); -// }); -// -// router.get('/verifyChain', async (req, res, next) => { -// BitboxHTTP({ -// method: 'post', -// auth: { -// username: username, -// password: password -// }, -// data: { -// jsonrpc: "1.0", -// id:"verifychain", -// method: "verifychain" -// } -// }) -// .then((response) => { -// res.json(response.data.result); -// }) -// .catch((error) => { -// res.send(error.response.data.error.message); -// }); -// }); -*/ - -async function verifyTxOutProofSingle(req, res, next) { - try { - // Validate input parameter - const proof = req.params.proof - if (!proof || proof === "") { - res.status(400) - return res.json({ error: "proof can not be empty" }) - } - - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() - - requestConfig.data.id = "verifytxoutproof" - requestConfig.data.method = "verifytxoutproof" - requestConfig.data.params = [req.params.proof] - - const response = await BitboxHTTP(requestConfig) - - return res.json(response.data.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 blockchain.ts/verifyTxOutProofSingle().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) - } -} - -async function verifyTxOutProofBulk(req, res, next) { - try { - const proofs = req.body.proofs - - // Reject if proofs is not an array. - if (!Array.isArray(proofs)) { - res.status(400) - return res.json({ - error: "proofs needs to be an array. Use GET for single proof." + return await _this.axios.request(options) }) - } - // Enforce array size rate limits - if (!routeUtils.validateArraySize(req, proofs)) { - res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 - return res.json({ - error: `Array too large.` + const axiosResult = await _this.axios.all(promises) + + // Extract the data component from the axios response. + const result = axiosResult.map(x => x.data.result) + + res.status(200) + return res.json(result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getBlockHeaderBulk().", err) + + return _this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getChainTips Get Chain Tips + * @apiName getChainTips + * @apiGroup Blockchain + * @apiDescription Return information about all known tips in the block tree, + * including the main chain as well as orphaned branches. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getChainTips" -H "accept: application/json" + * + */ + async getChainTips(req, res, next) { + try { + // Axios options + const options = _this.routeUtils.getAxiosOptions() + + options.data.id = "getchaintips" + options.data.method = "getchaintips" + options.data.params = [] + + const response = await _this.axios.request(options) + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getChainTips().", err) + + return _this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getDifficulty Get difficulty + * @apiName getDifficulty + * @apiGroup Blockchain + * @apiDescription Get the current difficulty value, used to regulate mining + * power on the network. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getDifficulty" -H "accept: application/json" + * + */ + async getDifficulty(req, res, next) { + try { + // Axios options + const options = _this.routeUtils.getAxiosOptions() + + options.data.id = "getdifficulty" + options.data.method = "getdifficulty" + options.data.params = [] + + const response = await _this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getDifficulty().", err) + + return _this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getMempoolEntry/:txid Get single mempool entry + * @apiName getMempoolEntry + * @apiGroup Blockchain + * @apiDescription Returns mempool data for given transaction. TXID must be in + * mempool (unconfirmed) + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getMempoolEntry/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" + * + */ + async getMempoolEntrySingle(req, res, next) { + try { + // Validate input parameter + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + // Axios options + const options = _this.routeUtils.getAxiosOptions() + + options.data.id = "getmempoolentry" + options.data.method = "getmempoolentry" + options.data.params = [txid] + + const response = await _this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getMempoolEntrySingle().", err) + + return _this.errorHandler(err, res) + } + } + + /** + * @api {post} /blockchain/getMempoolEntry Get bulk mempool entry + * @apiName getMempoolEntryBulk + * @apiGroup Blockchain + * @apiDescription Returns mempool data for multiple transactions + * + * @apiExample Example usage: + * curl -X POST https://mainnet.bchjs.cash/v3/blockchain/getMempoolEntry -H "Content-Type: application/json" -d "{\"txids\":[\"a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1\",\"5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e\"]}" + */ + async getMempoolEntryBulk(req, res, next) { + try { + const txids = req.body.txids + + if (!Array.isArray(txids)) { + res.status(400) + return res.json({ + error: "txids needs to be an array. Use GET for single txid." + }) + } + + // 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 blockchain/getMempoolEntry with these txids: ", + txids + ) + + // Validate each element in the array + for (let i = 0; i < txids.length; i++) { + const txid = txids[i] + + if (txid.length !== 64) { + res.status(400) + return res.json({ error: "This is not a txid" }) + } + } + + // Axios options + const options = _this.routeUtils.getAxiosOptions() + + // Loop through each txid and creates an array of requests to call in parallel + const promises = txids.map(async txid => { + options.data.id = "getmempoolentry" + options.data.method = "getmempoolentry" + options.data.params = [txid] + + return await _this.axios.request(options) }) + + const axiosResult = await _this.axios.all(promises) + + // Extract the data component from the axios response. + const result = axiosResult.map(x => x.data.result) + + res.status(200) + return res.json(result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getMempoolEntryBulk().", err) + + return _this.errorHandler(err, res) } + } - const { - BitboxHTTP, - username, - password, - requestConfig - } = routeUtils.setEnvVars() + /** + * @api {get} /blockchain/getMempoolAncestors/:txid Get Mempool Ancestors + * @apiName getMempoolAncestors + * @apiGroup Blockchain + * @apiDescription Returns mempool ancestors data for given TXID. It must be in + * mempool (unconfirmed). This call is handy to tell if a UTXO is bumping up + * against the 25 ancestor chain-limit. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getMempoolAncestors/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" + * + */ + async getMempoolAncestorsSingle(req, res, next) { + try { + // Validate input parameter + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } - // Validate each element in the array. - for (let i = 0; i < proofs.length; i++) { - const proof = proofs[i] + let verbose = req.params.verbose + if (verbose === undefined) verbose = false + // Axios options + const options = _this.routeUtils.getAxiosOptions() + + options.data.id = "getmempoolancestors" + options.data.method = "getmempoolancestors" + options.data.params = [txid, verbose] + + const response = await _this.axios.request(options) + // console.log(`response: ${util.inspect(response)}`) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getMempoolAncestorsSingle().", err) + + return _this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getMempoolInfo Get mempool info + * @apiName getMempoolInfo + * @apiGroup Blockchain + * @apiDescription Returns details on the active state of the TX memory pool. + * + * @apiExample Example usage: + * curl -X GET https://mainnet.bchjs.cash/v3/getMempoolInfo -H "accept: application/json" + * + */ + async getMempoolInfo(req, res, next) { + try { + // Axios options + const options = _this.routeUtils.getAxiosOptions() + + options.data.id = "getmempoolinfo" + options.data.method = "getmempoolinfo" + options.data.params = [] + + const response = await _this.axios.request(options) + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getMempoolInfo().", err) + + return _this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getRawMempool Get mempool info + * @apiName getMempoolInfo + * @apiGroup Blockchain + * @apiDescription Returns details on the active state of the TX memory pool. + * + * @apiExample Example usage: + * curl -X GET https://mainnet.bchjs.cash/v3/getMempoolInfo -H "accept: application/json" + * + */ + + /** + * @api {get} /blockchain/getRawMempool/?verbose= Get raw mempool + * @apiName getRawMempool + * @apiGroup Blockchain + * @apiDescription Returns all transaction ids in memory pool as a json array + * of string transaction ids. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/getRawMempool/?verbose=true" -H "accept: application/json" + * + * @apiParam {Boolean} verbose Return verbose data + * + */ + async getRawMempool(req, res, next) { + try { + // Axios options + const options = _this.routeUtils.getAxiosOptions() + + let verbose = false + if (req.query.verbose && req.query.verbose === "true") verbose = true + + options.data.id = "getrawmempool" + options.data.method = "getrawmempool" + options.data.params = [verbose] + + const response = await _this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getRawMempool().", err) + + return _this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getTxOut/:txid/:n?mempool= Get Tx Out + * @apiName getTxOut + * @apiGroup Blockchain + * @apiDescription Returns details about an unspent transaction output. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getTxOut/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33/0?mempool=false" -H "accept: application/json" + * + * @apiParam {String} txid Transaction id (required) + * @apiParam {Number} n Output number (required) + * @apiParam {Boolean} mempool Check mempool or not (optional) + * + */ + // Returns details about an unspent transaction output. + async getTxOut(req, res, next) { + try { + // Validate input parameter + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + let n = req.params.n + if (n === undefined || n === "") { + res.status(400) + return res.json({ error: "n can not be empty" }) + } + n = parseInt(n) + + let include_mempool = false + if (req.query.include_mempool && req.query.include_mempool === "true") + include_mempool = true + + // Axios options + const options = _this.routeUtils.getAxiosOptions() + + options.data.id = "gettxout" + options.data.method = "gettxout" + options.data.params = [txid, n, include_mempool] + + // console.log(`requestConfig: ${JSON.stringify(requestConfig, null, 2)}`) + + const response = await _this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getTxOut().", err) + + return _this.errorHandler(err, res) + } + } + + /** + * @api {get} /blockchain/getTxOutProofSingle/:txid Get Tx Out Proof + * @apiName getTxOutProofSingle + * @apiGroup Blockchain + * @apiDescription Returns a hex-encoded proof that 'txid' was included in a block. + * + * @apiExample Example usage: + * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getTxOutProofSingle/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" + * + * @apiParam {String} txid Transaction id (required) + * + */ + async getTxOutProofSingle(req, res, next) { + try { + // Validate input parameter + const txid = req.params.txid + if (!txid || txid === "") { + res.status(400) + return res.json({ error: "txid can not be empty" }) + } + + // Axios options + const options = _this.routeUtils.getAxiosOptions() + + options.data.id = "gettxoutproof" + options.data.method = "gettxoutproof" + options.data.params = [[txid]] + + const response = await _this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getTxOutProofSingle().", err) + + return _this.errorHandler(err, res) + } + } + + // Returns a hex-encoded proof that 'txid' was included in a block. + async getTxOutProofBulk(req, res, next) { + try { + const txids = req.body.txids + + // Reject if txids is not an array. + if (!Array.isArray(txids)) { + res.status(400) + return res.json({ + error: "txids needs to be an array. Use GET for single txid." + }) + } + + // 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." + }) + } + + // Axios options + const options = _this.routeUtils.getAxiosOptions() + + // Validate each element in the array. + for (let i = 0; i < txids.length; i++) { + const txid = txids[i] + + if (txid.length !== 64) { + res.status(400) + return res.json({ + error: `Invalid txid. Double check your txid is valid: ${txid}` + }) + } + } + + wlogger.debug( + "Executing blockchain/getTxOutProof with these txids: ", + txids + ) + + // Loop through each txid and creates an array of requests to call in parallel + const promises = txids.map(async txid => { + options.data.id = "gettxoutproof" + options.data.method = "gettxoutproof" + options.data.params = [[txid]] + + return await _this.axios.request(options) + }) + + // Wait for all parallel promisses to resolve. + const axiosResult = await _this.axios.all(promises) + + // Extract the data component from the axios response. + const result = axiosResult.map(x => x.data.result) + + res.status(200) + return res.json(result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/getTxOutProofBulk().", err) + + return _this.errorHandler(err, res) + } + } + + async verifyTxOutProofSingle(req, res, next) { + try { + // Validate input parameter + const proof = req.params.proof if (!proof || proof === "") { res.status(400) - return res.json({ error: `proof can not be empty: ${proof}` }) + return res.json({ error: "proof can not be empty" }) } + + // Axios options + const options = _this.routeUtils.getAxiosOptions() + + options.data.id = "verifytxoutproof" + options.data.method = "verifytxoutproof" + options.data.params = [req.params.proof] + + const response = await _this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/verifyTxOutProofSingle().", err) + + return _this.errorHandler(err, res) } + } - wlogger.debug( - `Executing blockchain/verifyTxOutProof with these proofs: `, - proofs - ) + async verifyTxOutProofBulk(req, res, next) { + try { + const proofs = req.body.proofs - // Loop through each proof and creates an array of requests to call in parallel - const promises = proofs.map(async proof => { - requestConfig.data.id = "verifytxoutproof" - requestConfig.data.method = "verifytxoutproof" - requestConfig.data.params = [proof] + // Reject if proofs is not an array. + if (!Array.isArray(proofs)) { + res.status(400) + return res.json({ + error: "proofs needs to be an array. Use GET for single proof." + }) + } - return await BitboxHTTP(requestConfig) - }) + // Enforce array size rate limits + if (!routeUtils.validateArraySize(req, proofs)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: "Array too large." + }) + } - // Wait for all parallel promisses to resolve. - const axiosResult = await axios.all(promises) + // Axios options + const options = _this.routeUtils.getAxiosOptions() - // Extract the data component from the axios response. - const result = axiosResult.map(x => x.data.result[0]) + // Validate each element in the array. + for (let i = 0; i < proofs.length; i++) { + const proof = proofs[i] - 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 }) + if (!proof || proof === "") { + res.status(400) + return res.json({ error: `proof can not be empty: ${proof}` }) + } + } + + wlogger.debug( + "Executing blockchain/verifyTxOutProof with these proofs: ", + proofs + ) + + // Loop through each proof and creates an array of requests to call in parallel + const promises = proofs.map(async proof => { + options.data.id = "verifytxoutproof" + options.data.method = "verifytxoutproof" + options.data.params = [proof] + + return await _this.axios.request(options) + }) + + // Wait for all parallel promisses to resolve. + const axiosResult = await _this.axios.all(promises) + + // Extract the data component from the axios response. + const result = axiosResult.map(x => x.data.result[0]) + + res.status(200) + return res.json(result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error("Error in blockchain.ts/verifyTxOutProofBulk().", err) + + return _this.errorHandler(err, res) } - - // Write out error to error log. - //logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error(`Error in blockchain.ts/verifyTxOutProofBulk().`, err) - - res.status(500) - return res.json({ error: util.inspect(err) }) } } -module.exports = { - router, - testableComponents: { - root, - getBestBlockHash, - //getBlock, - getBlockchainInfo, - getBlockCount, - getBlockHeaderSingle, - getBlockHeaderBulk, - getChainTips, - getDifficulty, - getMempoolInfo, - getRawMempool, - getMempoolEntrySingle, - getMempoolEntryBulk, - getMempoolAncestorsSingle, - getTxOut, - getTxOutProofSingle, - getTxOutProofBulk, - verifyTxOutProofSingle, - verifyTxOutProofBulk - } -} +module.exports = Blockchain diff --git a/src/routes/v3/full-node/blockchain2.js b/src/routes/v3/full-node/blockchain2.js deleted file mode 100644 index 8c34ff6..0000000 --- a/src/routes/v3/full-node/blockchain2.js +++ /dev/null @@ -1,885 +0,0 @@ -/* - 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) - this.router.get("/getBlockCount", this.getBlockCount) - this.router.get("/getBlockHeader/:hash", this.getBlockHeaderSingle) - this.router.post("/getBlockHeader", this.getBlockHeaderBulk) - this.router.get("/getChainTips", this.getChainTips) - this.router.get("/getDifficulty", this.getDifficulty) - this.router.get("/getMempoolEntry/:txid", this.getMempoolEntrySingle) - this.router.post("/getMempoolEntry", this.getMempoolEntryBulk) - this.router.get( - "/getMempoolAncestors/:txid", - this.getMempoolAncestorsSingle - ) - this.router.get("/getMempoolInfo", this.getMempoolInfo) - this.router.get("/getRawMempool", this.getRawMempool) - this.router.get("/getTxOut/:txid/:n", this.getTxOut) - this.router.get("/getTxOutProof/:txid", this.getTxOutProofSingle) - this.router.post("/getTxOutProof", this.getTxOutProofBulk) - this.router.get("/verifyTxOutProof/:proof", this.verifyTxOutProofSingle) - this.router.post("/verifyTxOutProof", this.verifyTxOutProofBulk) - } - - 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) - } - } - - /** - * @api {get} /blockchain/getBlockCount Get Block Count - * @apiName GetBlockCount - * @apiGroup Blockchain - * @apiDescription Returns the number of blocks in the longest blockchain. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getBlockCount" -H "accept: application/json" - * - * @apiSuccess {Number} bestBlockCount 587665 - */ - async getBlockCount(req, res, next) { - try { - // Axios options - const options = _this.routeUtils.getAxiosOptions() - options.data.id = "getblockcount" - options.data.method = "getblockcount" - options.data.params = [] - - const response = await _this.axios.request(options) - - return res.json(response.data.result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/getBlockCount().", err) - - return _this.errorHandler(err, res) - } - } - - /** - * @api {get} /blockchain/getBlockHeader/:hash Get single block header - * @apiName GetSingleBlockHeader - * @apiGroup Blockchain - * @apiDescription If verbose is false (default), returns a string that is - * serialized, hex-encoded data for blockheader 'hash'. If verbose is true, - * returns an Object with information about blockheader hash. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getBlockHeader/000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201?verbose=true" -H "accept: application/json" - * - * @apiParam {String} hash block hash - * @apiParam {Boolean} verbose Return verbose data - * - * @apiSuccess {Object} object Object containing data - * @apiSuccess {String} object.hash "000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201" - * @apiSuccess {Number} object.confirmations 61839 - * @apiSuccess {Number} object.height 500000 - * @apiSuccess {Number} object.version 536870912 - * @apiSuccess {String} object.versionHex "20000000" - * @apiSuccess {String} object.merkleroot "4af279645e1b337e655ae3286fc2ca09f58eb01efa6ab27adedd1e9e6ec19091" - * @apiSuccess {Number} object.time 1509343584 - * @apiSuccess {Number} object.mediantime 1509336533 - * @apiSuccess {Number} object.nonce 3604508752 - * @apiSuccess {String} object.bits "1809b91a" - * @apiSuccess {Number} object.difficulty 113081236211.4533 - * @apiSuccess {String} object.chainwork "0000000000000000000000000000000000000000007ae48aca46e3b449ad9714" - * @apiSuccess {String} object.previousblockhash "0000000000000000043831d6ebb013716f0580287ee5e5687e27d0ed72e6e523" - * @apiSuccess {String} object.nextblockhash "00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3" - */ - async getBlockHeaderSingle(req, res, next) { - try { - let verbose = false - if (req.query.verbose && req.query.verbose.toString() === "true") - verbose = true - - const hash = req.params.hash - if (!hash || hash === "") { - res.status(400) - return res.json({ error: "hash can not be empty" }) - } - - // Axios options - const options = _this.routeUtils.getAxiosOptions() - options.data.id = "getblockheader" - options.data.method = "getblockheader" - options.data.params = [hash, verbose] - - const response = await _this.axios.request(options) - - return res.json(response.data.result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/getBlockHeaderSingle().", err) - - return _this.errorHandler(err, res) - } - } - - /** - * @api {post} /blockchain/getBlockHeader Get multiple block headers - * @apiName GetBulkBlockHeader - * @apiGroup Blockchain - * @apiDescription If verbose is false (default), returns a string that is - * serialized, hex-encoded data for blockheader 'hash'. If verbose is true, - * returns an Object with information about blockheader hash. - * - * @apiExample Example usage: - * curl -X POST "https://rest.bitcoin.com/v3/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}" - * - * @apiParam {String} hash block hash - * @apiParam {Boolean} verbose Return verbose data - * - * @apiSuccess {Array} array array containing objects - * @apiSuccess {String} object.hash "000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201" - * @apiSuccess {Number} object.confirmations 61839 - * @apiSuccess {Number} object.height 500000 - * @apiSuccess {Number} object.version 536870912 - * @apiSuccess {String} object.versionHex "20000000" - * @apiSuccess {String} object.merkleroot "4af279645e1b337e655ae3286fc2ca09f58eb01efa6ab27adedd1e9e6ec19091" - * @apiSuccess {Number} object.time 1509343584 - * @apiSuccess {Number} object.mediantime 1509336533 - * @apiSuccess {Number} object.nonce 3604508752 - * @apiSuccess {String} object.bits "1809b91a" - * @apiSuccess {Number} object.difficulty 113081236211.4533 - * @apiSuccess {String} object.chainwork "0000000000000000000000000000000000000000007ae48aca46e3b449ad9714" - * @apiSuccess {String} object.previousblockhash "0000000000000000043831d6ebb013716f0580287ee5e5687e27d0ed72e6e523" - * @apiSuccess {String} object.nextblockhash "00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3" - */ - async getBlockHeaderBulk(req, res, next) { - try { - const hashes = req.body.hashes - const verbose = req.body.verbose ? req.body.verbose : false - - if (!Array.isArray(hashes)) { - res.status(400) - return res.json({ - error: "hashes needs to be an array. Use GET for single hash." - }) - } - - // 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." - }) - } - - wlogger.debug( - "Executing blockchain/getBlockHeaderBulk with these hashes: ", - hashes - ) - - // Validate each hash in the array. - for (let i = 0; i < hashes.length; i++) { - const hash = hashes[i] - - if (hash.length !== 64) { - res.status(400) - return res.json({ error: `This is not a hash: ${hash}` }) - } - } - - // Axios options - const options = _this.routeUtils.getAxiosOptions() - - // Loop through each hash and creates an array of requests to call in parallel - const promises = hashes.map(async hash => { - options.data.id = "getblockheader" - options.data.method = "getblockheader" - options.data.params = [hash, verbose] - - return await _this.axios.request(options) - }) - - const axiosResult = await _this.axios.all(promises) - - // Extract the data component from the axios response. - const result = axiosResult.map(x => x.data.result) - - res.status(200) - return res.json(result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/getBlockHeaderBulk().", err) - - return _this.errorHandler(err, res) - } - } - - /** - * @api {get} /blockchain/getChainTips Get Chain Tips - * @apiName getChainTips - * @apiGroup Blockchain - * @apiDescription Return information about all known tips in the block tree, - * including the main chain as well as orphaned branches. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getChainTips" -H "accept: application/json" - * - */ - async getChainTips(req, res, next) { - try { - // Axios options - const options = _this.routeUtils.getAxiosOptions() - - options.data.id = "getchaintips" - options.data.method = "getchaintips" - options.data.params = [] - - const response = await _this.axios.request(options) - return res.json(response.data.result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/getChainTips().", err) - - return _this.errorHandler(err, res) - } - } - - /** - * @api {get} /blockchain/getDifficulty Get difficulty - * @apiName getDifficulty - * @apiGroup Blockchain - * @apiDescription Get the current difficulty value, used to regulate mining - * power on the network. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getDifficulty" -H "accept: application/json" - * - */ - async getDifficulty(req, res, next) { - try { - // Axios options - const options = _this.routeUtils.getAxiosOptions() - - options.data.id = "getdifficulty" - options.data.method = "getdifficulty" - options.data.params = [] - - const response = await _this.axios.request(options) - - return res.json(response.data.result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/getDifficulty().", err) - - return _this.errorHandler(err, res) - } - } - - /** - * @api {get} /blockchain/getMempoolEntry/:txid Get single mempool entry - * @apiName getMempoolEntry - * @apiGroup Blockchain - * @apiDescription Returns mempool data for given transaction. TXID must be in - * mempool (unconfirmed) - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getMempoolEntry/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" - * - */ - async getMempoolEntrySingle(req, res, next) { - try { - // Validate input parameter - const txid = req.params.txid - if (!txid || txid === "") { - res.status(400) - return res.json({ error: "txid can not be empty" }) - } - - // Axios options - const options = _this.routeUtils.getAxiosOptions() - - options.data.id = "getmempoolentry" - options.data.method = "getmempoolentry" - options.data.params = [txid] - - const response = await _this.axios.request(options) - - return res.json(response.data.result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/getMempoolEntrySingle().", err) - - return _this.errorHandler(err, res) - } - } - - /** - * @api {post} /blockchain/getMempoolEntry Get bulk mempool entry - * @apiName getMempoolEntryBulk - * @apiGroup Blockchain - * @apiDescription Returns mempool data for multiple transactions - * - * @apiExample Example usage: - * curl -X POST https://mainnet.bchjs.cash/v3/blockchain/getMempoolEntry -H "Content-Type: application/json" -d "{\"txids\":[\"a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1\",\"5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e\"]}" - */ - async getMempoolEntryBulk(req, res, next) { - try { - const txids = req.body.txids - - if (!Array.isArray(txids)) { - res.status(400) - return res.json({ - error: "txids needs to be an array. Use GET for single txid." - }) - } - - // 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 blockchain/getMempoolEntry with these txids: ", - txids - ) - - // Validate each element in the array - for (let i = 0; i < txids.length; i++) { - const txid = txids[i] - - if (txid.length !== 64) { - res.status(400) - return res.json({ error: "This is not a txid" }) - } - } - - // Axios options - const options = _this.routeUtils.getAxiosOptions() - - // Loop through each txid and creates an array of requests to call in parallel - const promises = txids.map(async txid => { - options.data.id = "getmempoolentry" - options.data.method = "getmempoolentry" - options.data.params = [txid] - - return await _this.axios.request(options) - }) - - const axiosResult = await _this.axios.all(promises) - - // Extract the data component from the axios response. - const result = axiosResult.map(x => x.data.result) - - res.status(200) - return res.json(result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/getMempoolEntryBulk().", err) - - return _this.errorHandler(err, res) - } - } - - /** - * @api {get} /blockchain/getMempoolAncestors/:txid Get Mempool Ancestors - * @apiName getMempoolAncestors - * @apiGroup Blockchain - * @apiDescription Returns mempool ancestors data for given TXID. It must be in - * mempool (unconfirmed). This call is handy to tell if a UTXO is bumping up - * against the 25 ancestor chain-limit. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getMempoolAncestors/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" - * - */ - async getMempoolAncestorsSingle(req, res, next) { - try { - // Validate input parameter - const txid = req.params.txid - if (!txid || txid === "") { - res.status(400) - return res.json({ error: "txid can not be empty" }) - } - - let verbose = req.params.verbose - if (verbose === undefined) verbose = false - - // Axios options - const options = _this.routeUtils.getAxiosOptions() - - options.data.id = "getmempoolancestors" - options.data.method = "getmempoolancestors" - options.data.params = [txid, verbose] - - const response = await _this.axios.request(options) - // console.log(`response: ${util.inspect(response)}`) - - return res.json(response.data.result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/getMempoolAncestorsSingle().", err) - - return _this.errorHandler(err, res) - } - } - - /** - * @api {get} /blockchain/getMempoolInfo Get mempool info - * @apiName getMempoolInfo - * @apiGroup Blockchain - * @apiDescription Returns details on the active state of the TX memory pool. - * - * @apiExample Example usage: - * curl -X GET https://mainnet.bchjs.cash/v3/getMempoolInfo -H "accept: application/json" - * - */ - async getMempoolInfo(req, res, next) { - try { - // Axios options - const options = _this.routeUtils.getAxiosOptions() - - options.data.id = "getmempoolinfo" - options.data.method = "getmempoolinfo" - options.data.params = [] - - const response = await _this.axios.request(options) - return res.json(response.data.result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/getMempoolInfo().", err) - - return _this.errorHandler(err, res) - } - } - - /** - * @api {get} /blockchain/getRawMempool Get mempool info - * @apiName getMempoolInfo - * @apiGroup Blockchain - * @apiDescription Returns details on the active state of the TX memory pool. - * - * @apiExample Example usage: - * curl -X GET https://mainnet.bchjs.cash/v3/getMempoolInfo -H "accept: application/json" - * - */ - - /** - * @api {get} /blockchain/getRawMempool/?verbose= Get raw mempool - * @apiName getRawMempool - * @apiGroup Blockchain - * @apiDescription Returns all transaction ids in memory pool as a json array - * of string transaction ids. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/getRawMempool/?verbose=true" -H "accept: application/json" - * - * @apiParam {Boolean} verbose Return verbose data - * - */ - async getRawMempool(req, res, next) { - try { - // Axios options - const options = _this.routeUtils.getAxiosOptions() - - let verbose = false - if (req.query.verbose && req.query.verbose === "true") verbose = true - - options.data.id = "getrawmempool" - options.data.method = "getrawmempool" - options.data.params = [verbose] - - const response = await _this.axios.request(options) - - return res.json(response.data.result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/getRawMempool().", err) - - return _this.errorHandler(err, res) - } - } - - /** - * @api {get} /blockchain/getTxOut/:txid/:n?mempool= Get Tx Out - * @apiName getTxOut - * @apiGroup Blockchain - * @apiDescription Returns details about an unspent transaction output. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getTxOut/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33/0?mempool=false" -H "accept: application/json" - * - * @apiParam {String} txid Transaction id (required) - * @apiParam {Number} n Output number (required) - * @apiParam {Boolean} mempool Check mempool or not (optional) - * - */ - // Returns details about an unspent transaction output. - async getTxOut(req, res, next) { - try { - // Validate input parameter - const txid = req.params.txid - if (!txid || txid === "") { - res.status(400) - return res.json({ error: "txid can not be empty" }) - } - - let n = req.params.n - if (n === undefined || n === "") { - res.status(400) - return res.json({ error: "n can not be empty" }) - } - n = parseInt(n) - - let include_mempool = false - if (req.query.include_mempool && req.query.include_mempool === "true") - include_mempool = true - - // Axios options - const options = _this.routeUtils.getAxiosOptions() - - options.data.id = "gettxout" - options.data.method = "gettxout" - options.data.params = [txid, n, include_mempool] - - // console.log(`requestConfig: ${JSON.stringify(requestConfig, null, 2)}`) - - const response = await _this.axios.request(options) - - return res.json(response.data.result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/getTxOut().", err) - - return _this.errorHandler(err, res) - } - } - - /** - * @api {get} /blockchain/getTxOutProofSingle/:txid Get Tx Out Proof - * @apiName getTxOutProofSingle - * @apiGroup Blockchain - * @apiDescription Returns a hex-encoded proof that 'txid' was included in a block. - * - * @apiExample Example usage: - * curl -X GET "https://mainnet.bchjs.cash/v3/blockchain/getTxOutProofSingle/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" - * - * @apiParam {String} txid Transaction id (required) - * - */ - async getTxOutProofSingle(req, res, next) { - try { - // Validate input parameter - const txid = req.params.txid - if (!txid || txid === "") { - res.status(400) - return res.json({ error: "txid can not be empty" }) - } - - // Axios options - const options = _this.routeUtils.getAxiosOptions() - - options.data.id = "gettxoutproof" - options.data.method = "gettxoutproof" - options.data.params = [[txid]] - - const response = await _this.axios.request(options) - - return res.json(response.data.result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/getTxOutProofSingle().", err) - - return _this.errorHandler(err, res) - } - } - - // Returns a hex-encoded proof that 'txid' was included in a block. - async getTxOutProofBulk(req, res, next) { - try { - const txids = req.body.txids - - // Reject if txids is not an array. - if (!Array.isArray(txids)) { - res.status(400) - return res.json({ - error: "txids needs to be an array. Use GET for single txid." - }) - } - - // 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." - }) - } - - // Axios options - const options = _this.routeUtils.getAxiosOptions() - - // Validate each element in the array. - for (let i = 0; i < txids.length; i++) { - const txid = txids[i] - - if (txid.length !== 64) { - res.status(400) - return res.json({ - error: `Invalid txid. Double check your txid is valid: ${txid}` - }) - } - } - - wlogger.debug( - "Executing blockchain/getTxOutProof with these txids: ", - txids - ) - - // Loop through each txid and creates an array of requests to call in parallel - const promises = txids.map(async txid => { - options.data.id = "gettxoutproof" - options.data.method = "gettxoutproof" - options.data.params = [[txid]] - - return await _this.axios.request(options) - }) - - // Wait for all parallel promisses to resolve. - const axiosResult = await _this.axios.all(promises) - - // Extract the data component from the axios response. - const result = axiosResult.map(x => x.data.result) - - res.status(200) - return res.json(result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/getTxOutProofBulk().", err) - - return _this.errorHandler(err, res) - } - } - - async verifyTxOutProofSingle(req, res, next) { - try { - // Validate input parameter - const proof = req.params.proof - if (!proof || proof === "") { - res.status(400) - return res.json({ error: "proof can not be empty" }) - } - - // Axios options - const options = _this.routeUtils.getAxiosOptions() - - options.data.id = "verifytxoutproof" - options.data.method = "verifytxoutproof" - options.data.params = [req.params.proof] - - const response = await _this.axios.request(options) - - return res.json(response.data.result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/verifyTxOutProofSingle().", err) - - return _this.errorHandler(err, res) - } - } - - async verifyTxOutProofBulk(req, res, next) { - try { - const proofs = req.body.proofs - - // Reject if proofs is not an array. - if (!Array.isArray(proofs)) { - res.status(400) - return res.json({ - error: "proofs needs to be an array. Use GET for single proof." - }) - } - - // Enforce array size rate limits - if (!routeUtils.validateArraySize(req, proofs)) { - res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 - return res.json({ - error: "Array too large." - }) - } - - // Axios options - const options = _this.routeUtils.getAxiosOptions() - - // Validate each element in the array. - for (let i = 0; i < proofs.length; i++) { - const proof = proofs[i] - - if (!proof || proof === "") { - res.status(400) - return res.json({ error: `proof can not be empty: ${proof}` }) - } - } - - wlogger.debug( - "Executing blockchain/verifyTxOutProof with these proofs: ", - proofs - ) - - // Loop through each proof and creates an array of requests to call in parallel - const promises = proofs.map(async proof => { - options.data.id = "verifytxoutproof" - options.data.method = "verifytxoutproof" - options.data.params = [proof] - - return await _this.axios.request(options) - }) - - // Wait for all parallel promisses to resolve. - const axiosResult = await _this.axios.all(promises) - - // Extract the data component from the axios response. - const result = axiosResult.map(x => x.data.result[0]) - - res.status(200) - return res.json(result) - } catch (err) { - // Write out error to error log. - // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) - wlogger.error("Error in blockchain.ts/verifyTxOutProofBulk().", err) - - return _this.errorHandler(err, res) - } - } -} - -module.exports = Blockchain diff --git a/test/v3/blockchain.js b/test/v3/blockchain.js index 96bd801..8aa718d 100644 --- a/test/v3/blockchain.js +++ b/test/v3/blockchain.js @@ -10,8 +10,10 @@ const chai = require("chai") const assert = chai.assert -const nock = require("nock") // HTTP mocking -const blockchainRoute = require("../../src/routes/v3/full-node/blockchain") +const sinon = require("sinon") + +const Blockchain = require("../../src/routes/v3/full-node/blockchain") +const uut = new Blockchain() const util = require("util") util.inspect.defaultOptions = { depth: 1 } @@ -26,9 +28,10 @@ let originalEnvVars // Used during transition from integration to unit tests. describe("#BlockchainRouter", () => { let req, res + let sandbox // local node will be started in regtest mode on the port 48332 - //before(panda.runLocalNode) + // before(panda.runLocalNode) before(() => { // Save existing environment variables. @@ -57,20 +60,17 @@ describe("#BlockchainRouter", () => { // Explicitly reset the parmas and body. req.params = {} req.body = {} - - // Activate nock if it's inactive. - if (!nock.isActive()) nock.activate() + sandbox = sinon.createSandbox() }) afterEach(() => { - // Clean up HTTP mocks. - nock.cleanAll() // clear interceptor list. - nock.restore() + // Restore Sandbox + sandbox.restore() }) after(() => { // otherwise the panda will run forever - //process.exit() + // process.exit() // Restore any pre-existing environment variables. process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL @@ -80,20 +80,14 @@ describe("#BlockchainRouter", () => { }) describe("#root", () => { - // root route handler. - const root = blockchainRoute.testableComponents.root - it("should respond to GET for base route", async () => { - const result = root(req, res) + const result = uut.root(req, res) assert.equal(result.status, "blockchain", "Returns static string") }) }) describe("getBestBlockHash()", () => { - // block route handler. - const getBestBlockHash = blockchainRoute.testableComponents.getBestBlockHash - it("should throw 503 when network issues", async () => { // Save the existing RPC URL. const savedUrl2 = process.env.RPC_BASEURL @@ -101,8 +95,8 @@ describe("#BlockchainRouter", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - const result = await getBestBlockHash(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getBestBlockHash(req, res) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -115,20 +109,46 @@ describe("#BlockchainRouter", () => { ) }) - // it("return proper error connection is refused", async () => { - // - // }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getBestBlockHash(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getBestBlockHash(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) it("should GET /getBestBlockHash", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: mockData.mockBlockHash }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockBlockHash } }) } - const result = await getBestBlockHash(req, res) - // console.log(`result: ${util.inspect(result)}`) + const result = await uut.getBestBlockHash(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isString(result) assert.equal(result.length, 64, "Hash string is fixed length") @@ -136,10 +156,6 @@ describe("#BlockchainRouter", () => { }) describe("getBlockchainInfo()", () => { - // block route handler. - const getBlockchainInfo = - blockchainRoute.testableComponents.getBlockchainInfo - it("should throw 503 when network issues", async () => { // Save the existing RPC URL. const savedUrl2 = process.env.RPC_BASEURL @@ -147,8 +163,8 @@ describe("#BlockchainRouter", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - const result = await getBlockchainInfo(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getBlockchainInfo(req, res) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -161,16 +177,46 @@ describe("#BlockchainRouter", () => { ) }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getBestBlockHash(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getBestBlockHash(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("should GET /getBlockchainInfo", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: mockData.mockBlockchainInfo }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockBlockchainInfo } }) } - const result = await getBlockchainInfo(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getBlockchainInfo(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAnyKeys(result, [ "chain", @@ -189,9 +235,6 @@ describe("#BlockchainRouter", () => { }) describe("getBlockCount()", () => { - // block route handler. - const getBlockCount = blockchainRoute.testableComponents.getBlockCount - it("should throw 503 when network issues", async () => { // Save the existing RPC URL. const savedUrl2 = process.env.RPC_BASEURL @@ -199,8 +242,8 @@ describe("#BlockchainRouter", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - const result = await getBlockCount(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getBlockCount(req, res) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -213,33 +256,58 @@ describe("#BlockchainRouter", () => { ) }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getBlockCount(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getBlockCount(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("should GET /getBlockCount", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: 126769 }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: 126769 } }) } - const result = await getBlockCount(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getBlockCount(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.isNumber(result) }) }) describe("getBlockHeaderSingle()", async () => { - const getBlockHeader = - blockchainRoute.testableComponents.getBlockHeaderSingle - it("should throw 400 error if hash is missing", async () => { - const result = await getBlockHeader(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getBlockHeaderSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "hash can not be empty") }) - it("should throw 503 when network issues", async () => { // Save the existing RPC URL. const savedUrl2 = process.env.RPC_BASEURL @@ -250,8 +318,8 @@ describe("#BlockchainRouter", () => { req.params.hash = "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" - const result = await getBlockHeader(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getBlockHeaderSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -263,22 +331,55 @@ describe("#BlockchainRouter", () => { "Error message expected" ) }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + req.params.hash = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await uut.getBlockHeaderSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.params.hash = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await uut.getBlockHeaderSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) it("should GET block header", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { + sandbox.stub(uut.axios, "request").resolves({ + data: { result: "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c" - }) + } + }) } req.params.hash = "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - const result = await getBlockHeader(req, res) + const result = await uut.getBlockHeaderSingle(req, res) // console.log(`result: ${util.inspect(result)}`) assert.isString(result) @@ -291,17 +392,17 @@ describe("#BlockchainRouter", () => { it("should GET verbose block header", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: mockData.mockBlockHeader }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockBlockHeader } }) } req.query.verbose = true req.params.hash = "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - const result = await getBlockHeader(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getBlockHeaderSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, [ "hash", @@ -322,15 +423,13 @@ describe("#BlockchainRouter", () => { }) }) - describe("#getBlockHeaderBulk", () => { - // route handler. - const getBlockHeaderBulk = - blockchainRoute.testableComponents.getBlockHeaderBulk + // + describe("#getBlockHeaderBulk", () => { it("should throw an error for an empty body", async () => { req.body = {} - const result = await getBlockHeaderBulk(req, res) + const result = await uut.getBlockHeaderBulk(req, res) assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") assert.include( @@ -344,7 +443,7 @@ describe("#BlockchainRouter", () => { req.body.hashes = "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" - const result = await getBlockHeaderBulk(req, res) + const result = await uut.getBlockHeaderBulk(req, res) assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") assert.include( @@ -360,8 +459,8 @@ describe("#BlockchainRouter", () => { req.body.hashes = testArray - const result = await getBlockHeaderBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getBlockHeaderBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "Array too large") @@ -370,7 +469,7 @@ describe("#BlockchainRouter", () => { it("should throw a 400 error for an invalid hash", async () => { req.body.hashes = ["badHash"] - await getBlockHeaderBulk(req, res) + await uut.getBlockHeaderBulk(req, res) // console.log(`result: ${util.inspect(result)}`) assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") @@ -387,7 +486,7 @@ describe("#BlockchainRouter", () => { // Switch the Insight URL to something that will error out. process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" - const result = await getBlockHeaderBulk(req, res) + const result = await uut.getBlockHeaderBulk(req, res) // Restore the saved URL. process.env.BITCOINCOM_BASEURL = savedUrl @@ -399,6 +498,42 @@ describe("#BlockchainRouter", () => { process.env.BITCOINCOM_BASEURL = savedUrl } }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.body.hashes = [ + "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" + ] + + const result = await uut.getBlockHeaderBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.body.hashes = [ + "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" + ] + + const result = await uut.getBlockHeaderBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) it("should get concise block header for a single hash", async () => { req.body.hashes = [ @@ -407,13 +542,13 @@ describe("#BlockchainRouter", () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: mockData.mockBlockHeaderConcise }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockBlockHeaderConcise } }) } // Call the details API. - const result = await getBlockHeaderBulk(req, res) + const result = await uut.getBlockHeaderBulk(req, res) // console.log(`result: ${util.inspect(result)}`) // Assert that required fields exist in the returned object. @@ -434,14 +569,14 @@ describe("#BlockchainRouter", () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: mockData.mockBlockHeader }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockBlockHeader } }) } // Call the details API. - const result = await getBlockHeaderBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getBlockHeaderBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) // Assert that required fields exist in the returned object. assert.isArray(result) @@ -471,25 +606,20 @@ describe("#BlockchainRouter", () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .times(2) - .reply(200, { result: mockData.mockBlockHeaderConcise }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockBlockHeaderConcise } }) } // Call the details API. - const result = await getBlockHeaderBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getBlockHeaderBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.isArray(result) assert.equal(result.length, 2, "2 outputs for 2 inputs") }) }) - describe("getChainTips()", () => { - // block route handler. - const getChainTips = blockchainRoute.testableComponents.getChainTips - it("should throw 503 when network issues", async () => { // Save the existing RPC URL. const savedUrl2 = process.env.RPC_BASEURL @@ -497,8 +627,8 @@ describe("#BlockchainRouter", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - const result = await getChainTips(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getChainTips(req, res) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -510,27 +640,51 @@ describe("#BlockchainRouter", () => { "Error message expected" ) }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getChainTips(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getChainTips(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) it("should GET /getChainTips", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: mockData.mockChainTips }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockChainTips } }) } - const result = await getChainTips(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getChainTips(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.isArray(result) assert.hasAnyKeys(result[0], ["height", "hash", "branchlen", "status"]) }) }) - describe("getDifficulty()", () => { - // block route handler. - const getDifficulty = blockchainRoute.testableComponents.getDifficulty - it("should throw 503 when network issues", async () => { // Save the existing RPC URL. const savedUrl2 = process.env.RPC_BASEURL @@ -538,8 +692,8 @@ describe("#BlockchainRouter", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - const result = await getDifficulty(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getDifficulty(req, res) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -551,26 +705,50 @@ describe("#BlockchainRouter", () => { "Error message expected" ) }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getDifficulty(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getDifficulty(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) it("should GET /getDifficulty", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: 4049809.205246544 }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: 4049809.205246544 } }) } - const result = await getDifficulty(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getDifficulty(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.isNumber(result) }) }) - describe("getMempoolInfo()", () => { - // block route handler. - const getMempoolInfo = blockchainRoute.testableComponents.getMempoolInfo - it("should throw 503 when network issues", async () => { // Save the existing RPC URL. const savedUrl2 = process.env.RPC_BASEURL @@ -578,8 +756,8 @@ describe("#BlockchainRouter", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - const result = await getMempoolInfo(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getMempoolInfo(req, res) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -591,17 +769,45 @@ describe("#BlockchainRouter", () => { "Error message expected" ) }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getMempoolInfo(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getMempoolInfo(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) it("should GET /getMempoolInfo", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: mockData.mockMempoolInfo }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockMempoolInfo } }) } - const result = await getMempoolInfo(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getMempoolInfo(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAnyKeys(result, [ "result", @@ -612,11 +818,7 @@ describe("#BlockchainRouter", () => { ]) }) }) - describe("getRawMempool()", () => { - // block route handler. - const getRawMempool = blockchainRoute.testableComponents.getRawMempool - it("should throw 503 when network issues", async () => { // Save the existing RPC URL. const savedUrl2 = process.env.RPC_BASEURL @@ -624,8 +826,8 @@ describe("#BlockchainRouter", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - const result = await getRawMempool(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getRawMempool(req, res) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -637,31 +839,54 @@ describe("#BlockchainRouter", () => { "Error message expected" ) }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + const result = await uut.getRawMempool(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + const result = await uut.getRawMempool(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) it("should GET /getMempoolInfo", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: mockData.mockRawMempool }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockRawMempool } }) } - const result = await getRawMempool(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getRawMempool(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.isArray(result) // Not sure what other assertions should be made here. }) }) - - describe("getMempoolEntry()", () => { - // block route handler. - const getMempoolEntry = - blockchainRoute.testableComponents.getMempoolEntrySingle - + describe("getMempoolEntrySingle()", () => { it("should throw 400 if txid is empty", async () => { - const result = await getMempoolEntry(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getMempoolEntrySingle(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "txid can not be empty") @@ -674,10 +899,11 @@ describe("#BlockchainRouter", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - const result = await getMempoolEntry(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getMempoolEntrySingle(req, res) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -689,35 +915,65 @@ describe("#BlockchainRouter", () => { "Error message expected" ) }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" + + const result = await uut.getMempoolEntrySingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" + + const result = await uut.getMempoolEntrySingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) it("should GET /getMempoolEntry", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: { error: "Transaction not in mempool" } }) + sandbox.stub(uut.axios, "request").resolves({ + data: { result: { error: "Transaction not in mempool" } } + }) } - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - const result = await getMempoolEntry(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getMempoolEntrySingle(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.isString(result.error) assert.equal(result.error, "Transaction not in mempool") }) }) - describe("#getMempoolEntryBulk", () => { - // route handler. - const getMempoolEntryBulk = - blockchainRoute.testableComponents.getMempoolEntryBulk - it("should throw an error for an empty body", async () => { req.body = {} - const result = await getMempoolEntryBulk(req, res) + const result = await uut.getMempoolEntryBulk(req, res) assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") assert.include( @@ -728,9 +984,10 @@ describe("#BlockchainRouter", () => { }) it("should error on non-array single txid", async () => { - req.body.txids = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.body.txids = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - const result = await getMempoolEntryBulk(req, res) + const result = await uut.getMempoolEntryBulk(req, res) assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") assert.include( @@ -746,13 +1003,48 @@ describe("#BlockchainRouter", () => { req.body.txids = testArray - const result = await getMempoolEntryBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getMempoolEntryBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "Array too large") }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + req.body.txids = [ + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" + ] + + const result = await uut.getMempoolEntryBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.body.txids = [ + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" + ] + + const result = await uut.getMempoolEntryBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) // Only execute on integration tests. if (process.env.TEST !== "unit") { // Dev-note: This test passes because it expects an error. TXIDs do not @@ -760,11 +1052,11 @@ describe("#BlockchainRouter", () => { // integration test. it("should retrieve single mempool entry", async () => { req.body.txids = [ - `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" ] - const result = await getMempoolEntryBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getMempoolEntryBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.isString(result.error) @@ -776,12 +1068,12 @@ describe("#BlockchainRouter", () => { // integration test. it("should retrieve multiple mempool entries", async () => { req.body.txids = [ - `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde`, - `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde", + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" ] - const result = await getMempoolEntryBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getMempoolEntryBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.isString(result.error) @@ -789,15 +1081,10 @@ describe("#BlockchainRouter", () => { }) } }) - describe("getMempoolAncestorsSingle()", () => { - // block route handler. - const getMempoolAncestorsSingle = - blockchainRoute.testableComponents.getMempoolAncestorsSingle - it("should throw 400 if txid is empty", async () => { - const result = await getMempoolAncestorsSingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getMempoolAncestorsSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "txid can not be empty") @@ -810,10 +1097,11 @@ describe("#BlockchainRouter", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - const result = await getMempoolAncestorsSingle(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getMempoolAncestorsSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -825,37 +1113,67 @@ describe("#BlockchainRouter", () => { "Error message expected" ) }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" + + const result = await uut.getMempoolAncestorsSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" + + const result = await uut.getMempoolAncestorsSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) it("should GET /getMempoolAncestorsSingle", async () => { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: mockData.mockAncestors }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockAncestors } }) - req.params.txid = `bb0d349892d351da2767f8c45f6f7949713ff09bd12838d53e76158ddee3ce93` + req.params.txid = + "bb0d349892d351da2767f8c45f6f7949713ff09bd12838d53e76158ddee3ce93" - const result = await getMempoolAncestorsSingle(req, res) + const result = await uut.getMempoolAncestorsSingle(req, res) // console.log(`result: ${util.inspect(result)}`) assert.isArray(result) }) }) - describe("getTxOut()", () => { - // block route handler. - const getTxOut = blockchainRoute.testableComponents.getTxOut - it("should throw 400 if txid is empty", async () => { - const result = await getTxOut(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getTxOut(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "txid can not be empty") }) it("should throw 400 if n is empty", async () => { - req.params.txid = `sometxid` - const result = await getTxOut(req, res) - //console.log(`result: ${util.inspect(result)}`) + req.params.txid = "sometxid" + const result = await uut.getTxOut(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "n can not be empty") @@ -868,11 +1186,12 @@ describe("#BlockchainRouter", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" req.params.n = 0 - const result = await getTxOut(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getTxOut(req, res) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -885,20 +1204,59 @@ describe("#BlockchainRouter", () => { ) }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.params.txid = + "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" + req.params.n = 0 + req.query.include_mempool = "true" + + const result = await uut.getTxOut(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.params.txid = + "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" + req.params.n = 0 + req.query.include_mempool = "true" + + const result = await uut.getTxOut(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) // This test can only run for unit tests. See TODO at the top of this file. it("should GET /getTxOut", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: mockData.mockTxOut }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockTxOut } }) } - req.params.txid = `197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d` + req.params.txid = + "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" req.params.n = 0 req.query.include_mempool = "true" - const result = await getTxOut(req, res) + const result = await uut.getTxOut(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.hasAllKeys(result, [ @@ -918,13 +1276,10 @@ describe("#BlockchainRouter", () => { assert.isArray(result.scriptPubKey.addresses) }) }) - - describe("getTxOutProof()", () => { - const getTxOutProof = blockchainRoute.testableComponents.getTxOutProofSingle - + describe("getTxOutProofSingle()", () => { it("should throw 400 if txid is empty", async () => { - const result = await getTxOutProof(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getTxOutProofSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "txid can not be empty") @@ -937,10 +1292,11 @@ describe("#BlockchainRouter", () => { // Manipulate the URL to cause a 500 network error. process.env.RPC_BASEURL = "http://fakeurl/api/" - req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.txid = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - const result = await getTxOutProof(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getTxOutProofSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -952,33 +1308,67 @@ describe("#BlockchainRouter", () => { "Error message expected" ) }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.params.txid = + "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" + req.params.n = 0 + req.query.include_mempool = "true" + + const result = await uut.getTxOutProofSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.params.txid = + "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" + req.params.n = 0 + req.query.include_mempool = "true" + + const result = await uut.getTxOutProofSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) it("should GET /getTxOutProof", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: mockData.mockTxOutProof }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockTxOutProof } }) } - req.params.txid = `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` + req.params.txid = + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" - const result = await getTxOutProof(req, res) + const result = await uut.getTxOutProofSingle(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isString(result) }) }) - describe("#getTxOutProofBulk", () => { - // route handler. - const getTxOutProofBulk = - blockchainRoute.testableComponents.getTxOutProofBulk - it("should throw an error for an empty body", async () => { req.body = {} - const result = await getTxOutProofBulk(req, res) + const result = await uut.getTxOutProofBulk(req, res) assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") assert.include( @@ -989,9 +1379,10 @@ describe("#BlockchainRouter", () => { }) it("should error on non-array single txid", async () => { - req.body.txids = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.body.txids = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - const result = await getTxOutProofBulk(req, res) + const result = await uut.getTxOutProofBulk(req, res) assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") assert.include( @@ -1007,27 +1398,63 @@ describe("#BlockchainRouter", () => { req.body.txids = testArray - const result = await getTxOutProofBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.getTxOutProofBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "Array too large") }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.body.txids = [ + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" + ] + + const result = await uut.getTxOutProofBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.body.txids = [ + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" + ] + + const result = await uut.getTxOutProofBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) it("should GET proof for single txid", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: mockData.mockTxOutProof }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockTxOutProof } }) } req.body.txids = [ - `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" ] - const result = await getTxOutProofBulk(req, res) - //console.log(`result: ${JSON.stringify(result, null, 2)}`) + const result = await uut.getTxOutProofBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) assert.isString(result[0]) @@ -1036,32 +1463,27 @@ describe("#BlockchainRouter", () => { it("should GET proof for multiple txids", async () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .times(2) - .reply(200, { result: mockData.mockTxOutProof }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: mockData.mockTxOutProof } }) } req.body.txids = [ - `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266`, - `2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266` + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266", + "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" ] - const result = await getTxOutProofBulk(req, res) - //console.log(`result: ${JSON.stringify(result, null, 2)}`) + const result = await uut.getTxOutProofBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) assert.equal(result.length, 2, "Correct length of returned array") }) }) - - describe("verifyTxOutProof()", () => { - const verifyTxOutProof = - blockchainRoute.testableComponents.verifyTxOutProofSingle - + describe("verifyTxOutProofSingle()", () => { it("should throw 400 if proof is empty", async () => { - const result = await verifyTxOutProof(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.verifyTxOutProofSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "proof can not be empty") @@ -1076,8 +1498,8 @@ describe("#BlockchainRouter", () => { req.params.proof = mockData.mockTxOutProof - const result = await verifyTxOutProof(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.verifyTxOutProofSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) // Restore the saved URL. process.env.RPC_BASEURL = savedUrl2 @@ -1089,39 +1511,67 @@ describe("#BlockchainRouter", () => { "Error message expected" ) }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + req.params.proof = + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" + + const result = await uut.verifyTxOutProofSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.params.proof = + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" + + const result = await uut.verifyTxOutProofSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) it("should GET /verifyTxOutProof", async () => { const expected = "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: [expected] }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: [expected] } }) } req.params.proof = "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" - const result = await verifyTxOutProof(req, res) - //console.log(`result: ${JSON.stringify(result, null, 2)}`) + const result = await uut.verifyTxOutProofSingle(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) assert.isString(result[0]) assert.equal(result[0], expected) }) }) - describe("#verifyTxOutProofBulk", () => { - // route handler. - const verifyTxOutProofBulk = - blockchainRoute.testableComponents.verifyTxOutProofBulk - it("should throw an error for an empty body", async () => { req.body = {} - const result = await verifyTxOutProofBulk(req, res) + const result = await uut.verifyTxOutProofBulk(req, res) assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") assert.include( @@ -1134,7 +1584,7 @@ describe("#BlockchainRouter", () => { it("should error on non-array single txid", async () => { req.body.proofs = mockData.mockTxOutProof - const result = await verifyTxOutProofBulk(req, res) + const result = await uut.verifyTxOutProofBulk(req, res) assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") assert.include( @@ -1150,30 +1600,65 @@ describe("#BlockchainRouter", () => { req.body.proofs = testArray - const result = await verifyTxOutProofBulk(req, res) - //console.log(`result: ${util.inspect(result)}`) + const result = await uut.verifyTxOutProofBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) assert.hasAllKeys(result, ["error"]) assert.include(result.error, "Array too large") }) + it("returns proper error when downstream service stalls", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) + + req.body.proofs = [ + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" + ] + const result = await uut.verifyTxOutProofBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("returns proper error when downstream service is down", async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) + + req.body.proofs = [ + "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" + ] + const result = await uut.verifyTxOutProofBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + it("should get single proof", async () => { const expected = "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { result: [expected] }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: [expected] } }) } req.body.proofs = [ "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" ] - const result = await verifyTxOutProofBulk(req, res) - //console.log(`result: ${JSON.stringify(result, null, 2)}`) + const result = await uut.verifyTxOutProofBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) assert.isString(result[0]) @@ -1186,10 +1671,9 @@ describe("#BlockchainRouter", () => { // Mock the RPC call for unit tests. if (process.env.TEST === "unit") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .times(2) - .reply(200, { result: [expected] }) + sandbox + .stub(uut.axios, "request") + .resolves({ data: { result: [expected] } }) } req.body.proofs = [ @@ -1197,8 +1681,8 @@ describe("#BlockchainRouter", () => { "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" ] - const result = await verifyTxOutProofBulk(req, res) - //console.log(`result: ${JSON.stringify(result, null, 2)}`) + const result = await uut.verifyTxOutProofBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) assert.isString(result[0]) diff --git a/test/v3/blockchain2.js b/test/v3/blockchain2.js deleted file mode 100644 index 956de95..0000000 --- a/test/v3/blockchain2.js +++ /dev/null @@ -1,1693 +0,0 @@ -/* - TODO: - -getRawMempool - --Add tests for 'verbose' input values - -getMempoolEntry & getMempoolEntryBulk - --Needs e2e test to create unconfirmed tx, for real-world test. -*/ - -"use strict" - -const chai = require("chai") -const assert = chai.assert -const sinon = require("sinon") - -const Blockchain = require("../../src/routes/v3/full-node/blockchain2") -const uut = new Blockchain() - -const util = require("util") -util.inspect.defaultOptions = { depth: 1 } - -if (!process.env.TEST) process.env.TEST = "unit" - -// Mocking data. -const { mockReq, mockRes } = require("./mocks/express-mocks") -const mockData = require("./mocks/blockchain-mock") - -let originalEnvVars // Used during transition from integration to unit tests. - -describe("#BlockchainRouter2", () => { - let req, res - let sandbox - - // local node will be started in regtest mode on the port 48332 - // before(panda.runLocalNode) - - 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 === "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 = {} - sandbox = sinon.createSandbox() - }) - - afterEach(() => { - // Restore Sandbox - sandbox.restore() - }) - - after(() => { - // otherwise the panda will run forever - // process.exit() - - // 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", () => { - it("should respond to GET for base route", async () => { - const result = uut.root(req, res) - - assert.equal(result.status, "blockchain", "Returns static string") - }) - }) - - describe("getBestBlockHash()", () => { - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL - - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" - - const result = await uut.getBestBlockHash(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - const result = await uut.getBestBlockHash(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - const result = await uut.getBestBlockHash(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("should GET /getBestBlockHash", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockBlockHash } }) - } - - const result = await uut.getBestBlockHash(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isString(result) - assert.equal(result.length, 64, "Hash string is fixed length") - }) - }) - - describe("getBlockchainInfo()", () => { - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL - - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" - - const result = await uut.getBlockchainInfo(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - const result = await uut.getBestBlockHash(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - const result = await uut.getBestBlockHash(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("should GET /getBlockchainInfo", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockBlockchainInfo } }) - } - - const result = await uut.getBlockchainInfo(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAnyKeys(result, [ - "chain", - "blocks", - "headers", - "bestblockhash", - "difficulty", - "mediantime", - "verificationprogress", - "chainwork", - "pruned", - "softforks", - "bip9_softforks" - ]) - }) - }) - - describe("getBlockCount()", () => { - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL - - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" - - const result = await uut.getBlockCount(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - const result = await uut.getBlockCount(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - const result = await uut.getBlockCount(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("should GET /getBlockCount", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: 126769 } }) - } - - const result = await uut.getBlockCount(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.isNumber(result) - }) - }) - - describe("getBlockHeaderSingle()", async () => { - it("should throw 400 error if hash is missing", async () => { - const result = await uut.getBlockHeaderSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.include(result.error, "hash can not be empty") - }) - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL - - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" - - req.params.hash = - "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" - - const result = await uut.getBlockHeaderSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Network error: Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - req.params.hash = - "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" - - const result = await uut.getBlockHeaderSingle(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - req.params.hash = - "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" - - const result = await uut.getBlockHeaderSingle(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("should GET block header", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox.stub(uut.axios, "request").resolves({ - data: { - result: - "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c" - } - }) - } - - req.params.hash = - "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - - const result = await uut.getBlockHeaderSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.isString(result) - assert.equal( - result, - "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c" - ) - }) - - it("should GET verbose block header", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockBlockHeader } }) - } - - req.query.verbose = true - req.params.hash = - "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - - const result = await uut.getBlockHeaderSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, [ - "hash", - "confirmations", - "height", - "version", - "versionHex", - "merkleroot", - "time", - "mediantime", - "nonce", - "bits", - "difficulty", - "chainwork", - "previousblockhash", - "nextblockhash" - ]) - }) - }) - - // - - describe("#getBlockHeaderBulk", () => { - it("should throw an error for an empty body", async () => { - req.body = {} - - const result = await uut.getBlockHeaderBulk(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 hash", async () => { - req.body.hashes = - "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" - - const result = await uut.getBlockHeaderBulk(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 uut.getBlockHeaderBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.include(result.error, "Array too large") - }) - - it("should throw a 400 error for an invalid hash", async () => { - req.body.hashes = ["badHash"] - - await uut.getBlockHeaderBulk(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.hashes = [ - "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" - ] - - // Switch the Insight URL to something that will error out. - process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" - - const result = await uut.getBlockHeaderBulk(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("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - req.body.hashes = [ - "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - ] - - const result = await uut.getBlockHeaderBulk(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - req.body.hashes = [ - "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - ] - - const result = await uut.getBlockHeaderBulk(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("should get concise block header for a single hash", async () => { - req.body.hashes = [ - "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - ] - - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockBlockHeaderConcise } }) - } - - // Call the details API. - const result = await uut.getBlockHeaderBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Assert that required fields exist in the returned object. - assert.isArray(result) - assert.equal( - result[0], - "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c" - ) - }) - - it("should get verbose block header for a single hash", async () => { - req.body = { - hashes: [ - "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - ], - verbose: true - } - - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockBlockHeader } }) - } - - // Call the details API. - const result = await uut.getBlockHeaderBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Assert that required fields exist in the returned object. - assert.isArray(result) - assert.hasAllKeys(result[0], [ - "hash", - "confirmations", - "height", - "version", - "versionHex", - "merkleroot", - "time", - "mediantime", - "nonce", - "bits", - "difficulty", - "chainwork", - "previousblockhash", - "nextblockhash" - ]) - }) - - it("should get details for multiple block heights", async () => { - req.body.hashes = [ - "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0", - "000000000000000002fa9d7851b284c53a5831651b04211c266badf2ad2d8ef0" - ] - - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockBlockHeaderConcise } }) - } - - // Call the details API. - const result = await uut.getBlockHeaderBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.isArray(result) - assert.equal(result.length, 2, "2 outputs for 2 inputs") - }) - }) - describe("getChainTips()", () => { - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL - - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" - - const result = await uut.getChainTips(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - const result = await uut.getChainTips(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - const result = await uut.getChainTips(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("should GET /getChainTips", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockChainTips } }) - } - - const result = await uut.getChainTips(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.isArray(result) - assert.hasAnyKeys(result[0], ["height", "hash", "branchlen", "status"]) - }) - }) - describe("getDifficulty()", () => { - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL - - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" - - const result = await uut.getDifficulty(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - const result = await uut.getDifficulty(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - const result = await uut.getDifficulty(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("should GET /getDifficulty", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: 4049809.205246544 } }) - } - - const result = await uut.getDifficulty(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.isNumber(result) - }) - }) - describe("getMempoolInfo()", () => { - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL - - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" - - const result = await uut.getMempoolInfo(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - const result = await uut.getMempoolInfo(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - const result = await uut.getMempoolInfo(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("should GET /getMempoolInfo", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockMempoolInfo } }) - } - - const result = await uut.getMempoolInfo(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAnyKeys(result, [ - "result", - "bytes", - "usage", - "maxmempool", - "mempoolminfree" - ]) - }) - }) - describe("getRawMempool()", () => { - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL - - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" - - const result = await uut.getRawMempool(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - const result = await uut.getRawMempool(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - const result = await uut.getRawMempool(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("should GET /getMempoolInfo", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockRawMempool } }) - } - - const result = await uut.getRawMempool(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.isArray(result) - // Not sure what other assertions should be made here. - }) - }) - describe("getMempoolEntrySingle()", () => { - it("should throw 400 if txid is empty", async () => { - const result = await uut.getMempoolEntrySingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.include(result.error, "txid can not be empty") - }) - - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL - - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" - - req.params.txid = - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - - const result = await uut.getMempoolEntrySingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - req.params.txid = - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - - const result = await uut.getMempoolEntrySingle(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - req.params.txid = - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - - const result = await uut.getMempoolEntrySingle(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("should GET /getMempoolEntry", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox.stub(uut.axios, "request").resolves({ - data: { result: { error: "Transaction not in mempool" } } - }) - } - - req.params.txid = - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - - const result = await uut.getMempoolEntrySingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.isString(result.error) - assert.equal(result.error, "Transaction not in mempool") - }) - }) - describe("#getMempoolEntryBulk", () => { - it("should throw an error for an empty body", async () => { - req.body = {} - - const result = await uut.getMempoolEntryBulk(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 = - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - - const result = await uut.getMempoolEntryBulk(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 400 error if addresses array is too large", async () => { - const testArray = [] - for (var i = 0; i < 25; i++) testArray.push("") - - req.body.txids = testArray - - const result = await uut.getMempoolEntryBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.include(result.error, "Array too large") - }) - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - req.body.txids = [ - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - ] - - const result = await uut.getMempoolEntryBulk(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - req.body.txids = [ - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - ] - - const result = await uut.getMempoolEntryBulk(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - // Only execute on integration tests. - if (process.env.TEST !== "unit") { - // Dev-note: This test passes because it expects an error. TXIDs do not - // stay in the mempool for long, so it does not work well for a unit or - // integration test. - it("should retrieve single mempool entry", async () => { - req.body.txids = [ - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - ] - - const result = await uut.getMempoolEntryBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.isString(result.error) - assert.equal(result.error, "Transaction not in mempool") - }) - - // Dev-note: This test passes because it expects an error. TXIDs do not - // stay in the mempool for long, so it does not work well for a unit or - // integration test. - it("should retrieve multiple mempool entries", async () => { - req.body.txids = [ - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde", - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - ] - - const result = await uut.getMempoolEntryBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.isString(result.error) - assert.equal(result.error, "Transaction not in mempool") - }) - } - }) - describe("getMempoolAncestorsSingle()", () => { - it("should throw 400 if txid is empty", async () => { - const result = await uut.getMempoolAncestorsSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.include(result.error, "txid can not be empty") - }) - - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL - - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" - - req.params.txid = - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - - const result = await uut.getMempoolAncestorsSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - req.params.txid = - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - - const result = await uut.getMempoolAncestorsSingle(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - req.params.txid = - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - - const result = await uut.getMempoolAncestorsSingle(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("should GET /getMempoolAncestorsSingle", async () => { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockAncestors } }) - - req.params.txid = - "bb0d349892d351da2767f8c45f6f7949713ff09bd12838d53e76158ddee3ce93" - - const result = await uut.getMempoolAncestorsSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.isArray(result) - }) - }) - describe("getTxOut()", () => { - it("should throw 400 if txid is empty", async () => { - const result = await uut.getTxOut(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.include(result.error, "txid can not be empty") - }) - - it("should throw 400 if n is empty", async () => { - req.params.txid = "sometxid" - const result = await uut.getTxOut(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.include(result.error, "n can not be empty") - }) - - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL - - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" - - req.params.txid = - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - req.params.n = 0 - - const result = await uut.getTxOut(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - req.params.txid = - "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" - req.params.n = 0 - req.query.include_mempool = "true" - - const result = await uut.getTxOut(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - req.params.txid = - "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" - req.params.n = 0 - req.query.include_mempool = "true" - - const result = await uut.getTxOut(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - // This test can only run for unit tests. See TODO at the top of this file. - it("should GET /getTxOut", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockTxOut } }) - } - - req.params.txid = - "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" - req.params.n = 0 - req.query.include_mempool = "true" - - const result = await uut.getTxOut(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.hasAllKeys(result, [ - "bestblock", - "confirmations", - "value", - "scriptPubKey", - "coinbase" - ]) - assert.hasAllKeys(result.scriptPubKey, [ - "asm", - "hex", - "reqSigs", - "type", - "addresses" - ]) - assert.isArray(result.scriptPubKey.addresses) - }) - }) - describe("getTxOutProofSingle()", () => { - it("should throw 400 if txid is empty", async () => { - const result = await uut.getTxOutProofSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.include(result.error, "txid can not be empty") - }) - - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL - - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" - - req.params.txid = - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - - const result = await uut.getTxOutProofSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - req.params.txid = - "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" - req.params.n = 0 - req.query.include_mempool = "true" - - const result = await uut.getTxOutProofSingle(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - req.params.txid = - "197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d" - req.params.n = 0 - req.query.include_mempool = "true" - - const result = await uut.getTxOutProofSingle(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("should GET /getTxOutProof", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockTxOutProof } }) - } - - req.params.txid = - "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" - - const result = await uut.getTxOutProofSingle(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isString(result) - }) - }) - describe("#getTxOutProofBulk", () => { - it("should throw an error for an empty body", async () => { - req.body = {} - - const result = await uut.getTxOutProofBulk(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 = - "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" - - const result = await uut.getTxOutProofBulk(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 400 error if addresses array is too large", async () => { - const testArray = [] - for (var i = 0; i < 25; i++) testArray.push("") - - req.body.txids = testArray - - const result = await uut.getTxOutProofBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.include(result.error, "Array too large") - }) - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - req.body.txids = [ - "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" - ] - - const result = await uut.getTxOutProofBulk(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - req.body.txids = [ - "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" - ] - - const result = await uut.getTxOutProofBulk(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("should GET proof for single txid", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockTxOutProof } }) - } - - req.body.txids = [ - "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" - ] - - const result = await uut.getTxOutProofBulk(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.isString(result[0]) - }) - - it("should GET proof for multiple txids", async () => { - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: mockData.mockTxOutProof } }) - } - - req.body.txids = [ - "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266", - "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" - ] - - const result = await uut.getTxOutProofBulk(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.equal(result.length, 2, "Correct length of returned array") - }) - }) - describe("verifyTxOutProofSingle()", () => { - it("should throw 400 if proof is empty", async () => { - const result = await uut.verifyTxOutProofSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.include(result.error, "proof can not be empty") - }) - - it("should throw 503 when network issues", async () => { - // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL - - // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = "http://fakeurl/api/" - - req.params.proof = mockData.mockTxOutProof - - const result = await uut.verifyTxOutProofSingle(req, res) - // console.log(`result: ${util.inspect(result)}`) - - // Restore the saved URL. - process.env.RPC_BASEURL = savedUrl2 - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - req.params.proof = - "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" - - const result = await uut.verifyTxOutProofSingle(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - req.params.proof = - "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" - - const result = await uut.verifyTxOutProofSingle(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("should GET /verifyTxOutProof", async () => { - const expected = - "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" - - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: [expected] } }) - } - - req.params.proof = - "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" - - const result = await uut.verifyTxOutProofSingle(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.isString(result[0]) - assert.equal(result[0], expected) - }) - }) - describe("#verifyTxOutProofBulk", () => { - it("should throw an error for an empty body", async () => { - req.body = {} - - const result = await uut.verifyTxOutProofBulk(req, res) - - assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") - assert.include( - result.error, - "proofs needs to be an array", - "Proper error message" - ) - }) - - it("should error on non-array single txid", async () => { - req.body.proofs = mockData.mockTxOutProof - - const result = await uut.verifyTxOutProofBulk(req, res) - - assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") - assert.include( - result.error, - "proofs 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.proofs = testArray - - const result = await uut.verifyTxOutProofBulk(req, res) - // console.log(`result: ${util.inspect(result)}`) - - assert.hasAllKeys(result, ["error"]) - assert.include(result.error, "Array too large") - }) - - it("returns proper error when downstream service stalls", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNABORTED" }) - - req.body.proofs = [ - "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" - ] - const result = await uut.verifyTxOutProofBulk(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - it("returns proper error when downstream service is down", async () => { - // Mock the timeout error. - sandbox.stub(uut.axios, "request").throws({ code: "ECONNREFUSED" }) - - req.body.proofs = [ - "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" - ] - const result = await uut.verifyTxOutProofBulk(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isAbove(res.statusCode, 499, "HTTP status code 503 expected.") - assert.include( - result.error, - "Could not communicate with full node", - "Error message expected" - ) - }) - - it("should get single proof", async () => { - const expected = - "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" - - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: [expected] } }) - } - - req.body.proofs = [ - "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" - ] - - const result = await uut.verifyTxOutProofBulk(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.isString(result[0]) - assert.equal(result[0], expected) - }) - - it("should get multiple proofs", async () => { - const expected = - "2c7ae9f865f7ce0c33c189b2f83414176903ce4b06ed9f8b7bcf55efbd4a7266" - - // Mock the RPC call for unit tests. - if (process.env.TEST === "unit") { - sandbox - .stub(uut.axios, "request") - .resolves({ data: { result: [expected] } }) - } - - req.body.proofs = [ - "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700", - "000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700" - ] - - const result = await uut.verifyTxOutProofBulk(req, res) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.isString(result[0]) - assert.equal(result[0], expected) - assert.equal(result.length, 2) - }) - }) -})