Merge pull request #171 from Permissionless-Software-Foundation/dh-endpoints

feat(REST): Added REST API endpoint to bch-api for the psf-slp-indexer
This commit is contained in:
Chris Troutner
2022-01-16 06:41:33 -08:00
committed by GitHub
6 changed files with 881 additions and 8 deletions
+1 -1
View File
@@ -25,7 +25,7 @@
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/", "coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/",
"docs": "./node_modules/.bin/apidoc -i src/routes/v5 -o docs", "docs": "./node_modules/.bin/apidoc -i src/routes/v5 -o docs",
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha -g '#utxosBulk' --exit --timeout 30000 test/v5/", "test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha -g '#utxosBulk' --exit --timeout 30000 test/v5/",
"test:temp2": "export NETWORK=mainnet && mocha -g '#utxosBulk' --exit --timeout 30000 test/v5/" "test:temp2": "export SLP_INDEXER_API=http://fakeurl/api/ && export NETWORK=mainnet && mocha --exit --timeout 30000 test/v5/psf-slp-indexer"
}, },
"engines": { "engines": {
"node": ">=10.15.1" "node": ">=10.15.1"
+4
View File
@@ -55,6 +55,7 @@ const EncryptionV5 = require('./routes/v5/encryption')
const PriceV5 = require('./routes/v5/price') const PriceV5 = require('./routes/v5/price')
const JWTV5 = require('./routes/v5/jwt') const JWTV5 = require('./routes/v5/jwt')
const BcashSLP = require('./routes/v5/bcash/slp') const BcashSLP = require('./routes/v5/bcash/slp')
const PsfSlpIndexer = require('./routes/v5/psf-slp-indexer')
// const Ninsight = require('./routes/v5/ninsight') // const Ninsight = require('./routes/v5/ninsight')
require('dotenv').config() require('dotenv').config()
@@ -84,6 +85,7 @@ const utilV5 = new UtilV5({ electrumx: electrumxv5 })
const dsproofV5 = new DSProofV5() const dsproofV5 = new DSProofV5()
const jwtV5 = new JWTV5() const jwtV5 = new JWTV5()
const bcashSLP = new BcashSLP() const bcashSLP = new BcashSLP()
const psfSlpIndexer = new PsfSlpIndexer()
const app = express() const app = express()
app.locals.env = process.env app.locals.env = process.env
@@ -200,6 +202,8 @@ app.use(`/${v5prefix}/` + 'ninsight', ninsight.router)
app.use(`/${v5prefix}/` + 'bcash/slp', bcashSLP.router) app.use(`/${v5prefix}/` + 'bcash/slp', bcashSLP.router)
app.use(`/${v5prefix}/` + 'psf/slp', psfSlpIndexer.router)
// Daniel: // Daniel:
// app.use(`/${v5prefix}/` + 'psfslp', psfSlp.router) // app.use(`/${v5prefix}/` + 'psfslp', psfSlp.router)
+235 -7
View File
@@ -6,23 +6,251 @@
const express = require('express') const express = require('express')
const router = express.Router() const router = express.Router()
const axios = require('axios') const axios = require('axios')
const util = require('util')
// Local libraries
const RouteUtils = require('../../util/route-utils')
const routeUtils = new RouteUtils()
const BCHJS = require('@psf/bch-js')
const bchjs = new BCHJS()
// Local libraries // Local libraries
// const wlogger = require('../../../util/winston-logging') // const wlogger = require('../../../util/winston-logging')
// const config = require('../../../../config') const config = require('../../../config')
let _this
class PsfSlpIndexer { class PsfSlpIndexer {
constructor () { constructor () {
// Encapsulate dependencies // Encapsulate dependencies
this.axios = axios this.axios = axios
this.router = router this.router = router
this.routeUtils = routeUtils
this.config = config
this.bchjs = bchjs
// Define routes // Define routes
// this.router.get('/', this.root) this.router.get('/', this.root)
// this.router.get('/status', this.getStatus) this.router.get('/status', this.getStatus)
// this.router.post('/address', this.getAddress) this.router.post('/address', this.getAddress)
// this.router.post('/txid', this.getTxid) this.router.post('/txid', this.getTxid)
// this.routner.post('/token', this.getTokenStats) this.router.post('/token', this.getTokenStats)
_this = this
}
// Root API endpoint. Simply acknowledges that it exists.
root (req, res, next) {
return res.json({ status: 'psf-slp-indexer' })
}
/**
* @api {get} /psf/slp/status/ Indexer Status.
* @apiName SLP indexer status.
* @apiGroup PSF SLP
* @apiDescription Return SLP indexer status
*
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:3000/v5/psf/slp/status
*
*
*/
async getStatus (req, res, next) {
try {
// Verify env var is set for interacting with the indexer.
_this.checkEnvVar()
const response = await _this.axios.get(
`${_this.psfSlpIndexerApi}slp/status/`
)
res.status(200)
return res.json(response.data)
} catch (err) {
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /psf/slp/address/ SLP balance for address.
* @apiName SLP balance for address.
* @apiGroup PSF SLP
* @apiDescription Return SLP balance for address
*
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "address": "bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n" }' localhost:3000/v5/psf/slp/address
*
*
*/
async getAddress (req, res, next) {
try {
// Verify env var is set for interacting with the indexer.
_this.checkEnvVar()
// Validate the input data.
const address = req.body.address
if (!address || address === '') {
res.status(400)
return res.json({
success: false,
error: 'address can not be empty'
})
}
// Ensure the input is a valid BCH address.
try {
_this.bchjs.SLP.Address.toCashAddress(address)
} catch (err) {
res.status(400)
return res.json({
success: false,
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 cashAddr = _this.bchjs.SLP.Address.toCashAddress(address)
const networkIsValid = _this.routeUtils.validateNetwork(cashAddr)
if (!networkIsValid) {
res.status(400)
return res.json({
success: false,
error:
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
})
}
const response = await _this.axios.post(
`${_this.psfSlpIndexerApi}slp/address/`,
{ address }
)
res.status(200)
return res.json(response.data)
} catch (err) {
// console.log('err', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /psf/slp/txid/ SLP transaction data.
* @apiName SLP transaction data.
* @apiGroup PSF SLP
* @apiDescription Return slp transaction data.
*
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "txid": "f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315" }' localhost:3000/v5/psf/slp/txid
*
*
*/
async getTxid (req, res, next) {
try {
// Verify env var is set for interacting with the indexer.
_this.checkEnvVar()
const txid = req.body.txid
if (!txid || txid === '') {
res.status(400)
return res.json({
success: false,
error: 'txid can not be empty'
})
}
if (txid.length !== 64) {
res.status(400)
return res.json({
success: false,
error: 'This is not a txid'
})
}
const response = await _this.axios.post(
`${_this.psfSlpIndexerApi}slp/tx/`,
{ txid }
)
res.status(200)
return res.json(response.data)
} catch (err) {
// console.log('err', err)
return _this.errorHandler(err, res)
}
}
/**
* @api {post} /psf/slp/tokenStats/ List stats for a single slp token.
* @apiName List stats for a single slp token.
* @apiGroup PSF SLP
* @apiDescription Return list stats for a single slp token.
*
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "tokenId": "a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2" }' localhost:3000/v5/psf/slp/token
*
*
*/
async getTokenStats (req, res, next) {
try {
// Verify env var is set for interacting with the indexer.
_this.checkEnvVar()
const tokenId = req.body.tokenId
if (!tokenId || tokenId === '') {
res.status(400)
return res.json({
success: false,
error: 'tokenId can not be empty'
})
}
const response = await _this.axios.post(
`${_this.psfSlpIndexerApi}slp/token/`,
{ tokenId }
)
res.status(200)
return res.json(response.data)
} catch (err) {
return _this.errorHandler(err, res)
}
}
// Check the the environment variable is set correctly.
checkEnvVar () {
_this.psfSlpIndexerApi = process.env.SLP_INDEXER_API
if (!_this.psfSlpIndexerApi) {
throw new Error(
'SLP_INDEXER_API env var not set. Can not connect to PSF SLP indexer.'
)
}
}
// DRY error handler.
errorHandler (err, res) {
// Attempt to decode the error message.
const { msg, status } = _this.routeUtils.decodeError(err)
// console.log('errorHandler msg: ', msg)
// console.log('errorHandler status: ', status)
if (msg) {
res.status(status)
return res.json({ success: false, error: msg })
}
// Handle error patterns specific to this route.
if (err.message) {
res.status(400)
return res.json({ success: false, error: err.message })
}
// If error can be handled, return the stack trace
res.status(500)
return res.json({ error: util.inspect(err) })
} }
} }
+3
View File
@@ -73,4 +73,7 @@ export LOG_MAX_FILES=5d
# (Optional) if using a bcash node, set this variable. Otherwise leave it as-is. # (Optional) if using a bcash node, set this variable. Otherwise leave it as-is.
export BCASH_SERVER=http://localhost export BCASH_SERVER=http://localhost
# PSF SLP indexer
export SLP_INDEXER_API=https://psf-slp-indexer.fullstack.cash/
npm start npm start
+130
View File
@@ -0,0 +1,130 @@
/*
This library contains mocking data for running unit tests on the psf-slp-indexer route.
*/
'use strict'
const tokenStats = {
tokenData: {
type: 1,
ticker: 'TP03',
name: 'Test Plugin 03',
tokenId: '13cad617d523c8eb4ab11fff19c010e0e0a4ea4360b58e0c8c955a45a146a669',
documentUri: 'fullstack.cash',
documentHash: 'i\u00043s\u0003tz}/PǚZ>T)',
decimals: 0,
mintBatonIsActive: false,
tokensInCirculationBN: '1',
tokensInCirculationStr: '1',
blockCreated: 722420,
totalBurned: '0',
totalMinted: '1',
txs: [
{
txid: '13cad617d523c8eb4ab11fff19c010e0e0a4ea4360b58e0c8c955a45a146a669',
height: 722420,
type: 'GENESIS',
qty: '1'
}
]
}
}
const txData = {
txData: {
txid: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
hash: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
version: 2,
size: 339,
locktime: 0,
vin: [
{
txid: '8370db30d94761ab9a11b71ecd22541151bf6125c8c613f0f6fab8ab794565a7',
vout: 0,
scriptSig: {
asm: '304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4[ALL|FORKID] 02791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851',
hex: '47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851'
},
sequence: 4294967295,
address: 'bitcoincash:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qwgaqm3wq',
value: 0.00051303,
tokenQty: 0,
tokenQtyStr: '0',
tokenId: null
}
],
vout: [
{
value: 0,
n: 0,
scriptPubKey: {
asm: 'OP_RETURN 5262419 1 47454e45534953 54524f5554 54726f75742773207465737420746f6b656e 74726f757473626c6f672e636f6d 0 2 2 000000174876e800',
hex: '6a04534c500001010747454e455349530554524f55541254726f75742773207465737420746f6b656e0e74726f757473626c6f672e636f6d4c000102010208000000174876e800',
type: 'nulldata'
},
tokenQtyStr: '0',
tokenQty: 0
}
],
hex: '0200000001a7654579abb8faf6f013c6c82561bf51115422cd1eb7119aab6147d930db7083000000006a47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851ffffffff040000000000000000476a04534c500001010747454e455349530554524f55541254726f75742773207465737420746f6b656e0e74726f757473626c6f672e636f6d4c000102010208000000174876e80022020000000000001976a914db4d39ceb7794ffe5d06855f249e1d3a7f1b024088ac22020000000000001976a914db4d39ceb7794ffe5d06855f249e1d3a7f1b024088accec20000000000001976a9145904159f2f69bfa63eefa712633a0d96dc2e7e8888ac00000000',
blockhash: '0000000000000000009f65225a3e12e23a7ea057c869047e0f36563a1f410267',
confirmations: 97398,
time: 1581773131,
blocktime: 1581773131,
blockheight: 622414,
isSlpTx: true,
tokenTxType: 'GENESIS',
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
tokenType: 1,
tokenTicker: 'TROUT',
tokenName: "Trout's test token",
tokenDecimals: 2,
tokenUri: 'troutsblog.com',
tokenDocHash: '',
isValidSlp: true
}
}
const balance = {
balance: {
utxos: [
{
txid: 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9',
vout: 1,
type: 'token',
qty: '1800',
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
address: 'bitcoincash:qrqy3kj7r822ps6628vwqq5k8hyjl6ey3y4eea2m4s'
}
],
txs: [
{
txid: '078b2c48ed1db0d5d5996f2889b8d847a49200d0a781f6aa6752f740f312688f',
height: 717796
},
{
txid: 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9',
height: 717832
}
],
balances: [
{
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
qty: '1800'
}
]
}
}
const status = {
status: {
startBlockHeight: 543376,
syncedBlockHeight: 722860,
chainBlockHeight: 722679
}
}
module.exports = {
tokenStats,
txData,
balance,
status
}
+508
View File
@@ -0,0 +1,508 @@
/*
TESTS FOR THE PSF-SLP-INDEXER.JS LIBRARY
*/
'use strict'
const chai = require('chai')
const assert = chai.assert
const PsfSlpIndexerRouter = require('../../src/routes/v5/psf-slp-indexer')
const uut = new PsfSlpIndexerRouter()
// const nock = require('nock') // HTTP mocking
const sinon = require('sinon')
let originalEnvVars // Used during transition from integration to unit tests.
// Mocking data.
const { mockReq, mockRes } = require('./mocks/express-mocks')
const mockData = require('./mocks/psf-slp-indexer-mocks')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
describe('#PsfSlpIndexer', () => {
let req, res
let sandbox
before(() => {
// Save existing environment variables.
originalEnvVars = {
SLP_INDEXER_API: process.env.SLP_INDEXER_API
}
// Set default environment variables for unit tests.
if (!process.env.TEST) process.env.TEST = 'unit'
if (process.env.TEST === 'unit') {
process.env.SLP_INDEXER_API = 'http://fakeurl/api/'
}
})
// 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 = {}
req.query = {}
sandbox = sinon.createSandbox()
})
afterEach(() => {
// Restore Sandbox
sandbox.restore()
})
after(() => {
// Restore any pre-existing environment variables.
process.env.SLP_INDEXER_API = originalEnvVars.SLP_INDEXER_API
})
describe('#root', async () => {
// root route handler.
it('should respond to GET for base route', async () => {
const result = uut.root(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(result.status, 'psf-slp-indexer', 'Returns static string')
})
})
describe('#getTokenStats', async () => {
it('should throw 400 error if tokenId is missing', async () => {
const result = await uut.getTokenStats(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(result.error, 'tokenId can not be empty')
})
it('should throw 503 when network issues', async () => {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Save the existing RPC URL.
const savedUrl2 = process.env.SLP_INDEXER_API
// Manipulate the URL to cause a 500 network error.
process.env.SLP_INDEXER_API = 'http://fakeurl/api/'
await uut.getTokenStats(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Restore the saved URL.
process.env.SLP_INDEXER_API = savedUrl2
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
})
it('returns proper error when downstream service stalls', async () => {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNABORTED' })
const result = await uut.getTokenStats(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 () => {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNREFUSED' })
const result = await uut.getTokenStats(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 tokens stats', async function () {
req.body.tokenId =
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.tokenStats })
} else {
return this.skip()
}
const result = await uut.getTokenStats(req, res)
const tokenData = result.tokenData
assert.property(tokenData, 'type')
assert.property(tokenData, 'ticker')
assert.property(tokenData, 'name')
assert.property(tokenData, 'tokenId')
assert.property(tokenData, 'documentUri')
assert.property(tokenData, 'documentHash')
assert.property(tokenData, 'decimals')
assert.property(tokenData, 'mintBatonIsActive')
assert.property(tokenData, 'tokensInCirculationBN')
assert.property(tokenData, 'tokensInCirculationStr')
assert.property(tokenData, 'blockCreated')
assert.property(tokenData, 'totalBurned')
assert.property(tokenData, 'totalMinted')
assert.property(tokenData, 'txs')
})
})
describe('#getTxid', async () => {
it('should throw 400 error if txid is missing', async () => {
const result = await uut.getTxid(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(result.error, 'txid can not be empty')
})
it('should throw 400 error if txid is invalid format', async () => {
req.body.txid = 'txid'
const result = await uut.getTxid(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(result.error, 'This is not a txid')
})
it('should throw 503 when network issues', async () => {
req.body.txid =
'f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315'
// Save the existing RPC URL.
const savedUrl2 = process.env.SLP_INDEXER_API
// Manipulate the URL to cause a 500 network error.
process.env.SLP_INDEXER_API = 'http://fakeurl/api/'
await uut.getTxid(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Restore the saved URL.
process.env.SLP_INDEXER_API = savedUrl2
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
})
it('returns proper error when downstream service stalls', async () => {
req.body.txid =
'f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNABORTED' })
const result = await uut.getTxid(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 () => {
req.body.txid =
'f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNREFUSED' })
const result = await uut.getTxid(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 tx data', async function () {
req.body.txid =
'f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315'
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.txData })
} else {
return this.skip()
}
const result = await uut.getTxid(req, res)
const txData = result.txData
assert.property(txData, 'txid')
assert.property(txData, 'hash')
assert.property(txData, 'version')
assert.property(txData, 'size')
assert.property(txData, 'locktime')
assert.property(txData, 'vin')
assert.property(txData, 'vout')
assert.property(txData, 'hex')
assert.property(txData, 'blockhash')
assert.property(txData, 'confirmations')
assert.property(txData, 'time')
assert.property(txData, 'blocktime')
assert.property(txData, 'blockheight')
assert.property(txData, 'isSlpTx')
assert.property(txData, 'tokenTxType')
assert.property(txData, 'tokenId')
assert.property(txData, 'tokenType')
assert.property(txData, 'tokenTicker')
assert.property(txData, 'tokenName')
assert.property(txData, 'tokenDecimals')
assert.property(txData, 'tokenUri')
assert.property(txData, 'tokenDocHash')
assert.property(txData, 'isValidSlp')
})
})
describe('#getAddress', async () => {
it('should throw 400 error if address is missing', async () => {
const result = await uut.getAddress(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(result.error, 'address can not be empty')
})
it('should throw 400 error if address is invalid format', async () => {
req.body.address = 'address'
const result = await uut.getAddress(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(
result.error,
'Invalid BCH address. Double check your address is valid'
)
})
it('should throw 400 error for invalid network address', async () => {
req.body.address = 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4'
const result = await uut.getAddress(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.hasAllKeys(result, ['error', 'success'])
assert.isFalse(result.success)
assert.include(
result.error,
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
)
})
it('should throw 503 when network issues', async () => {
req.body.address =
'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'
// Save the existing RPC URL.
const savedUrl2 = process.env.SLP_INDEXER_API
// Manipulate the URL to cause a 500 network error.
process.env.SLP_INDEXER_API = 'http://fakeurl/api/'
await uut.getAddress(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Restore the saved URL.
process.env.SLP_INDEXER_API = savedUrl2
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
})
it('returns proper error when downstream service stalls', async () => {
req.body.address =
'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNABORTED' })
const result = await uut.getAddress(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 () => {
req.body.address =
'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'
// Mock the timeout error.
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNREFUSED' })
const result = await uut.getAddress(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 address data', async function () {
req.body.address =
'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.balance })
} else {
return this.skip()
}
const result = await uut.getAddress(req, res)
const balance = result.balance
assert.property(balance, 'utxos')
assert.property(balance, 'txs')
assert.property(balance, 'balances')
assert.isArray(balance.utxos)
assert.isArray(balance.txs)
assert.isArray(balance.balances)
})
})
describe('#getStatus', async () => {
it('should throw 503 when network issues', async () => {
// Save the existing RPC URL.
const savedUrl2 = process.env.SLP_INDEXER_API
// Manipulate the URL to cause a 500 network error.
process.env.SLP_INDEXER_API = 'http://fakeurl/api/'
await uut.getStatus(req, res)
// console.log(`result: ${util.inspect(result)}`)
// Restore the saved URL.
process.env.SLP_INDEXER_API = savedUrl2
assert.isAbove(
res.statusCode,
499,
'HTTP status code 500 or greater expected.'
)
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
})
it('returns proper error when downstream service stalls', async () => {
// Mock the timeout error.
sandbox.stub(uut.axios, 'get').throws({ code: 'ECONNABORTED' })
const result = await uut.getStatus(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, 'get').throws({ code: 'ECONNREFUSED' })
const result = await uut.getStatus(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 indexer status', async function () {
// Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') {
sandbox.stub(uut.axios, 'get').resolves({ data: mockData.status })
} else {
return this.skip()
}
const result = await uut.getStatus(req, res)
const status = result.status
assert.property(status, 'startBlockHeight')
assert.property(status, 'syncedBlockHeight')
assert.property(status, 'chainBlockHeight')
})
})
describe('#errorHandler', () => {
it('should handle unexpected errors', () => {
sandbox.stub(uut.routeUtils, 'decodeError').returns({ msg: false })
const result = uut.errorHandler(new Error('test error'), res)
// console.log('result: ', result)
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
assert.property(result, 'error')
})
it('should handle unknow errors', () => {
sandbox.stub(uut.routeUtils, 'decodeError').returns({ msg: false })
const result = uut.errorHandler({}, res)
// console.log('result: ', result)
assert.equal(res.statusCode, 500, 'HTTP status code 500 expected.')
assert.property(result, 'error')
})
})
describe('#checkEnvVar', () => {
it('should throw errors if SLP_INDEXER_API env var is not provided', () => {
const savedSlpIndexerUrl = process.env.SLP_INDEXER_API
try {
process.env.SLP_INDEXER_API = ''
uut.checkEnvVar()
assert.fail('unexpected code path')
} catch (error) {
assert.include(
error.message,
'SLP_INDEXER_API env var not set. Can not connect to PSF SLP indexer.',
'Error message expected'
)
}
process.env.SLP_INDEXER_API = savedSlpIndexerUrl
})
})
})