diff --git a/package-lock.json b/package-lock.json index 710a89e..e15f7e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6458,6 +6458,13 @@ "p-cancelable": "^1.0.0", "to-readable-stream": "^1.0.0", "url-parse-lax": "^3.0.0" + }, + "dependencies": { + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + } } }, "graceful-fs": { @@ -13340,11 +13347,6 @@ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, - "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" - }, "p-each-series": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz", diff --git a/package.json b/package.json index ae22554..9d226ba 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/", "test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/slp.js", "test:integration": "mocha test/v3/integration", - "test:integration:slpdb": "mocha --timeout 25000 test/v3/integration/slp*.js", + "test:integration:slpdb": "mocha --timeout 25000 -g '#validate2Single' test/v3/integration/slp*.js", "coverage": "nyc report --reporter=text-lcov | coveralls", "coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v3/", "docs": "./node_modules/.bin/apidoc -i src/routes/v3 -o docs" diff --git a/src/routes/v3/slp.js b/src/routes/v3/slp.js index 8452722..551a694 100644 --- a/src/routes/v3/slp.js +++ b/src/routes/v3/slp.js @@ -13,6 +13,7 @@ const Slpdb = require('./services/slpdb') // const strftime = require('strftime') const wlogger = require('../../util/winston-logging') +// Instantiate a local copy of bch-js using the local REST API server. const LOCAL_RESTURL = process.env.LOCAL_RESTURL ? process.env.LOCAL_RESTURL : 'https://api.fullstack.cash/v3/' @@ -51,6 +52,7 @@ class Slp { constructor () { _this = this + // Encapsulate external libraries. _this.axios = axios _this.routeUtils = routeUtils _this.BigNumber = BigNumber @@ -71,6 +73,7 @@ class Slp { _this.router.post('/convert', _this.convertAddressBulk) _this.router.post('/validateTxid', _this.validateBulk) _this.router.get('/validateTxid/:txid', _this.validateSingle) + _this.router.get('/validateTxid2/:txid', _this.validate2Single) _this.router.get('/txDetails/:txid', _this.txDetails) _this.router.get('/tokenStats/:tokenId', _this.tokenStats) _this.router.get( @@ -1062,7 +1065,7 @@ class Slp { * @api {get} /slp/validateTxid/{txid} Validate single SLP transaction by txid. * @apiName Validate single SLP transaction by txid. * @apiGroup SLP - * @apiDescription Validate single SLP transaction by txid. + * @apiDescription Validate single SLP transaction by txid, using SLPDB. * * * @apiExample Example usage: @@ -1135,11 +1138,66 @@ class Slp { } } - // Returns a Boolean if the input TXID is a valid SLP TXID. - // async function isValidSlpTxid (txid) { - // const isValid = await slpValidator.isValidSlpTxid(txid) - // return isValid - // } + /** + * @api {get} /slp/validateTxid2/{txid} Validate single SLP transaction by txid. + * @apiName Validate single SLP transaction by txid. + * @apiGroup SLP + * @apiDescription Validate single SLP transaction by txid, using slp-validate. + * Slower, less efficient method of validating an SLP TXID using the slp-validate + * npm library. This method is independent of SLPDB and can be used as a fall-back + * when SLPDB returns 'null' values. + * + * + * @apiExample Example usage: + * curl -X GET "https://api.fullstack.cash/v3/slp/validateTxid2/f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a" -H "accept:application/json" + * + * + */ + async validate2Single (req, res, next) { + try { + const txid = req.params.txid + + // Validate input + if (!txid || txid === '') { + res.status(400) + return res.json({ error: 'txid can not be empty' }) + } + + wlogger.debug( + 'Executing slp/validate2Single/:txid with this txid: ', + txid + ) + + // null by default. + // Default return value. + const result = { + txid: txid, + isValid: null, + msg: '' + } + + // Request options + const opt = { + method: 'get', + baseURL: `${process.env.SLP_API_URL}slp/validate/${txid}`, + timeout: 10000 // Exit after 10 seconds. + } + const tokenRes = await _this.axios.request(opt) + // console.log(`tokenRes.data: ${JSON.stringify(tokenRes.data, null, 2)}`) + // console.log(`tokenRes: `, tokenRes) + + // Overwrite the default value with the result from slp-api. + result.isValid = tokenRes.data.isValid + + res.status(200) + return res.json(result) + } catch (err) { + // console.log('validate2Single error: ', err) + wlogger.error('Error in slp.ts/validate2Single().', err) + + return _this.errorHandler(err, res) + } + } /** * @api {get} /slp/txDetails/{txid} SLP transaction details. diff --git a/test/v3/integration/slp.js b/test/v3/integration/slp.js index 279d077..4de4e88 100644 --- a/test/v3/integration/slp.js +++ b/test/v3/integration/slp.js @@ -213,4 +213,50 @@ describe('#slp', () => { assert.equal(result.slpUtxos[2].utxos.length, 2) }) }) + + describe('#validate2Single', () => { + it('should invalidate a known invalid TXID', async () => { + const txid = + 'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a' + + req.params.txid = txid + const result = await slp.validate2Single(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result.txid, txid) + assert.equal(result.isValid, false) + }) + + it('should validate a known valid TXID', async () => { + const txid = + '3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488' + + req.params.txid = txid + const result = await slp.validate2Single(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result.txid, txid) + assert.equal(result.isValid, true) + }) + + // CT 10-11-2020: This test is valid, but because of the cacheing built + // into slp-validate, it will not consistently pass or fail. To manually + // Run this test, re-start slp-api or find a token txid with a long DAG. + // it('should cancel if validation takes too long', async () => { + // const txid = + // 'eacb1085dfa296fef6d4ae2c0f4529a1bef096dd2325bdcc6dcb5241b3bdb579' + // // '2822f7d21e17ca8c36846613dc52caf0d03e355a13022a6cec066d197e59bb27' + // + // req.params.txid = txid + // const result = await slp.validate2Single(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' + // ) + // }) + }) }) diff --git a/test/v3/mocks/slp-mocks.js b/test/v3/mocks/slp-mocks.js index c90e2db..198e9b7 100644 --- a/test/v3/mocks/slp-mocks.js +++ b/test/v3/mocks/slp-mocks.js @@ -352,7 +352,8 @@ const mockTxHistory = [ e: { h: '7bdd586ebd1e5f3dfd5295ab6e896c48b25855ab77a72c035ce1e7aacc965c2d', i: 1, - s: 'RzBEAiAsu5o9EnxgZChjxKygxhGLhlfxJJbQfd4PP/vp2od3fAIgPpMqg+Sp4YpiInQZ6weJwELq8LEIrHXJpueBz7yF5gRBIQLsyL3809mRphc/+tPAcTQO5bi0kqZRFKpKIHXE3qN8Rw==', + s: + 'RzBEAiAsu5o9EnxgZChjxKygxhGLhlfxJJbQfd4PP/vp2od3fAIgPpMqg+Sp4YpiInQZ6weJwELq8LEIrHXJpueBz7yF5gRBIQLsyL3809mRphc/+tPAcTQO5bi0kqZRFKpKIHXE3qN8Rw==', a: 'simpleledger:qrrrpqmdkggpnw0czwg0jgcjd7yhu25jy5zxh2gqdq' } }, @@ -361,7 +362,8 @@ const mockTxHistory = [ e: { h: 'df49feff24dc34a10a44a0cbd7c908d964801ec7218512c84574fbce698535f0', i: 3, - s: 'SDBFAiEAhW3zbKTlPrOXD2E2oEcNof6vCMPGYIg8vOVSE9c8IakCIFLg/gxydG9eL8HMAzkScGElKWJRnfhVMpMHU3evNcrZQSEDRS7F+pSC8OxSldsT4FctJLZBU7f2+FDiE05ae1xqtN0=', + s: + 'SDBFAiEAhW3zbKTlPrOXD2E2oEcNof6vCMPGYIg8vOVSE9c8IakCIFLg/gxydG9eL8HMAzkScGElKWJRnfhVMpMHU3evNcrZQSEDRS7F+pSC8OxSldsT4FctJLZBU7f2+FDiE05ae1xqtN0=', a: 'simpleledger:qpkpeqfslejw5pptzcy25h2jxhsc9k0vts43n26up0' } } @@ -371,7 +373,8 @@ const mockTxHistory = [ e: { v: 0, i: 0, - s: 'agRTTFAAAQEEU0VORCA46XxdfTWFosvz+VgMgsozmF+csIRdTcziIMtwn5U4sAgAAAAAAJiWgAgAAAAAJHfU+g==' + s: + 'agRTTFAAAQEEU0VORCA46XxdfTWFosvz+VgMgsozmF+csIRdTcziIMtwn5U4sAgAAAAAAJiWgAgAAAAAJHfU+g==' } }, { @@ -402,7 +405,8 @@ const mockTxHistory = [ slp: { detail: { decimals: 8, - tokenIdHex: '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0', + tokenIdHex: + '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0', transactionType: 'SEND', versionType: 1, documentUri: 'psfoundation.cash', @@ -439,7 +443,8 @@ const mockTxHistory = [ e: { h: 'e4e1e1f6b502cbd42f69b919634cea3e37fc8595e6ca800e0a7d8f6dcdfb249e', i: 3, - s: 'SDBFAiEAjCGRmU28x24LMTwg5XqC+fLf3zGhTYCsSWpmF8Eshq0CIAJhxAKMbwKWC4ASmjWzplpno2ch+hGNUD6HNWOdbd4SQSECeRsZo5Fl29g0A9bfJo1E/WIdowWBsLblyxWnEB7ViFE=', + s: + 'SDBFAiEAjCGRmU28x24LMTwg5XqC+fLf3zGhTYCsSWpmF8Eshq0CIAJhxAKMbwKWC4ASmjWzplpno2ch+hGNUD6HNWOdbd4SQSECeRsZo5Fl29g0A9bfJo1E/WIdowWBsLblyxWnEB7ViFE=', a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7' } }, @@ -448,7 +453,8 @@ const mockTxHistory = [ e: { h: '438420345dd8b7fb4ea74aaf2e3090f44899bf3c71662243946a441de6dae720', i: 2, - s: 'SDBFAiEAhmLYL4QY3mvZo3/i0XG9PJAYMruw3MaGOLJ3Z4si7hwCIAaaGwRnj/cZK3L/KKFBoMtItpGRZ10GowNN//vgn6PKQSECeRsZo5Fl29g0A9bfJo1E/WIdowWBsLblyxWnEB7ViFE=', + s: + 'SDBFAiEAhmLYL4QY3mvZo3/i0XG9PJAYMruw3MaGOLJ3Z4si7hwCIAaaGwRnj/cZK3L/KKFBoMtItpGRZ10GowNN//vgn6PKQSECeRsZo5Fl29g0A9bfJo1E/WIdowWBsLblyxWnEB7ViFE=', a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7' } } @@ -458,7 +464,8 @@ const mockTxHistory = [ e: { v: 0, i: 0, - s: 'agRTTFAAAQEEU0VORCCk+1wtoaoGTiUBikP5FlBABx2emEuhkMIip/WQU6+EsggAAAAAAA9CQAgAAAAXQnHEwA==' + s: + 'agRTTFAAAQEEU0VORCCk+1wtoaoGTiUBikP5FlBABx2emEuhkMIip/WQU6+EsggAAAAAAA9CQAgAAAAXQnHEwA==' } }, { @@ -489,7 +496,8 @@ const mockTxHistory = [ slp: { detail: { decimals: 2, - tokenIdHex: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + tokenIdHex: + 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', transactionType: 'SEND', versionType: 1, documentUri: 'troutsblog.com', diff --git a/test/v3/slp.js b/test/v3/slp.js index 47d5537..e847904 100644 --- a/test/v3/slp.js +++ b/test/v3/slp.js @@ -362,6 +362,7 @@ describe('#SLP', () => { assert.hasAllKeys(result, ['error']) assert.include(result.error, 'Array too large') }) + it('returns proper error when downstream service stalls', async () => { // Mock the timeout error. sandbox.stub(slpRoute.axios, 'request').throws({ code: 'ECONNABORTED' }) @@ -462,6 +463,80 @@ describe('#SLP', () => { }) }) + describe('#validate2Single', () => { + it('should throw 400 if txid is empty', async () => { + req.params.txid = '' + const result = await slpRoute.validate2Single(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'txid can not be empty') + }) + + it('should invalidate a known invalid TXID', async () => { + if (process.env.TEST === 'unit') { + // Mock to prevent live network connection. + sandbox + .stub(slpRoute.axios, 'request') + .resolves({ data: { isValid: false } }) + } + + const txid = + 'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a' + + req.params.txid = txid + const result = await slpRoute.validate2Single(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result.txid, txid) + assert.equal(result.isValid, false) + }) + + it('should validate a known valid TXID', async () => { + if (process.env.TEST === 'unit') { + // Mock to prevent live network connection. + sandbox + .stub(slpRoute.axios, 'request') + .resolves({ data: { isValid: true } }) + } + + const txid = + '3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488' + + req.params.txid = txid + const result = await slpRoute.validate2Single(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result.txid, txid) + assert.equal(result.isValid, true) + }) + + // This test can only be run as a mocked unit test. It's too inconsistent + // to run as an integration test, due to the caching built into slp-validate. + if (process.env.TEST === 'unit') { + it('should cancel if validation takes too long', async () => { + // Mock the timeout error. + sandbox.stub(slpRoute.axios, 'request').throws({ + code: 'ECONNABORTED' + }) + + const txid = + 'eacb1085dfa296fef6d4ae2c0f4529a1bef096dd2325bdcc6dcb5241b3bdb579' + + req.params.txid = txid + const result = await slpRoute.validate2Single(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' + ) + }) + } + }) + describe('tokenStats()', () => { it('should throw 400 if tokenID is empty', async () => { req.params.tokenId = ''