diff --git a/package.json b/package.json index c33556e..6a81775 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/", "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: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": { "node": ">=10.15.1" diff --git a/src/app.js b/src/app.js index 4c0bc57..be79aff 100644 --- a/src/app.js +++ b/src/app.js @@ -55,6 +55,7 @@ const EncryptionV5 = require('./routes/v5/encryption') const PriceV5 = require('./routes/v5/price') const JWTV5 = require('./routes/v5/jwt') const BcashSLP = require('./routes/v5/bcash/slp') +const PsfSlpIndexer = require('./routes/v5/psf-slp-indexer') // const Ninsight = require('./routes/v5/ninsight') require('dotenv').config() @@ -84,6 +85,7 @@ const utilV5 = new UtilV5({ electrumx: electrumxv5 }) const dsproofV5 = new DSProofV5() const jwtV5 = new JWTV5() const bcashSLP = new BcashSLP() +const psfSlpIndexer = new PsfSlpIndexer() const app = express() 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}/` + 'psf/slp', psfSlpIndexer.router) + // Daniel: // app.use(`/${v5prefix}/` + 'psfslp', psfSlp.router) diff --git a/src/routes/v5/psf-slp-indexer.js b/src/routes/v5/psf-slp-indexer.js index d274cc4..bd76003 100644 --- a/src/routes/v5/psf-slp-indexer.js +++ b/src/routes/v5/psf-slp-indexer.js @@ -6,23 +6,251 @@ const express = require('express') const router = express.Router() 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 // const wlogger = require('../../../util/winston-logging') -// const config = require('../../../../config') +const config = require('../../../config') + +let _this class PsfSlpIndexer { constructor () { // Encapsulate dependencies this.axios = axios this.router = router - + this.routeUtils = routeUtils + this.config = config + this.bchjs = bchjs // Define routes - // this.router.get('/', this.root) - // this.router.get('/status', this.getStatus) - // this.router.post('/address', this.getAddress) - // this.router.post('/txid', this.getTxid) - // this.routner.post('/token', this.getTokenStats) + this.router.get('/', this.root) + this.router.get('/status', this.getStatus) + this.router.post('/address', this.getAddress) + this.router.post('/txid', this.getTxid) + 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) }) } } diff --git a/start-dev-example.sh b/start-dev-example.sh index 94d499f..68e9390 100755 --- a/start-dev-example.sh +++ b/start-dev-example.sh @@ -73,4 +73,7 @@ export LOG_MAX_FILES=5d # (Optional) if using a bcash node, set this variable. Otherwise leave it as-is. export BCASH_SERVER=http://localhost +# PSF SLP indexer +export SLP_INDEXER_API=https://psf-slp-indexer.fullstack.cash/ + npm start diff --git a/test/v5/mocks/psf-slp-indexer-mocks.js b/test/v5/mocks/psf-slp-indexer-mocks.js new file mode 100644 index 0000000..76c830b --- /dev/null +++ b/test/v5/mocks/psf-slp-indexer-mocks.js @@ -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\u0004���3��s\u0003�tz}�/��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 +} diff --git a/test/v5/psf-slp-indexer.js b/test/v5/psf-slp-indexer.js new file mode 100644 index 0000000..eca0d68 --- /dev/null +++ b/test/v5/psf-slp-indexer.js @@ -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 + }) + }) +})