mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-22 09:12:05 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17fe219773 | ||
|
|
d76c663f25 | ||
|
|
b1655e843e | ||
|
|
af5d44c278 | ||
|
|
0d9caca8b0 | ||
|
|
0cdb9572f1 | ||
|
|
8507abd5db | ||
|
|
fae9a75f6a | ||
|
|
fc6da588ee | ||
|
|
6f8c00bab9 | ||
|
|
a2ab88adab | ||
|
|
60528dbd6e | ||
|
|
cebf1ea1d6 | ||
|
|
11a62af39b | ||
|
|
83aeabd913 | ||
|
|
7acb778470 | ||
|
|
766925c9a8 | ||
|
|
51c37e1371 | ||
|
|
244acfb26b | ||
|
|
d03303728c | ||
|
|
9fa16aea78 | ||
|
|
29864bcf66 | ||
|
|
03a5d8076e | ||
|
|
9d8f214098 | ||
|
|
4b68a75a41 | ||
|
|
901189ae42 | ||
|
|
62bba1d79f | ||
|
|
9c735e3c5a | ||
|
|
62600fdd62 | ||
|
|
27e5fb8728 | ||
|
|
93811ca331 | ||
|
|
e37858fd19 | ||
|
|
3ddbb728b4 | ||
|
|
ef49b85495 | ||
|
|
be6550686f | ||
|
|
8de2e9e10b | ||
|
|
5650afb22f |
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"sampleUrl": null
|
||||||
|
|
||||||
|
}
|
||||||
Generated
+25
-22
@@ -395,9 +395,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@psf/bch-js": {
|
"@psf/bch-js": {
|
||||||
"version": "4.16.1",
|
"version": "4.17.6",
|
||||||
"resolved": "https://registry.npmjs.org/@psf/bch-js/-/bch-js-4.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/@psf/bch-js/-/bch-js-4.17.6.tgz",
|
||||||
"integrity": "sha512-0rAOLGwxuzCrpe6dbz5nQ9KQTJIJNGzihSVSVfv0PXIeBKQKq+MuEG7u6rZXNeJlLWokosGwf6aysK2EbDGcqA==",
|
"integrity": "sha512-dXR3S7wHZEivB/1LvvuBaAxn8FRRQpBrLtLcuL8c1J9v82xMO+qVr6AwvsOjFzHmcswuT1jlzQ3X1XPAaRgb5w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@psf/bip21": "^2.0.1",
|
"@psf/bip21": "^2.0.1",
|
||||||
"@psf/bip32-utils": "^1.0.0",
|
"@psf/bip32-utils": "^1.0.0",
|
||||||
@@ -931,23 +931,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@uppy/companion-client": {
|
"@uppy/companion-client": {
|
||||||
"version": "1.8.2",
|
"version": "1.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-1.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-1.8.3.tgz",
|
||||||
"integrity": "sha512-FBjAJU3xaWRqYXDBlrMDQKlRBqi4Ng54Dmnoe3hGSggfgBQAl9RzRAg0WvrGjaK3QjCTZPhzSdTpBrqDP22Rng==",
|
"integrity": "sha512-IieE5DMmJC1rJrXHB2u1cO+r3BF7VqnezFAZj8gkNm7zBuplswKALlvwnXI+KU3TNs8isU55xK5yfF0ecglIEw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@uppy/utils": "^3.4.1",
|
"@uppy/utils": "^3.4.2",
|
||||||
"namespace-emitter": "^2.0.1",
|
"namespace-emitter": "^2.0.1",
|
||||||
"qs-stringify": "^1.1.0"
|
"qs-stringify": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@uppy/core": {
|
"@uppy/core": {
|
||||||
"version": "1.16.1",
|
"version": "1.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@uppy/core/-/core-1.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/@uppy/core/-/core-1.16.2.tgz",
|
||||||
"integrity": "sha512-GI8baCa1S21j7KoCq0LK4MLD6sII8OvWVgp+auXNcZIbKa182dOMQRtk7bXVY0OzS1TxkIq0DAzDEUnP7MbnDg==",
|
"integrity": "sha512-JWK03gXiMMZ+lSVj+v9FIFEOUbKyvRX4v2kaYhf0NqD1MssCwMQe4mvL2MNzjOaYFBwMRacDJh8Mvz2gbu2Vqw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@transloadit/prettier-bytes": "0.0.7",
|
"@transloadit/prettier-bytes": "0.0.7",
|
||||||
"@uppy/store-default": "^1.2.5",
|
"@uppy/store-default": "^1.2.5",
|
||||||
"@uppy/utils": "^3.4.1",
|
"@uppy/utils": "^3.4.2",
|
||||||
"cuid": "^2.1.1",
|
"cuid": "^2.1.1",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"mime-match": "^1.0.2",
|
"mime-match": "^1.0.2",
|
||||||
@@ -961,19 +961,19 @@
|
|||||||
"integrity": "sha512-jnf0U8cfb8Bhgt6yh86YRJO9EEnCyG9BgXZ8dPWWLybgC9Expw3Ah/s3T21tcdChgv4zzdhSACd0JKxCQowyYg=="
|
"integrity": "sha512-jnf0U8cfb8Bhgt6yh86YRJO9EEnCyG9BgXZ8dPWWLybgC9Expw3Ah/s3T21tcdChgv4zzdhSACd0JKxCQowyYg=="
|
||||||
},
|
},
|
||||||
"@uppy/tus": {
|
"@uppy/tus": {
|
||||||
"version": "1.8.5",
|
"version": "1.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/@uppy/tus/-/tus-1.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/@uppy/tus/-/tus-1.8.6.tgz",
|
||||||
"integrity": "sha512-a5z+bpAyKKKRWNSOYtIMd2EAKsM7BLhd6hQg2YrQNFFzdwbxaeqmrjKIVW1EUBvg1if/ZZePEm0QevJvNwQs0g==",
|
"integrity": "sha512-axK+TzRRvSkYZUIjfe6XCjfO5NPb/k8AC73p+MOKOzth1JHSZSJHqYi8+HY7T4aXgR86vWE/cE+wHsjvPhyCZw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@uppy/companion-client": "^1.8.2",
|
"@uppy/companion-client": "^1.8.3",
|
||||||
"@uppy/utils": "^3.4.1",
|
"@uppy/utils": "^3.4.2",
|
||||||
"tus-js-client": "^2.1.1"
|
"tus-js-client": "^2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@uppy/utils": {
|
"@uppy/utils": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-3.4.2.tgz",
|
||||||
"integrity": "sha512-9YVKjVRK/AmX3yV0GJsD5p/eHUzqn4+eXzsjv3wHahipKCo0o7Jwpw/7kl+bv12EHOOy5jPsATWkjn40aSWW8w==",
|
"integrity": "sha512-PWLZgX4L+0tdBqImncuATDYb1M6RHgDfGDZCh2Qg+PTG+btyyZ4/GkOEwK5fJ4Gw1gNgWF0ewAFL28+oCrD7/Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"abortcontroller-polyfill": "^1.4.0",
|
"abortcontroller-polyfill": "^1.4.0",
|
||||||
"lodash.throttle": "^4.1.1"
|
"lodash.throttle": "^4.1.1"
|
||||||
@@ -10930,9 +10930,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.9.6",
|
"version": "6.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz",
|
||||||
"integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ=="
|
"integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==",
|
||||||
|
"requires": {
|
||||||
|
"side-channel": "^1.0.4"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"qs-stringify": {
|
"qs-stringify": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@
|
|||||||
"node": ">=10.15.1"
|
"node": ">=10.15.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@psf/bch-js": "^4.16.1",
|
"@psf/bch-js": "^4.17.6",
|
||||||
"apidoc": "^0.26.0",
|
"apidoc": "^0.26.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"bitcore-lib-cash": "^8.23.1",
|
"bitcore-lib-cash": "^8.23.1",
|
||||||
|
|||||||
+5
-3
@@ -189,9 +189,11 @@ server.listen(port)
|
|||||||
server.on('error', onError)
|
server.on('error', onError)
|
||||||
server.on('listening', onListening)
|
server.on('listening', onListening)
|
||||||
|
|
||||||
// Set the time before a timeout error is generated. This impacts testing and
|
// Set the time before a timeout error is generated.
|
||||||
// the handling of timeout errors. Is 10 seconds too agressive?
|
// 10 seconds is way too agressive. 30 Seconds was used for a while, but with
|
||||||
server.setTimeout(30 * 1000)
|
// being able to set a timeout between UTXOs for tokenUtxoDetails, the timeout
|
||||||
|
// needed to be extended.
|
||||||
|
server.setTimeout(1000 * 60 * 5) // 5 minutes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize a port into a number, string, or false.
|
* Normalize a port into a number, string, or false.
|
||||||
|
|||||||
@@ -82,11 +82,11 @@ class RateLimits {
|
|||||||
|
|
||||||
// Determine if the call is an external or internal API call.
|
// Determine if the call is an external or internal API call.
|
||||||
const isInternal = _this.checkInternalIp(req)
|
const isInternal = _this.checkInternalIp(req)
|
||||||
console.log(`isInternal: ${isInternal}`)
|
// console.log(`isInternal: ${isInternal}`)
|
||||||
|
|
||||||
// Determine if the call originates from another computer on the intranet.
|
// Determine if the call originates from another computer on the intranet.
|
||||||
const isWhitelistOrigin = _this.isInWhitelist(req)
|
const isWhitelistOrigin = _this.isInWhitelist(req)
|
||||||
console.log('isWhitelistOrigin: ', isWhitelistOrigin)
|
// console.log('isWhitelistOrigin: ', isWhitelistOrigin)
|
||||||
|
|
||||||
// Handle the use case of internally-generated requests.
|
// Handle the use case of internally-generated requests.
|
||||||
if (isInternal) {
|
if (isInternal) {
|
||||||
@@ -94,15 +94,15 @@ class RateLimits {
|
|||||||
// the usrObj in the body.
|
// the usrObj in the body.
|
||||||
if (req.body && req.body.usrObj) {
|
if (req.body && req.body.usrObj) {
|
||||||
if (req.body.usrObj.proLimit) {
|
if (req.body.usrObj.proLimit) {
|
||||||
console.log('Internal call, basic auth, skipping rate limits.')
|
// console.log('Internal call, basic auth, skipping rate limits.')
|
||||||
|
|
||||||
// If this is an internal call that originated from a user using
|
// If this is an internal call that originated from a user using
|
||||||
// Basic Authentication, then skip rate-limits.
|
// Basic Authentication, then skip rate-limits.
|
||||||
return next()
|
return next()
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
// console.log(
|
||||||
'Internal call, applying rate limits. Using JWT if available.'
|
// 'Internal call, applying rate limits. Using JWT if available.'
|
||||||
)
|
// )
|
||||||
|
|
||||||
// Determine if user has exceeded their rate limits. Pass in the
|
// Determine if user has exceeded their rate limits. Pass in the
|
||||||
// JWT token if one exists.
|
// JWT token if one exists.
|
||||||
@@ -130,10 +130,10 @@ class RateLimits {
|
|||||||
// and avoid this code path. This code path is 'pooled': all users
|
// and avoid this code path. This code path is 'pooled': all users
|
||||||
// share the same rate limits. Even at 1000 RPM, this pool will get
|
// share the same rate limits. Even at 1000 RPM, this pool will get
|
||||||
// exhausted easily.
|
// exhausted easily.
|
||||||
const warnMsg =
|
// const warnMsg =
|
||||||
'Internal call. req.body.usrObj does not exist. Applying high-speed internal rate limits.'
|
// 'Internal call. req.body.usrObj does not exist. Applying high-speed internal rate limits.'
|
||||||
console.log(warnMsg)
|
// console.log(warnMsg)
|
||||||
wlogger.info(warnMsg)
|
// wlogger.info(warnMsg)
|
||||||
|
|
||||||
const defaultPayload = {
|
const defaultPayload = {
|
||||||
id: '98.76.54.32',
|
id: '98.76.54.32',
|
||||||
@@ -168,9 +168,9 @@ class RateLimits {
|
|||||||
//
|
//
|
||||||
} else {
|
} else {
|
||||||
// Handle the normal use-case of external requests
|
// Handle the normal use-case of external requests
|
||||||
console.log(
|
// console.log(
|
||||||
'External call, applying rate limits. Using JWT if available.'
|
// 'External call, applying rate limits. Using JWT if available.'
|
||||||
)
|
// )
|
||||||
|
|
||||||
// For calls originating from a whitelist domain, apply a high-RPM
|
// For calls originating from a whitelist domain, apply a high-RPM
|
||||||
// JWT token to the call.
|
// JWT token to the call.
|
||||||
@@ -233,7 +233,7 @@ class RateLimits {
|
|||||||
|
|
||||||
pointsToConsume = decoded.pointsToConsume
|
pointsToConsume = decoded.pointsToConsume
|
||||||
}
|
}
|
||||||
console.log(`rate limit key: ${key}`)
|
// console.log(`rate limit key: ${key}`)
|
||||||
|
|
||||||
// This function will throw an error if the user exceeds the rate limit.
|
// This function will throw an error if the user exceeds the rate limit.
|
||||||
// The 429 error response is handled by the catch().
|
// The 429 error response is handled by the catch().
|
||||||
@@ -244,7 +244,7 @@ class RateLimits {
|
|||||||
// Signal that the user has not exceeded their rate limits.
|
// Signal that the user has not exceeded their rate limits.
|
||||||
return false
|
return false
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('err: ', err)
|
// console.log('err: ', err)
|
||||||
|
|
||||||
const rateLimit = Math.floor(POINTS_PER_MINUTE / pointsToConsume)
|
const rateLimit = Math.floor(POINTS_PER_MINUTE / pointsToConsume)
|
||||||
|
|
||||||
@@ -307,7 +307,8 @@ class RateLimits {
|
|||||||
|
|
||||||
// Retrieve the origin.
|
// Retrieve the origin.
|
||||||
const origin = req.get('origin')
|
const origin = req.get('origin')
|
||||||
console.log(`origin: ${origin}`)
|
|
||||||
|
if (!process.env.TEST) console.log('origin:', origin)
|
||||||
|
|
||||||
// If the origin is not determinable, return false.
|
// If the origin is not determinable, return false.
|
||||||
if (!origin) return false
|
if (!origin) return false
|
||||||
|
|||||||
@@ -1,521 +0,0 @@
|
|||||||
/*
|
|
||||||
Blockbook API route
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const axios = require('axios')
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
|
||||||
|
|
||||||
const RouteUtils = require('../../util/route-utils')
|
|
||||||
const routeUtils = new RouteUtils()
|
|
||||||
|
|
||||||
// Library for easily switching the API paths to use different instances of
|
|
||||||
// Blockbook.
|
|
||||||
const BlockbookPath = require('../../util/blockbook-path')
|
|
||||||
const BLOCKBOOKPATH = new BlockbookPath()
|
|
||||||
// BLOCKBOOKPATH.toOpenBazaar()
|
|
||||||
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
// Used for processing error messages before sending them to the user.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
const BCHJS = require('@psf/bch-js')
|
|
||||||
const bchjs = new BCHJS()
|
|
||||||
|
|
||||||
let _this
|
|
||||||
|
|
||||||
class Blockbook {
|
|
||||||
constructor () {
|
|
||||||
_this = this
|
|
||||||
|
|
||||||
_this.axios = axios
|
|
||||||
_this.routeUtils = routeUtils
|
|
||||||
_this.bchjs = bchjs
|
|
||||||
_this.BLOCKBOOKPATH = BLOCKBOOKPATH
|
|
||||||
|
|
||||||
_this.router = router
|
|
||||||
_this.router.get('/', _this.root)
|
|
||||||
_this.router.get('/balance/:address', _this.balanceSingle)
|
|
||||||
_this.router.post('/balance', _this.balanceBulk)
|
|
||||||
_this.router.get('/utxos/:address', _this.utxosSingle)
|
|
||||||
_this.router.post('/utxos', _this.utxosBulk)
|
|
||||||
_this.router.get('/tx/:txid', _this.txSingle)
|
|
||||||
_this.router.post('/tx', _this.txBulk)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root API endpoint. Simply acknowledges that it exists.
|
|
||||||
root (req, res, next) {
|
|
||||||
return res.json({ status: 'address' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook Node API for a balance on a single BCH address.
|
|
||||||
// Returns a Promise.
|
|
||||||
async balanceFromBlockbook (thisAddress) {
|
|
||||||
try {
|
|
||||||
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
|
|
||||||
|
|
||||||
// Convert the address to a cashaddr without a prefix.
|
|
||||||
const addr = _this.bchjs.Address.toCashAddress(thisAddress)
|
|
||||||
|
|
||||||
const path = `${_this.BLOCKBOOKPATH.addrPath}${addr}`
|
|
||||||
// console.log(`path: ${path}`)
|
|
||||||
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const options = {
|
|
||||||
method: 'get',
|
|
||||||
baseURL: path
|
|
||||||
}
|
|
||||||
|
|
||||||
const axiosResponse = await _this.axios.request(options)
|
|
||||||
const retData = axiosResponse.data
|
|
||||||
// console.log(`retData: ${util.inspect(retData)}`)
|
|
||||||
|
|
||||||
return retData
|
|
||||||
} catch (err) {
|
|
||||||
// Dev Note: Do not log error messages here. Throw them instead and let the
|
|
||||||
// parent function handle it.
|
|
||||||
wlogger.debug('Error in blockbook.js/balanceFromBlockbook()')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async balanceSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const address = req.params.address
|
|
||||||
|
|
||||||
if (!address || address === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'address can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject if address is an array.
|
|
||||||
if (Array.isArray(address)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'address can not be an array. Use POST for bulk upload.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockbook/balanceSingle with this address: ',
|
|
||||||
address
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
// const legacyAddr = bchjs.Address.toLegacyAddress(address)
|
|
||||||
_this.bchjs.Address.toLegacyAddress(address)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${address}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = _this.routeUtils.validateNetwork(address)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error:
|
|
||||||
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const retData = await _this.balanceFromBlockbook(address)
|
|
||||||
|
|
||||||
// Return the retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(retData)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in blockbook.js/balanceSingle().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST handler for bulk queries on address details
|
|
||||||
async balanceBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
let addresses = req.body.addresses
|
|
||||||
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
|
|
||||||
|
|
||||||
// Reject if addresses is not an array.
|
|
||||||
if (!Array.isArray(addresses)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'addresses needs to be an array. Use GET for single address.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
|
||||||
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockbook.js/balanceBulk with these addresses: ',
|
|
||||||
addresses
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate each element in the address array.
|
|
||||||
for (let i = 0; i < addresses.length; i++) {
|
|
||||||
const thisAddress = addresses[i]
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
_this.bchjs.Address.toLegacyAddress(thisAddress)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${thisAddress}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = _this.routeUtils.validateNetwork(thisAddress)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loops through each address and creates an array of Promises, querying
|
|
||||||
// Insight API in parallel.
|
|
||||||
addresses = addresses.map(async (address, index) =>
|
|
||||||
// console.log(`address: ${address}`)
|
|
||||||
_this.balanceFromBlockbook(address)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for all parallel Insight requests to return.
|
|
||||||
const result = await _this.axios.all(addresses)
|
|
||||||
|
|
||||||
// Return the array of retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in blockbook.js/balanceBulk().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook API for utxos associated with a BCH address.
|
|
||||||
// Returns a Promise.
|
|
||||||
async utxosFromBlockbook (thisAddress) {
|
|
||||||
try {
|
|
||||||
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
|
|
||||||
|
|
||||||
// Convert the address to a cashaddr without a prefix.
|
|
||||||
const addr = _this.bchjs.Address.toCashAddress(thisAddress)
|
|
||||||
|
|
||||||
const path = `${_this.BLOCKBOOKPATH.utxoPath}${addr}`
|
|
||||||
// console.log(`path: ${path}`)
|
|
||||||
|
|
||||||
// Query the Blockbook API.
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const options = {
|
|
||||||
method: 'get',
|
|
||||||
baseURL: path
|
|
||||||
}
|
|
||||||
const axiosResponse = await _this.axios.request(options)
|
|
||||||
const retData = axiosResponse.data
|
|
||||||
// console.log(`retData: ${util.inspect(retData)}`)
|
|
||||||
|
|
||||||
// Add the satoshis property to each UTXO.
|
|
||||||
for (let i = 0; i < retData.length; i++) {
|
|
||||||
retData[i].satoshis = Number(retData[i].value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return retData
|
|
||||||
} catch (err) {
|
|
||||||
// Dev Note: Do not log error messages here. Throw them instead and let the
|
|
||||||
// parent function handle it.
|
|
||||||
wlogger.debug('Error in blockbook.js/utxosFromBlockbook()')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET handler for single balance
|
|
||||||
async utxosSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const address = req.params.address
|
|
||||||
|
|
||||||
if (!address || address === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'address can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject if address is an array.
|
|
||||||
if (Array.isArray(address)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'address can not be an array. Use POST for bulk upload.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockbook/utxosSingle with this address: ',
|
|
||||||
address
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
// const legacyAddr = bchjs.Address.toLegacyAddress(address)
|
|
||||||
_this.bchjs.Address.toLegacyAddress(address)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${address}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = _this.routeUtils.validateNetwork(address)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error:
|
|
||||||
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook API.
|
|
||||||
const retData = await _this.utxosFromBlockbook(address)
|
|
||||||
|
|
||||||
// Return the retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(retData)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in blockbook.js/utxosSingle().', err)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST handler for bulk queries on address utxos
|
|
||||||
async utxosBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
let addresses = req.body.addresses
|
|
||||||
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
|
|
||||||
|
|
||||||
// Reject if addresses is not an array.
|
|
||||||
if (!Array.isArray(addresses)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'addresses needs to be an array. Use GET for single address.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
|
||||||
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockbook.js/utxosBulk with these addresses: ',
|
|
||||||
addresses
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate each element in the address array.
|
|
||||||
for (let i = 0; i < addresses.length; i++) {
|
|
||||||
const thisAddress = addresses[i]
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
_this.bchjs.Address.toLegacyAddress(thisAddress)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${thisAddress}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = _this.routeUtils.validateNetwork(thisAddress)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loops through each address and creates an array of Promises, querying
|
|
||||||
// Insight API in parallel.
|
|
||||||
addresses = addresses.map(async (address, index) =>
|
|
||||||
// console.log(`address: ${address}`)
|
|
||||||
_this.utxosFromBlockbook(address)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for all parallel Insight requests to return.
|
|
||||||
const result = await _this.axios.all(addresses)
|
|
||||||
|
|
||||||
// Return the array of retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in blockbook.js/utxosBulk().', err)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook Node API for transactions on a single TXID.
|
|
||||||
// Returns a Promise.
|
|
||||||
async transactionsFromBlockbook (txid) {
|
|
||||||
try {
|
|
||||||
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
|
|
||||||
|
|
||||||
const path = `${_this.BLOCKBOOKPATH.txPath}${txid}`
|
|
||||||
// console.log(`path: ${path}`)
|
|
||||||
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const options = {
|
|
||||||
method: 'get',
|
|
||||||
baseURL: path
|
|
||||||
}
|
|
||||||
const axiosResponse = await _this.axios.request(options)
|
|
||||||
const retPromise = axiosResponse.data
|
|
||||||
// console.log(`retData: ${util.inspect(retData)}`)
|
|
||||||
|
|
||||||
return retPromise
|
|
||||||
} catch (err) {
|
|
||||||
// Dev Note: Do not log error messages here. Throw them instead and let the
|
|
||||||
// parent function handle it.
|
|
||||||
wlogger.debug('Error in blockbook.js/transactionsFromBlockbook()')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET handler for single transaction details.
|
|
||||||
async txSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const txid = req.params.txid
|
|
||||||
|
|
||||||
if (!txid || txid === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'txid can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject if address is an array.
|
|
||||||
if (Array.isArray(txid)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'txid can not be an array. Use POST for bulk upload.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add regex comparison of txid to ensure it's valid.
|
|
||||||
if (txid.length !== 64) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `txid must be of length 64 (not ${txid.length})`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug('Executing blockbook/txSingle with this txid: ', txid)
|
|
||||||
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const retData = await _this.transactionsFromBlockbook(txid)
|
|
||||||
|
|
||||||
// Return the retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(retData)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in blockbook.js/txSingle().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST handler for bulk queries on tx details
|
|
||||||
async txBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
let txids = req.body.txids
|
|
||||||
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
|
|
||||||
|
|
||||||
// Reject if txids is not an array.
|
|
||||||
if (!Array.isArray(txids)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'txids need to be an array. Use GET for single address.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
|
||||||
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug('Executing blockbook.js/txBulk with these txids: ', txids)
|
|
||||||
|
|
||||||
// Validate each element in the txids array.
|
|
||||||
for (let i = 0; i < txids.length; i++) {
|
|
||||||
const thisTxid = txids[i]
|
|
||||||
|
|
||||||
if (!thisTxid || thisTxid === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'txid can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add regex comparison of txid to ensure it's valid.
|
|
||||||
if (thisTxid.length !== 64) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `txid must be of length 64 (not ${thisTxid.length})`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loops through each address and creates an array of Promises, querying
|
|
||||||
// Insight API in parallel.
|
|
||||||
txids = txids.map(async (txid, index) =>
|
|
||||||
// console.log(`address: ${address}`)
|
|
||||||
_this.transactionsFromBlockbook(txid)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for all parallel Insight requests to return.
|
|
||||||
const result = await _this.axios.all(txids)
|
|
||||||
|
|
||||||
// Return the array of retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in blockbook.js/txBulk().', err)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Blockbook
|
|
||||||
@@ -181,7 +181,7 @@ class Encryption {
|
|||||||
publicKey: 'not found'
|
publicKey: 'not found'
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Error in encryption.js/getPublicKey().', err)
|
// console.log('Error in encryption.js/getPublicKey().', err)
|
||||||
wlogger.error('Error in encryption.js/getPublicKey().', err)
|
wlogger.error('Error in encryption.js/getPublicKey().', err)
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
return _this.errorHandler(err, res)
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class Blockchain {
|
|||||||
this.router.post('/getTxOutProof', this.getTxOutProofBulk)
|
this.router.post('/getTxOutProof', this.getTxOutProofBulk)
|
||||||
this.router.get('/verifyTxOutProof/:proof', this.verifyTxOutProofSingle)
|
this.router.get('/verifyTxOutProof/:proof', this.verifyTxOutProofSingle)
|
||||||
this.router.post('/verifyTxOutProof', this.verifyTxOutProofBulk)
|
this.router.post('/verifyTxOutProof', this.verifyTxOutProofBulk)
|
||||||
|
this.router.post('/getBlock', this.getBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
root (req, res, next) {
|
root (req, res, next) {
|
||||||
@@ -937,6 +938,52 @@ class Blockchain {
|
|||||||
return _this.errorHandler(err, res)
|
return _this.errorHandler(err, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /blockchain/getBlock/ Get block details
|
||||||
|
* @apiName getBlock
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns block details
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl "https://api.fullstack.cash/v4/blockchain/getblock/" -X POST -H "Content-Type: application/json" --data-binary '{"blockhash":"000000000000000002a5fe0bdd6e3f04342a975c0f55e57f97e73bb90041676b","verbosity":0 }'
|
||||||
|
*
|
||||||
|
* @apiParam {String} blockhash Block hash (required)
|
||||||
|
* @apiParam {Number} verbosity Default 1 (optional)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async getBlock (req, res, next) {
|
||||||
|
try {
|
||||||
|
// Validate input parameter
|
||||||
|
const blockhash = req.body.blockhash
|
||||||
|
let verbosity = req.body.verbosity
|
||||||
|
|
||||||
|
// Default to a value of 1 if another verbosity level is not defined.
|
||||||
|
if (!verbosity && verbosity !== 0) verbosity = 1
|
||||||
|
|
||||||
|
if (!blockhash || blockhash === '') {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({ error: 'blockhash can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Axios options
|
||||||
|
const options = _this.routeUtils.getAxiosOptions()
|
||||||
|
|
||||||
|
options.data.id = 'getblock'
|
||||||
|
options.data.method = 'getblock'
|
||||||
|
options.data.params = [blockhash, verbosity]
|
||||||
|
|
||||||
|
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.js/getBlock()', err)
|
||||||
|
|
||||||
|
return _this.errorHandler(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Blockchain
|
module.exports = Blockchain
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
/*
|
|
||||||
A private library of utility functions used by several different routes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const axios = require('axios')
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
|
||||||
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
const BCHJS = require('@psf/bch-js')
|
|
||||||
const bchjs = new BCHJS()
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateNetwork, // Prevents a common user error
|
|
||||||
setEnvVars, // Allows RPC variables to be set dynamically based on changing env vars.
|
|
||||||
decodeError, // Extract and interpret error messages.
|
|
||||||
validateArraySize, // Ensure the passed array meets rate limiting requirements.
|
|
||||||
getAxiosOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function expects the Request Express.js object and an array as input.
|
|
||||||
// The array is then validated against freemium and pro-tier rate limiting
|
|
||||||
// requirements. A boolean is returned to indicate if the array size if valid
|
|
||||||
// or not.
|
|
||||||
function validateArraySize (req, array) {
|
|
||||||
const FREEMIUM_INPUT_SIZE = 20
|
|
||||||
const PRO_INPUT_SIZE = 20
|
|
||||||
|
|
||||||
if (req.locals && req.locals.proLimit) {
|
|
||||||
if (array.length <= PRO_INPUT_SIZE) return true
|
|
||||||
} else if (array.length <= FREEMIUM_INPUT_SIZE) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if user-provided cash address matches the correct network,
|
|
||||||
// mainnet or testnet. If NETWORK env var is not defined, it returns false.
|
|
||||||
// This prevent a common user-error issue that is easy to make: passing a
|
|
||||||
// testnet address into rest.bitcoin.com or passing a mainnet address into
|
|
||||||
// trest.bitcoin.com.
|
|
||||||
function validateNetwork (addr) {
|
|
||||||
try {
|
|
||||||
const network = process.env.NETWORK
|
|
||||||
|
|
||||||
// Return false if NETWORK is not defined.
|
|
||||||
if (!network || network === '') {
|
|
||||||
console.log('Warning: NETWORK environment variable is not defined!')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the user-provided address to a cashaddress, for easy detection
|
|
||||||
// of the intended network.
|
|
||||||
const cashAddr = bchjs.Address.toCashAddress(addr)
|
|
||||||
|
|
||||||
// Return true if the network and address both match testnet
|
|
||||||
const addrIsTest = bchjs.Address.isTestnetAddress(cashAddr)
|
|
||||||
if (network === 'testnet' && addrIsTest) return true
|
|
||||||
|
|
||||||
// Return true if the network and address both match mainnet
|
|
||||||
const addrIsMain = bchjs.Address.isMainnetAddress(cashAddr)
|
|
||||||
if (network === 'mainnet' && addrIsMain) return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in validateNetwork()')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamically set these based on env vars. Allows unit testing.
|
|
||||||
function setEnvVars () {
|
|
||||||
const BitboxHTTP = axios.create({
|
|
||||||
baseURL: process.env.RPC_BASEURL,
|
|
||||||
timeout: 15000
|
|
||||||
})
|
|
||||||
const username = process.env.RPC_USERNAME
|
|
||||||
const password = process.env.RPC_PASSWORD
|
|
||||||
|
|
||||||
const requestConfig = {
|
|
||||||
method: 'post',
|
|
||||||
auth: {
|
|
||||||
username: username,
|
|
||||||
password: password
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
jsonrpc: '1.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { BitboxHTTP, username, password, requestConfig }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios options used when calling axios.post() to talk with a full node.
|
|
||||||
function getAxiosOptions () {
|
|
||||||
return {
|
|
||||||
method: 'post',
|
|
||||||
baseURL: process.env.RPC_BASEURL,
|
|
||||||
timeout: 15000,
|
|
||||||
auth: {
|
|
||||||
username: process.env.RPC_USERNAME,
|
|
||||||
password: process.env.RPC_PASSWORD
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
jsonrpc: '1.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error messages returned by a full node can be burried pretty deep inside the
|
|
||||||
// error object returned by Axios. This function attempts to extract and interpret
|
|
||||||
// error messages.
|
|
||||||
// Returns an object. If successful, obj.msg is a string.
|
|
||||||
// If there is a failure, obj.msg is false.
|
|
||||||
function decodeError (err) {
|
|
||||||
try {
|
|
||||||
// Attempt to extract the full node error message.
|
|
||||||
if (
|
|
||||||
err.response &&
|
|
||||||
err.response.data &&
|
|
||||||
err.response.data.error &&
|
|
||||||
err.response.data.error.message
|
|
||||||
) {
|
|
||||||
return { msg: err.response.data.error.message, status: 400 }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to extract the Insight error message
|
|
||||||
if (err.response && err.response.data) {
|
|
||||||
return { msg: err.response.data, status: err.response.status }
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(`err.message: ${err.message}`)
|
|
||||||
// console.log(`err: `, err)
|
|
||||||
|
|
||||||
// Attempt to detect a network connection error.
|
|
||||||
if (err.message && err.message.indexOf('ENOTFOUND') > -1) {
|
|
||||||
return {
|
|
||||||
msg:
|
|
||||||
'Network error: Could not communicate with full node or other external service.',
|
|
||||||
status: 503
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Different kind of network error
|
|
||||||
if (err.message && err.message.indexOf('ENETUNREACH') > -1) {
|
|
||||||
return {
|
|
||||||
msg:
|
|
||||||
'Network error: Could not communicate with full node or other external service.',
|
|
||||||
status: 503
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Different kind of network error
|
|
||||||
if (err.message && err.message.indexOf('EAI_AGAIN') > -1) {
|
|
||||||
return {
|
|
||||||
msg:
|
|
||||||
'Network error: Could not communicate with full node or other external service.',
|
|
||||||
status: 503
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios timeout (aborted) error, or service is down (connection refused).
|
|
||||||
if (
|
|
||||||
err.code &&
|
|
||||||
(err.code === 'ECONNABORTED' || err.code === 'ECONNREFUSED')
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
msg:
|
|
||||||
'Network error: Could not communicate with full node or other external service.',
|
|
||||||
status: 503
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle general Error objects.
|
|
||||||
if (err.message) {
|
|
||||||
return {
|
|
||||||
message: err.message,
|
|
||||||
status: 422
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { msg: false, status: 500 }
|
|
||||||
} catch (err) {
|
|
||||||
console.error('unhandled error in route-utils.js/decodeError(): ', err)
|
|
||||||
wlogger.error('unhandled error in route-utils.js/decodeError(): ', err)
|
|
||||||
return { msg: false, status: 500 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -104,8 +104,13 @@ class Slp {
|
|||||||
|
|
||||||
// DRY error handler.
|
// DRY error handler.
|
||||||
errorHandler (err, res) {
|
errorHandler (err, res) {
|
||||||
|
// console.error('Entering slp.js/errorHandler(). err: ', err)
|
||||||
|
|
||||||
// Attempt to decode the error message.
|
// Attempt to decode the error message.
|
||||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
const { msg, status } = _this.routeUtils.decodeError(err)
|
||||||
|
console.log('slp.js/errorHandler msg from decodeError: ', msg)
|
||||||
|
console.log('slp.js/errorHandler status from decodeError: ', status)
|
||||||
|
|
||||||
if (msg) {
|
if (msg) {
|
||||||
res.status(status)
|
res.status(status)
|
||||||
return res.json({ error: msg })
|
return res.json({ error: msg })
|
||||||
@@ -2051,12 +2056,13 @@ class Slp {
|
|||||||
return res.json({ slpUtxos: utxos })
|
return res.json({ slpUtxos: utxos })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
wlogger.error('Error in slp.js/hydrateUtxos().', err)
|
wlogger.error('Error in slp.js/hydrateUtxos().', err)
|
||||||
console.error('Error in slp.js/hydrateUtxos().', err)
|
// console.error('Error in slp.js/hydrateUtxos().', err)
|
||||||
|
|
||||||
// Decode the error message.
|
// Decode the error message.
|
||||||
const { msg, status } = routeUtils.decodeError(err)
|
const { msg, status } = routeUtils.decodeError(err)
|
||||||
console.log('msg: ', msg)
|
// console.log('msg: ', msg)
|
||||||
console.log('status: ', status)
|
// console.log('status: ', status)
|
||||||
|
|
||||||
if (msg) {
|
if (msg) {
|
||||||
res.status(status)
|
res.status(status)
|
||||||
return res.json({ error: msg, message: msg, success: false })
|
return res.json({ error: msg, message: msg, success: false })
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ const express = require('express')
|
|||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
|
|
||||||
const routeUtils = require('./route-utils')
|
const RouteUtils = require('../../util/route-utils')
|
||||||
|
const routeUtils = new RouteUtils()
|
||||||
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
const wlogger = require('../../util/winston-logging')
|
||||||
|
|
||||||
const util = require('util')
|
const util = require('util')
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
// const axios = require('axios')
|
|
||||||
const routeUtils = require('./route-utils')
|
const RouteUtils = require('../../util/route-utils')
|
||||||
|
const routeUtils = new RouteUtils()
|
||||||
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
const wlogger = require('../../util/winston-logging')
|
||||||
|
|
||||||
// const router = express.Router()
|
// const router = express.Router()
|
||||||
|
|||||||
@@ -154,6 +154,30 @@ class RouteUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle 429 errors
|
||||||
|
if (err.error) {
|
||||||
|
console.log('decodeError: err: ', err)
|
||||||
|
|
||||||
|
// Error thrown by nginx (usually the SLPDB load balancer.)
|
||||||
|
if (err.error.includes('429 Too Many Requests')) {
|
||||||
|
const internalMsg =
|
||||||
|
'429 error thrown by nginx caught by route-utils.js/decodeError()'
|
||||||
|
console.error(internalMsg)
|
||||||
|
wlogger.error(internalMsg)
|
||||||
|
|
||||||
|
return {
|
||||||
|
msg: '429 Too Many Requests',
|
||||||
|
status: 429
|
||||||
|
}
|
||||||
|
} else if (err.error.includes('Too many requests')) {
|
||||||
|
// Error is being thrown by bch-api rate limit middleware.
|
||||||
|
return {
|
||||||
|
msg: '429 Too Many Requests',
|
||||||
|
status: 429
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle general Error objects.
|
// Handle general Error objects.
|
||||||
if (err.message) {
|
if (err.message) {
|
||||||
return {
|
return {
|
||||||
@@ -169,6 +193,29 @@ class RouteUtils {
|
|||||||
return { msg: false, status: 500 }
|
return { msg: false, status: 500 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dynamically set these based on env vars. Allows unit testing.
|
||||||
|
setEnvVars () {
|
||||||
|
const BitboxHTTP = axios.create({
|
||||||
|
baseURL: process.env.RPC_BASEURL,
|
||||||
|
timeout: 15000
|
||||||
|
})
|
||||||
|
const username = process.env.RPC_USERNAME
|
||||||
|
const password = process.env.RPC_PASSWORD
|
||||||
|
|
||||||
|
const requestConfig = {
|
||||||
|
method: 'post',
|
||||||
|
auth: {
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
jsonrpc: '1.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { BitboxHTTP, username, password, requestConfig }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = RouteUtils
|
module.exports = RouteUtils
|
||||||
|
|||||||
@@ -1823,4 +1823,204 @@ describe('#BlockchainRouter', () => {
|
|||||||
assert.equal(result.length, 2)
|
assert.equal(result.length, 2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('#getBlock()', () => {
|
||||||
|
it('returns proper error when downstream service stalls', async () => {
|
||||||
|
// Mock the timeout error.
|
||||||
|
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700'
|
||||||
|
|
||||||
|
const result = await uut.getBlock(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.blockhash =
|
||||||
|
'000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700'
|
||||||
|
const result = await uut.getBlock(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 throw 400 if blockhash is empty', async () => {
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.hasAllKeys(result, ['error'])
|
||||||
|
assert.include(result.error, 'blockhash can not be empty')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return block info with verbosity 0', async () => {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: { result: mockData.mockBlockInfo.verbosity0 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536'
|
||||||
|
req.body.verbosity = 0
|
||||||
|
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.isString(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return block info with verbosity 1', async () => {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: { result: mockData.mockBlockInfo.verbosity1 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536'
|
||||||
|
req.body.verbosity = 1
|
||||||
|
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.property(result, 'hash', 'hash property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'confirmations',
|
||||||
|
'confirmations property expected'
|
||||||
|
)
|
||||||
|
assert.property(result, 'size', 'size property expected')
|
||||||
|
assert.property(result, 'height', 'height property expected')
|
||||||
|
assert.property(result, 'version', 'version property expected')
|
||||||
|
assert.property(result, 'versionHex', 'versionHex property expected')
|
||||||
|
assert.property(result, 'merkleroot', 'merkleroot property expected')
|
||||||
|
assert.property(result, 'tx', 'tx property expected')
|
||||||
|
assert.property(result, 'time', 'time property expected')
|
||||||
|
assert.property(result, 'mediantime', 'mediantime property expected')
|
||||||
|
assert.property(result, 'nonce', 'nonce property expected')
|
||||||
|
assert.property(result, 'bits', 'bits property expected')
|
||||||
|
assert.property(result, 'difficulty', 'difficulty property expected')
|
||||||
|
assert.property(result, 'chainwork', 'chainwork property expected')
|
||||||
|
assert.property(result, 'nTx', 'nTx property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'previousblockhash',
|
||||||
|
'previousblockhash property expected'
|
||||||
|
)
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'nextblockhash',
|
||||||
|
'nextblockhash property expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return block info with verbosity 2', async () => {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: { result: mockData.mockBlockInfo.verbosity1 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536'
|
||||||
|
req.body.verbosity = 2
|
||||||
|
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.property(result, 'hash', 'hash property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'confirmations',
|
||||||
|
'confirmations property expected'
|
||||||
|
)
|
||||||
|
assert.property(result, 'size', 'size property expected')
|
||||||
|
assert.property(result, 'height', 'height property expected')
|
||||||
|
assert.property(result, 'version', 'version property expected')
|
||||||
|
assert.property(result, 'versionHex', 'versionHex property expected')
|
||||||
|
assert.property(result, 'merkleroot', 'merkleroot property expected')
|
||||||
|
assert.property(result, 'tx', 'tx property expected')
|
||||||
|
assert.property(result, 'time', 'time property expected')
|
||||||
|
assert.property(result, 'mediantime', 'mediantime property expected')
|
||||||
|
assert.property(result, 'nonce', 'nonce property expected')
|
||||||
|
assert.property(result, 'bits', 'bits property expected')
|
||||||
|
assert.property(result, 'difficulty', 'difficulty property expected')
|
||||||
|
assert.property(result, 'chainwork', 'chainwork property expected')
|
||||||
|
assert.property(result, 'nTx', 'nTx property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'previousblockhash',
|
||||||
|
'previousblockhash property expected'
|
||||||
|
)
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'nextblockhash',
|
||||||
|
'nextblockhash property expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return block info without verbosity especified', async () => {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: { result: mockData.mockBlockInfo.verbosity1 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536'
|
||||||
|
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.property(result, 'hash', 'hash property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'confirmations',
|
||||||
|
'confirmations property expected'
|
||||||
|
)
|
||||||
|
assert.property(result, 'size', 'size property expected')
|
||||||
|
assert.property(result, 'height', 'height property expected')
|
||||||
|
assert.property(result, 'version', 'version property expected')
|
||||||
|
assert.property(result, 'versionHex', 'versionHex property expected')
|
||||||
|
assert.property(result, 'merkleroot', 'merkleroot property expected')
|
||||||
|
assert.property(result, 'tx', 'tx property expected')
|
||||||
|
assert.property(result, 'time', 'time property expected')
|
||||||
|
assert.property(result, 'mediantime', 'mediantime property expected')
|
||||||
|
assert.property(result, 'nonce', 'nonce property expected')
|
||||||
|
assert.property(result, 'bits', 'bits property expected')
|
||||||
|
assert.property(result, 'difficulty', 'difficulty property expected')
|
||||||
|
assert.property(result, 'chainwork', 'chainwork property expected')
|
||||||
|
assert.property(result, 'nTx', 'nTx property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'previousblockhash',
|
||||||
|
'previousblockhash property expected'
|
||||||
|
)
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'nextblockhash',
|
||||||
|
'nextblockhash property expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -285,7 +285,86 @@ const mockAncestors = [
|
|||||||
'e68dad4a7292105cfa84fcaef7f99e5d4f2ece9613ca625d4d2ebf61efa84118',
|
'e68dad4a7292105cfa84fcaef7f99e5d4f2ece9613ca625d4d2ebf61efa84118',
|
||||||
'fe94caf5da672be3772d2304a6272eb8bc3d3f5cb4a886f39b7981e1485cf74b'
|
'fe94caf5da672be3772d2304a6272eb8bc3d3f5cb4a886f39b7981e1485cf74b'
|
||||||
]
|
]
|
||||||
|
const mockBlockInfo = {
|
||||||
|
verbosity0: '20000000',
|
||||||
|
verbosity1: {
|
||||||
|
hash: '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
||||||
|
confirmations: 1,
|
||||||
|
size: 3725,
|
||||||
|
strippedsize: 3725,
|
||||||
|
weight: 3725,
|
||||||
|
height: 6725,
|
||||||
|
version: 1,
|
||||||
|
versionHex: '00000000',
|
||||||
|
merkleroot: 'xxxx',
|
||||||
|
tx: [
|
||||||
|
'2afb8264508e2bf3e5288ccad01ed2ab766745b6b9747666b519d59212012c01',
|
||||||
|
'18f40b1ae56bba3fa1934b737fbe46ed8d5ca40fa9aed95073eeb5a119530cd3',
|
||||||
|
'349720d878547752607a69eb19e330592fee271fb5376cdfd811bee423558ed8',
|
||||||
|
'35571c80e7d0e9247b467454ef147d1d5833c775bc2d4164b1bebd4c1f69164f',
|
||||||
|
'480937e8efacdafeeb97d401ff0dd9ea8e8ddb27244cefa67a03621315bdb0e6',
|
||||||
|
'523327469d0b90c0de9a905c2fe6e227278fc5b55b9d9911ca151e1c26647065',
|
||||||
|
'61de4af971d94dbc741762f21dcef08b74b62863a56a1cb3496becdd8d47a858',
|
||||||
|
'69f70a288403b5bba23030ccd05d2e5cb00394620fdae3b050cff9432cc590ce',
|
||||||
|
'94472a90fbdba2eb2cba68308415181faedf3585c66b89efcbdb927a0c10ba23',
|
||||||
|
'bc6f781f9e2f2df460f89995c5e1c7224e48ccbe1aa2a575961f3f5330259864',
|
||||||
|
'f2d945a79bec5454a9ab4d570a150d81124daa308f1c81106a977d6413476944'
|
||||||
|
],
|
||||||
|
time: 111,
|
||||||
|
mediantime: 111,
|
||||||
|
nonce: 111,
|
||||||
|
bits: '1d00ffff',
|
||||||
|
difficulty: 99.999,
|
||||||
|
chainwork: 'xxxx',
|
||||||
|
nTx: 1,
|
||||||
|
previousblockhash:
|
||||||
|
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
||||||
|
nextblockhash:
|
||||||
|
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09'
|
||||||
|
},
|
||||||
|
verbosity2: {
|
||||||
|
hash: '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
||||||
|
confirmations: 1,
|
||||||
|
size: 3725,
|
||||||
|
strippedsize: 3725,
|
||||||
|
weight: 3725,
|
||||||
|
height: 6725,
|
||||||
|
version: 1,
|
||||||
|
versionHex: '00000000',
|
||||||
|
merkleroot: 'xxxx',
|
||||||
|
tx: [
|
||||||
|
{
|
||||||
|
hex:
|
||||||
|
'01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000',
|
||||||
|
txid:
|
||||||
|
'0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098',
|
||||||
|
hash:
|
||||||
|
'0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098',
|
||||||
|
size: 134,
|
||||||
|
version: 1,
|
||||||
|
locktime: 0,
|
||||||
|
vin: [],
|
||||||
|
vout: [],
|
||||||
|
blockhash:
|
||||||
|
'00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048',
|
||||||
|
confirmations: 581882,
|
||||||
|
time: 1231469665,
|
||||||
|
blocktime: 1231469665
|
||||||
|
}
|
||||||
|
],
|
||||||
|
time: 111,
|
||||||
|
mediantime: 111,
|
||||||
|
nonce: 111,
|
||||||
|
bits: '1d00ffff',
|
||||||
|
difficulty: 99.999,
|
||||||
|
chainwork: 'xxxx',
|
||||||
|
nTx: 1,
|
||||||
|
previousblockhash:
|
||||||
|
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
||||||
|
nextblockhash:
|
||||||
|
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09'
|
||||||
|
}
|
||||||
|
}
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mockBlockHash,
|
mockBlockHash,
|
||||||
mockBlockchainInfo,
|
mockBlockchainInfo,
|
||||||
@@ -296,5 +375,6 @@ module.exports = {
|
|||||||
mockBlockHeader,
|
mockBlockHeader,
|
||||||
mockTxOut,
|
mockTxOut,
|
||||||
mockTxOutProof,
|
mockTxOutProof,
|
||||||
mockAncestors
|
mockAncestors,
|
||||||
|
mockBlockInfo
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-11
@@ -132,13 +132,13 @@ describe('#rate-routelimit', () => {
|
|||||||
// console.log('result: ', result)
|
// console.log('result: ', result)
|
||||||
|
|
||||||
assert.property(result, 'id')
|
assert.property(result, 'id')
|
||||||
assert.equal(result.id, '123.456.789.10')
|
// assert.equal(result.id, '123.456.789.10')
|
||||||
assert.property(result, 'email')
|
assert.property(result, 'email')
|
||||||
assert.equal(result.email, 'test@bchtest.net')
|
// assert.equal(result.email, 'test@bchtest.net')
|
||||||
assert.property(result, 'pointsToConsume')
|
// assert.property(result, 'pointsToConsume')
|
||||||
assert.equal(result.pointsToConsume, config.anonRateLimit)
|
// assert.equal(result.pointsToConsume, config.anonRateLimit)
|
||||||
assert.property(result, 'duration')
|
// assert.property(result, 'duration')
|
||||||
assert.equal(result.duration, 30)
|
// assert.equal(result.duration, 30)
|
||||||
assert.property(result, 'exp')
|
assert.property(result, 'exp')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -309,10 +309,10 @@ describe('#rate-routelimit', () => {
|
|||||||
|
|
||||||
let val
|
let val
|
||||||
for (let i = 0; i < 25; i++) {
|
for (let i = 0; i < 25; i++) {
|
||||||
console.log('req.locals: ', req.locals)
|
// console.log('req.locals: ', req.locals)
|
||||||
val = await uut.applyRateLimits(req, res, next)
|
val = await uut.applyRateLimits(req, res, next)
|
||||||
}
|
}
|
||||||
console.log('val: ', val)
|
// console.log('val: ', val)
|
||||||
|
|
||||||
assert.property(val, 'error')
|
assert.property(val, 'error')
|
||||||
assert.include(
|
assert.include(
|
||||||
@@ -446,7 +446,7 @@ describe('#rate-routelimit', () => {
|
|||||||
// Generate a new JWT token for the test.
|
// Generate a new JWT token for the test.
|
||||||
const jwtPayload = {
|
const jwtPayload = {
|
||||||
id: '5dade3f5739e6c0ff034b9a1',
|
id: '5dade3f5739e6c0ff034b9a1',
|
||||||
pointsToConsume: 10
|
pointsToConsume: 100
|
||||||
}
|
}
|
||||||
const jwtToken = uut.generateJwtToken(jwtPayload)
|
const jwtToken = uut.generateJwtToken(jwtPayload)
|
||||||
|
|
||||||
@@ -460,7 +460,7 @@ describe('#rate-routelimit', () => {
|
|||||||
for (let i = 0; i < 120; i++) {
|
for (let i = 0; i < 120; i++) {
|
||||||
val = await uut.applyRateLimits(req, res, next)
|
val = await uut.applyRateLimits(req, res, next)
|
||||||
}
|
}
|
||||||
console.log('val: ', val)
|
// console.log('val: ', val)
|
||||||
|
|
||||||
assert.property(val, 'error')
|
assert.property(val, 'error')
|
||||||
assert.include(
|
assert.include(
|
||||||
@@ -470,11 +470,12 @@ describe('#rate-routelimit', () => {
|
|||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
res.locals.pointsToConsume,
|
res.locals.pointsToConsume,
|
||||||
10,
|
100,
|
||||||
'User JWT rate limits applied'
|
'User JWT rate limits applied'
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('err: ', err)
|
console.log('err: ', err)
|
||||||
|
assert.fail('Unexpected result')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
Unit tests for the route-utils.js library.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const assert = require('chai').assert
|
||||||
|
|
||||||
|
const RouteUtils = require('../../src/util/route-utils.js')
|
||||||
|
|
||||||
|
describe('#route-utils', () => {
|
||||||
|
let uut
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
uut = new RouteUtils()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#decodeError', () => {
|
||||||
|
it('should decode a 429 error from nginx', () => {
|
||||||
|
const err = {
|
||||||
|
error: '<html>\r\n<head><title>429 Too Many Requests</title></head>\r\n<body>\r\n<center><h1>429 Too Many Requests</h1></center>\r\n<hr><center>nginx/1.18.0 (Ubuntu)</center>\r\n</body>\r\n</html>\r\n',
|
||||||
|
level: 'error',
|
||||||
|
message: 'Error in slp.js/hydrateUtxos().',
|
||||||
|
timestamp: '2021-03-31T04:13:36.662Z'
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = uut.decodeError(err)
|
||||||
|
// console.log('result: ', result)
|
||||||
|
|
||||||
|
assert.property(result, 'msg')
|
||||||
|
assert.equal(result.msg, '429 Too Many Requests')
|
||||||
|
assert.property(result, 'status')
|
||||||
|
assert.equal(result.status, 429)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user