From cd9f2b52921c143d232759af84999d45cddf6eef Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Thu, 13 Jan 2022 13:05:03 -0400 Subject: [PATCH] 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') + }) + }) +})