From cd9f2b52921c143d232759af84999d45cddf6eef Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Thu, 13 Jan 2022 13:05:03 -0400 Subject: [PATCH 1/6] feat(REST): Added REST API endpoint to bch-api for the psf-slp-indexer --- package.json | 2 +- src/app.js | 4 + src/routes/v5/psf-slp-indexer.js | 83 ++++++++++- test/v5/mocks/psf-slp-indexer-mocks.js | 35 +++++ test/v5/psf-slp-indexer.js | 182 +++++++++++++++++++++++++ 5 files changed, 300 insertions(+), 6 deletions(-) create mode 100644 test/v5/mocks/psf-slp-indexer-mocks.js create mode 100644 test/v5/psf-slp-indexer.js 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..4631cbc 100644 --- a/src/routes/v5/psf-slp-indexer.js +++ b/src/routes/v5/psf-slp-indexer.js @@ -6,23 +6,96 @@ const express = require('express') const router = express.Router() const axios = require('axios') +const RouteUtils = require('../../util/route-utils') +const routeUtils = new RouteUtils() // Local libraries // const wlogger = require('../../../util/winston-logging') // const config = require('../../../../config') - +let _this class PsfSlpIndexer { constructor () { // Encapsulate dependencies - this.axios = axios - this.router = router + _this = this + _this.axios = axios + _this.router = router + _this.routeUtils = routeUtils + _this.psfSlpIndexerApi = process.env.SLP_INDEXER_API + if (!this.psfSlpIndexerApi) { + // console.warn('FULCRUM_API env var not set. Can not connect to Fulcrum indexer.') + throw new Error( + 'SLP_INDEXER_API env var not set. Can not connect to psf slp indexer.' + ) + } // Define routes - // this.router.get('/', this.root) + _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.post('/token', this.getTokenStats) + } + + /** + * @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 { + 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) + } + } + + // 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) }) + } + + // Root API endpoint. Simply acknowledges that it exists. + root (req, res, next) { + return res.json({ status: 'psf-slp-indexer' }) } } 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..29080d1 --- /dev/null +++ b/test/v5/mocks/psf-slp-indexer-mocks.js @@ -0,0 +1,35 @@ +/* + 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' + } + ] + } +} + +module.exports = { + tokenStats +} diff --git a/test/v5/psf-slp-indexer.js b/test/v5/psf-slp-indexer.js new file mode 100644 index 0000000..3751ad0 --- /dev/null +++ b/test/v5/psf-slp-indexer.js @@ -0,0 +1,182 @@ +/* + 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 = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = 'unit' + if (process.env.TEST === 'unit') { + process.env.BITCOINCOM_BASEURL = 'http://fakeurl/api/' + process.env.RPC_BASEURL = 'http://fakeurl/api' + process.env.RPC_USERNAME = 'fakeusername' + process.env.RPC_PASSWORD = 'fakepassword' + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + // Restore Sandbox + sandbox.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe('#root', 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.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = 'http://fakeurl/api/' + + await uut.getTokenStats(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + 'HTTP status code 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') + }) + }) +}) From b72ed28d497ea256d816932f64f3446d4f6fe669 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 13 Jan 2022 09:35:02 -0800 Subject: [PATCH 2/6] Added linting --- src/routes/v5/psf-slp-indexer.js | 62 +++++++++++++++++++------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/routes/v5/psf-slp-indexer.js b/src/routes/v5/psf-slp-indexer.js index 4631cbc..e418358 100644 --- a/src/routes/v5/psf-slp-indexer.js +++ b/src/routes/v5/psf-slp-indexer.js @@ -6,34 +6,39 @@ 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() // Local libraries // const wlogger = require('../../../util/winston-logging') -// const config = require('../../../../config') -let _this +const config = require('../../../config') + +// let _this + class PsfSlpIndexer { constructor () { // Encapsulate dependencies - _this = this - _this.axios = axios - _this.router = router - _this.routeUtils = routeUtils - _this.psfSlpIndexerApi = process.env.SLP_INDEXER_API - if (!this.psfSlpIndexerApi) { - // console.warn('FULCRUM_API env var not set. Can not connect to Fulcrum indexer.') - throw new Error( - 'SLP_INDEXER_API env var not set. Can not connect to psf slp indexer.' - ) - } + this.axios = axios + this.router = router + this.routeUtils = routeUtils + this.config = config // Define routes - _this.router.get('/', this.root) + 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.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' }) } /** @@ -50,6 +55,9 @@ class PsfSlpIndexer { */ 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) @@ -58,22 +66,33 @@ class PsfSlpIndexer { error: 'tokenId can not be empty' }) } - const response = await _this.axios.post( - `${_this.psfSlpIndexerApi}slp/token/`, + + 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) + 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) + const { msg, status } = this.routeUtils.decodeError(err) // console.log('errorHandler msg: ', msg) // console.log('errorHandler status: ', status) @@ -92,11 +111,6 @@ class PsfSlpIndexer { 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: 'psf-slp-indexer' }) - } } module.exports = PsfSlpIndexer From ea3801165cf1263e4df76ce37400446a522b987f Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Fri, 14 Jan 2022 18:32:49 -0400 Subject: [PATCH 3/6] finished endpoints --- src/routes/v5/psf-slp-indexer.js | 147 ++++++++++- test/v5/mocks/psf-slp-indexer-mocks.js | 99 +++++++- test/v5/psf-slp-indexer.js | 329 +++++++++++++++++++++++++ 3 files changed, 569 insertions(+), 6 deletions(-) diff --git a/src/routes/v5/psf-slp-indexer.js b/src/routes/v5/psf-slp-indexer.js index e418358..2630d50 100644 --- a/src/routes/v5/psf-slp-indexer.js +++ b/src/routes/v5/psf-slp-indexer.js @@ -12,6 +12,9 @@ const util = require('util') 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') @@ -25,12 +28,12 @@ class PsfSlpIndexer { 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.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 @@ -41,6 +44,142 @@ class PsfSlpIndexer { 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. diff --git a/test/v5/mocks/psf-slp-indexer-mocks.js b/test/v5/mocks/psf-slp-indexer-mocks.js index 29080d1..76c830b 100644 --- a/test/v5/mocks/psf-slp-indexer-mocks.js +++ b/test/v5/mocks/psf-slp-indexer-mocks.js @@ -30,6 +30,101 @@ const tokenStats = { } } -module.exports = { - tokenStats +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 index 3751ad0..efc5050 100644 --- a/test/v5/psf-slp-indexer.js +++ b/test/v5/psf-slp-indexer.js @@ -179,4 +179,333 @@ describe('#PsfSlpIndexer', () => { 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.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = 'http://fakeurl/api/' + + await uut.getTxid(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + 'HTTP status code 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.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = 'http://fakeurl/api/' + + await uut.getAddress(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + assert.isAbove( + res.statusCode, + 499, + 'HTTP status code 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.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = 'http://fakeurl/api/' + + await uut.getStatus(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + assert.isAbove( + res.statusCode, + 499, + 'HTTP status code 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 + }) + }) }) From 9aea5533f85e0b365c2807db8a62df6d6d114e77 Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Fri, 14 Jan 2022 19:28:44 -0400 Subject: [PATCH 4/6] Changed the use for _this --- src/routes/v5/psf-slp-indexer.js | 46 ++++++++++++++++---------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/routes/v5/psf-slp-indexer.js b/src/routes/v5/psf-slp-indexer.js index 2630d50..4dcb74c 100644 --- a/src/routes/v5/psf-slp-indexer.js +++ b/src/routes/v5/psf-slp-indexer.js @@ -19,7 +19,7 @@ const bchjs = new BCHJS() // const wlogger = require('../../../util/winston-logging') const config = require('../../../config') -// let _this +let _this class PsfSlpIndexer { constructor () { @@ -36,7 +36,7 @@ class PsfSlpIndexer { this.router.post('/txid', this.getTxid) this.router.post('/token', this.getTokenStats) - // _this = this + _this = this } // Root API endpoint. Simply acknowledges that it exists. @@ -59,14 +59,14 @@ class PsfSlpIndexer { async getStatus (req, res, next) { try { // Verify env var is set for interacting with the indexer. - this.checkEnvVar() + _this.checkEnvVar() - const response = await this.axios.get(`${this.psfSlpIndexerApi}slp/status/`) + 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) + return _this.errorHandler(err, res) } } @@ -85,7 +85,7 @@ class PsfSlpIndexer { async getAddress (req, res, next) { try { // Verify env var is set for interacting with the indexer. - this.checkEnvVar() + _this.checkEnvVar() // Validate the input data. const address = req.body.address @@ -99,7 +99,7 @@ class PsfSlpIndexer { // Ensure the input is a valid BCH address. try { - this.bchjs.SLP.Address.toCashAddress(address) + _this.bchjs.SLP.Address.toCashAddress(address) } catch (err) { res.status(400) return res.json({ @@ -109,8 +109,8 @@ class PsfSlpIndexer { } // 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) + const cashAddr = _this.bchjs.SLP.Address.toCashAddress(address) + const networkIsValid = _this.routeUtils.validateNetwork(cashAddr) if (!networkIsValid) { res.status(400) return res.json({ @@ -120,8 +120,8 @@ class PsfSlpIndexer { }) } - const response = await this.axios.post( - `${this.psfSlpIndexerApi}slp/address/`, + const response = await _this.axios.post( + `${_this.psfSlpIndexerApi}slp/address/`, { address } ) @@ -129,7 +129,7 @@ class PsfSlpIndexer { return res.json(response.data) } catch (err) { // console.log('err', err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -148,7 +148,7 @@ class PsfSlpIndexer { async getTxid (req, res, next) { try { // Verify env var is set for interacting with the indexer. - this.checkEnvVar() + _this.checkEnvVar() const txid = req.body.txid if (!txid || txid === '') { @@ -167,8 +167,8 @@ class PsfSlpIndexer { }) } - const response = await this.axios.post( - `${this.psfSlpIndexerApi}slp/tx/`, + const response = await _this.axios.post( + `${_this.psfSlpIndexerApi}slp/tx/`, { txid } ) @@ -176,7 +176,7 @@ class PsfSlpIndexer { return res.json(response.data) } catch (err) { // console.log('err', err) - return this.errorHandler(err, res) + return _this.errorHandler(err, res) } } @@ -195,7 +195,7 @@ class PsfSlpIndexer { async getTokenStats (req, res, next) { try { // Verify env var is set for interacting with the indexer. - this.checkEnvVar() + _this.checkEnvVar() const tokenId = req.body.tokenId if (!tokenId || tokenId === '') { @@ -206,22 +206,22 @@ class PsfSlpIndexer { }) } - const response = await this.axios.post( - `${this.psfSlpIndexerApi}slp/token/`, + 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) + return _this.errorHandler(err, res) } } // Check the the environment variable is set correctly. checkEnvVar () { - this.psfSlpIndexerApi = process.env.SLP_INDEXER_API - if (!this.psfSlpIndexerApi) { + _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.' ) @@ -231,7 +231,7 @@ class PsfSlpIndexer { // DRY error handler. errorHandler (err, res) { // Attempt to decode the error message. - const { msg, status } = this.routeUtils.decodeError(err) + const { msg, status } = _this.routeUtils.decodeError(err) // console.log('errorHandler msg: ', msg) // console.log('errorHandler status: ', status) From 4255d98c71b2102ab2fadb56a9e788249939fce7 Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Fri, 14 Jan 2022 22:29:01 -0400 Subject: [PATCH 5/6] Fixed unit tests --- test/v5/psf-slp-indexer.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/test/v5/psf-slp-indexer.js b/test/v5/psf-slp-indexer.js index efc5050..98827ec 100644 --- a/test/v5/psf-slp-indexer.js +++ b/test/v5/psf-slp-indexer.js @@ -27,19 +27,13 @@ describe('#PsfSlpIndexer', () => { before(() => { // Save existing environment variables. originalEnvVars = { - BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, - RPC_BASEURL: process.env.RPC_BASEURL, - RPC_USERNAME: process.env.RPC_USERNAME, - RPC_PASSWORD: process.env.RPC_PASSWORD + 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.BITCOINCOM_BASEURL = 'http://fakeurl/api/' - process.env.RPC_BASEURL = 'http://fakeurl/api' - process.env.RPC_USERNAME = 'fakeusername' - process.env.RPC_PASSWORD = 'fakepassword' + process.env.SLP_INDEXER_API = 'http://fakeurl/api/' } }) @@ -64,10 +58,7 @@ describe('#PsfSlpIndexer', () => { after(() => { // Restore any pre-existing environment variables. - process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL - process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL - process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME - process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + process.env.SLP_INDEXER_API = originalEnvVars.SLP_INDEXER_API }) describe('#root', async () => { From 3a1ad53a421f43bdc2b60c99ea478e6e3c4d6374 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 16 Jan 2022 06:39:37 -0800 Subject: [PATCH 6/6] Fixing integration tests --- src/routes/v5/psf-slp-indexer.js | 48 ++++++++++++----------- start-dev-example.sh | 3 ++ test/v5/psf-slp-indexer.js | 66 +++++++++++++++++--------------- 3 files changed, 64 insertions(+), 53 deletions(-) diff --git a/src/routes/v5/psf-slp-indexer.js b/src/routes/v5/psf-slp-indexer.js index 4dcb74c..bd76003 100644 --- a/src/routes/v5/psf-slp-indexer.js +++ b/src/routes/v5/psf-slp-indexer.js @@ -45,23 +45,25 @@ class PsfSlpIndexer { } /** - * @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 - * - * - */ + * @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/`) + const response = await _this.axios.get( + `${_this.psfSlpIndexerApi}slp/status/` + ) res.status(200) return res.json(response.data) @@ -71,17 +73,17 @@ class PsfSlpIndexer { } /** - * @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 - * - * - */ + * @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. 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/psf-slp-indexer.js b/test/v5/psf-slp-indexer.js index 98827ec..eca0d68 100644 --- a/test/v5/psf-slp-indexer.js +++ b/test/v5/psf-slp-indexer.js @@ -86,16 +86,16 @@ describe('#PsfSlpIndexer', () => { 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2' // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL + const savedUrl2 = process.env.SLP_INDEXER_API // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = 'http://fakeurl/api/' + 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.RPC_BASEURL = savedUrl2 + process.env.SLP_INDEXER_API = savedUrl2 assert.isAbove( res.statusCode, @@ -104,6 +104,7 @@ describe('#PsfSlpIndexer', () => { ) // 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' @@ -145,9 +146,7 @@ describe('#PsfSlpIndexer', () => { 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2' // Mock the RPC call for unit tests. if (process.env.TEST === 'unit') { - sandbox - .stub(uut.axios, 'post') - .resolves({ data: mockData.tokenStats }) + sandbox.stub(uut.axios, 'post').resolves({ data: mockData.tokenStats }) } else { return this.skip() } @@ -190,21 +189,22 @@ describe('#PsfSlpIndexer', () => { 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.RPC_BASEURL + const savedUrl2 = process.env.SLP_INDEXER_API // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = 'http://fakeurl/api/' + 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.RPC_BASEURL = savedUrl2 + process.env.SLP_INDEXER_API = savedUrl2 assert.isAbove( res.statusCode, @@ -254,9 +254,7 @@ describe('#PsfSlpIndexer', () => { 'f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315' // Mock the RPC call for unit tests. if (process.env.TEST === 'unit') { - sandbox - .stub(uut.axios, 'post') - .resolves({ data: mockData.txData }) + sandbox.stub(uut.axios, 'post').resolves({ data: mockData.txData }) } else { return this.skip() } @@ -306,7 +304,10 @@ describe('#PsfSlpIndexer', () => { assert.hasAllKeys(result, ['error', 'success']) assert.isFalse(result.success) - assert.include(result.error, 'Invalid BCH address. Double check your address is valid') + 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' @@ -316,23 +317,27 @@ describe('#PsfSlpIndexer', () => { 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.') + 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' + req.body.address = + 'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n' // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL + const savedUrl2 = process.env.SLP_INDEXER_API // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = 'http://fakeurl/api/' + 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.RPC_BASEURL = savedUrl2 + process.env.SLP_INDEXER_API = savedUrl2 assert.isAbove( res.statusCode, 499, @@ -340,8 +345,10 @@ describe('#PsfSlpIndexer', () => { ) // 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' + req.body.address = + 'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n' // Mock the timeout error. sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNABORTED' }) @@ -358,7 +365,8 @@ describe('#PsfSlpIndexer', () => { }) it('returns proper error when downstream service is down', async () => { - req.body.address = 'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n' + req.body.address = + 'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n' // Mock the timeout error. sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNREFUSED' }) @@ -375,13 +383,12 @@ describe('#PsfSlpIndexer', () => { }) it('should GET address data', async function () { - req.body.address = 'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n' + 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 }) + sandbox.stub(uut.axios, 'post').resolves({ data: mockData.balance }) } else { return this.skip() } @@ -400,16 +407,16 @@ describe('#PsfSlpIndexer', () => { describe('#getStatus', async () => { it('should throw 503 when network issues', async () => { // Save the existing RPC URL. - const savedUrl2 = process.env.RPC_BASEURL + const savedUrl2 = process.env.SLP_INDEXER_API // Manipulate the URL to cause a 500 network error. - process.env.RPC_BASEURL = 'http://fakeurl/api/' + 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.RPC_BASEURL = savedUrl2 + process.env.SLP_INDEXER_API = savedUrl2 assert.isAbove( res.statusCode, 499, @@ -417,6 +424,7 @@ describe('#PsfSlpIndexer', () => { ) // 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' }) @@ -450,9 +458,7 @@ describe('#PsfSlpIndexer', () => { 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 }) + sandbox.stub(uut.axios, 'get').resolves({ data: mockData.status }) } else { return this.skip() }