diff --git a/.travis.yml b/.travis.yml index 551328c..3415e75 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,10 @@ sudo: required services: - docker + - redis-server + +#before_install: +# - ./install-redis.sh script: "npm run test" 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." 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..56db9e1 100644 --- a/docker/mainnet/docker-compose.yml +++ b/docker/mainnet/docker-compose.yml @@ -1,10 +1,17 @@ -bch-api: +bch-api-main: build: . dockerfile: Dockerfile #image: bitcore container_name: bch-api-main - #links: - # - mongodb + links: + - redis-main ports: - "12400:3000" restart: always + +redis-main: + image: redis:latest + container_name: redis-main + restart: always + ports: + - "6379:6379" 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/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 diff --git a/package-lock.json b/package-lock.json index c00d725..dd3088f 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", @@ -2256,6 +2288,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 +3078,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", @@ -3231,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", @@ -5690,6 +5740,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", @@ -6244,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", @@ -6271,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", @@ -6282,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", @@ -6473,12 +6593,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", @@ -6490,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", @@ -12165,6 +12318,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": "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", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", @@ -12332,6 +12490,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 +13804,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..9aa4a60 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,9 @@ "express-basic-auth": "^1.1.3", "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", @@ -44,6 +47,7 @@ "passport-http": "^0.3.0", "pg": "^7.11.0", "pg-hstore": "^2.3.2", + "rate-limiter-flexible": "^2.0.0", "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..3e7fad5 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") @@ -22,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/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") @@ -35,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 @@ -78,11 +83,15 @@ 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}/`, rateLimits.routeRateLimit) +app.use(`/${v3prefix}/`, rateLimits.rateLimitByResource) + 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..1d71a6c 100644 --- a/src/middleware/route-ratelimit.js +++ b/src/middleware/route-ratelimit.js @@ -1,26 +1,39 @@ -/* - 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") 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") + +// 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") +const rateLimitOptions = { + storeClient: redisClient, + points: 100, // Number of points + 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) @@ -32,169 +45,448 @@ 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 + + this.jwt = jwt + this.rateLimiter = new RateLimiterRedis(rateLimitOptions) } - // 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}`) + // Used to disconnect from the Redis DB. + // Called by unit tests so that node.js thread doesn't live forever. + closeRedis() { + redisClient.disconnect() + } - // URL for the auth server. - const path = `${authServer}apitoken/isvalid/${req.locals.jwtToken}` + // 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. - // 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 + 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() + + // Create a res.locals object if not passed in. + if (!req.locals) { + req.locals = { + // default values + jwtToken: "", + proLimit: false, + apiLevel: 0 + } + } + + // 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}`) + + // 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 = _this.evalUserPermissioins(req, jwtInfo) + // console.log( + // `userPermissions: ${JSON.stringify(userPermissions, null, 2)}` + // ) + + req.locals.proLimit = userPermissions.proLimit + req.locals.apiLevel = userPermissions.apiLevel + } + } + } + + // 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) + } + + // 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. + 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 + } + + // 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 + let decoded = {} + + // Create a res.locals object if not passed in. + if (!req.locals) { + req.locals = { + // default values + jwtToken: "", + proLimit: false, + apiLevel: 0 + } + } + + // 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 { + // The resource being consumed: full node, indexer, SLPDB, etc. + const resource = _this.getResource(req.url) + wlogger.debug(`resource: ${resource}`) + + let key = userId ? userId : req.ip + + // const pointsToConsume = userId ? 1 : 30 + decoded.resource = resource + const pointsToConsume = _this.calcPoints(decoded) + + wlogger.info( + `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) + + // 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/newRateLimit(): `, err) + // throw err + } + + 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)}`) - // 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)}` - // ) + const apiLevel = jwtInfo.apiLevel + const resource = jwtInfo.resource - req.locals.proLimit = userPermissions.proLimit - req.locals.apiLevel = userPermissions.apiLevel + const level30Routes = ["insight", "bitcore", "blockbook"] + const level40Routes = ["slp"] + + wlogger.debug(`apiLevel: ${apiLevel}`) + + // Only evaluate if user is using a JWT token. + if (jwtInfo.id) { + // SLP indexer routes + if (level40Routes.includes(resource)) { + if (apiLevel >= 40) retVal = 1 + // else if (apiLevel >= 10) retVal = 10 + else retVal = 10 + } + + // Normal indexer routes + else if (level30Routes.includes(resource)) { + if (apiLevel >= 30) retVal = 1 + else retVal = 10 + } + + // Full node tier + else if (apiLevel >= 20) { + retVal = 1 + } + + // Free tier, full node only. + else { + retVal = 10 + } } + + return retVal + } catch (err) { + wlogger.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 { + wlogger.debug(`url: ${JSON.stringify(url, null, 2)}`) + + const splitUrl = url.split("/") + const resource = splitUrl[1] + + return resource + } catch (err) { + wlogger.error(`Error in getResource().`) + throw err } } - // Current route - const rateLimitTier = req.locals.proLimit ? "PRO" : "BASIC" - const path = req.baseUrl + req.path + // 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 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` - }) + // 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 } - // 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` - }) - } - }) - } + next() } - //console.log(`calling uniqueRateLimits() on this route: ${route}`) + // Calculates the points consumed, based on the explicit rateLimit defined + // in the JWT token. + calcPoints2(jwtInfo) { + let retVal = 30 // By default, use anonymous tier. - // Call rate limit for this route - uniqueRateLimits[route](req, res, next) + 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 + } } -// This function returns the 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 - } - } - } - - return retObj -} - -module.exports = { routeRateLimit } +module.exports = RateLimits 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 28db016..0000000 --- a/src/routes/v3/full-node/blockchain2.js +++ /dev/null @@ -1,133 +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) - } - - 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 curl Example: - * curl -X GET "https://api.fullstack.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://api.fullstack.cash/v3/blockchain/getBlockchainInfo" -H "accept: application/json" - * - * @apiSuccess {Object} object Object containing data - * @apiSuccess {String} object.chain "main" - * @apiSuccess {Number} object.blocks 561838 - * @apiSuccess {Number} object.headers 561838 - * @apiSuccess {String} object.bestblockhash "000000000000000002307dd38cd01c7308b8febfcdf5772cf087b5bb023d55bc" - * @apiSuccess {Number} object.difficulty 246585566638.1496 - * @apiSuccess {String} object.mediantime 1545402693 - * @apiSuccess {Number} object.verificationprogress 0.999998831622689 - * @apiSuccess {Boolean} object.chainwork "000000000000000000000000000000000000000000d8c09a8ab7262080266b3e" - * @apiSuccess {Number} object.pruned false - * @apiSuccess {Array} object.softforks Array of objects - * @apiSuccess {String} object.softforks.id "bip34" - * @apiSuccess {String} object.softforks.version 2 - * @apiSuccess {Object} object.softforks.reject - * @apiSuccess {String} object.softforks.reject.status true - */ - async getBlockchainInfo(req, res, next) { - try { - // Axios options - const options = this.routeUtils.getAxiosOptions() - options.data.id = "getblockchaininfo" - options.data.method = "getblockchaininfo" - options.data.params = [] - - const response = await this.axios.request(options) - - return res.json(response.data.result) - } catch (err) { - // Write out error to error log. - wlogger.error(`Error in blockchain.ts/getBlockchainInfo().`, err) - - return this.errorHandler(err, res) - } - } -} - -module.exports = Blockchain 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 debaa2d..0000000 --- a/test/v3/blockchain2.js +++ /dev/null @@ -1,1271 +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 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() - -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 = {} - - // 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() - - 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()", () => { - // 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 - - // 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()", () => { - // 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 - - // 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") { - nock(`${process.env.RPC_BASEURL}`) - .post(uri => uri.includes("/")) - .reply(200, { 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()", () => { - // // 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) - // }) - // }) -}) 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 50d7ca6..2b832a1 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") @@ -12,7 +13,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") +let rateLimits = new RateLimits() +let rateLimitMiddleware = rateLimits.routeRateLimit + const controlRoute = require("../../src/routes/v3/full-node/control") const jwtAuth = require("../../src/middleware/jwt-auth") @@ -23,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 = { @@ -47,6 +53,17 @@ describe("#route-ratelimits & jwt-auth", () => { req.params = {} req.body = {} req.query = {} + req.locals = {} + + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + after(() => { + rateLimits.closeRedis() }) describe("#jwt-auth.js", () => { @@ -71,83 +88,10 @@ 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", () => { + rateLimitMiddleware = new RateLimits() let routeRateLimit = rateLimitMiddleware.routeRateLimit const getInfo = controlRoute.testableComponents.getInfo @@ -186,10 +130,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" @@ -216,7 +161,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" @@ -241,10 +186,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" @@ -273,10 +219,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" @@ -303,6 +250,546 @@ 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") + }) + }) + + 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 10 points for free tier requesting full node access", () => { + const jwtInfo = { + apiLevel: 10, + resource: "blockchain", + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 10) + }) + + 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 full node 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 10 points for full-node tier requesting indexer access", () => { + const jwtInfo = { + apiLevel: 20, + resource: "blockbook", + id: "5e3a0415eb29a962da2708b4" + } + + const result = rateLimits.calcPoints(jwtInfo) + assert.equal(result, 10) + }) + + it("should return 10 points for full node 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 indexer 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 indexer tier requesting indexer access", () => { + const jwtInfo = { + apiLevel: 30, + 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: 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) + }) + }) + + 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" + req.path = "/control/getNetworkInfo" + req.url = req.path + req.method = "GET" + + // Call the route twice to trigger the rate handling. + 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) + }) + + 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.rateLimitByResource(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: "5e3a0415eb29a962da2708b1" + } + + // 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.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 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: "5e3a0415eb29a962da2708b2" + } + + // 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.rateLimitByResource(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 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.` + ) + }) + }) + + 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.