From 75a3cee7bdc20cb0051d06f58062320932ece4ac Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 23 Apr 2020 21:49:31 -0700 Subject: [PATCH] Adding getTxOut as a POST call --- src/routes/v3/full-node/blockchain.js | 81 ++++++++++++++--- test/v3/blockchain.js | 126 ++++++++++++++++++++++++++ 2 files changed, 194 insertions(+), 13 deletions(-) diff --git a/src/routes/v3/full-node/blockchain.js b/src/routes/v3/full-node/blockchain.js index e11f375..c1f23bf 100644 --- a/src/routes/v3/full-node/blockchain.js +++ b/src/routes/v3/full-node/blockchain.js @@ -48,6 +48,7 @@ class Blockchain { this.router.get('/getMempoolInfo', this.getMempoolInfo) this.router.get('/getRawMempool', this.getRawMempool) this.router.get('/getTxOut/:txid/:n', this.getTxOut) + this.router.post('/getTxOut', this.getTxOutPost) this.router.get('/getTxOutProof/:txid', this.getTxOutProofSingle) this.router.post('/getTxOutProof', this.getTxOutProofBulk) this.router.get('/verifyTxOutProof/:proof', this.verifyTxOutProofSingle) @@ -248,7 +249,7 @@ class Blockchain { * returns an Object with information about blockheader hash. * * @apiExample Example usage: - * curl -X POST "https://rest.bitcoin.com/v3/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}" + * curl -X POST "https://api.fullstack.cash/v3/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}" * * @apiParam {String} hash block hash * @apiParam {Boolean} verbose Return verbose data @@ -283,7 +284,7 @@ class Blockchain { // Enforce array size rate limits if (!routeUtils.validateArraySize(req, hashes)) { - res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330 return res.json({ error: 'Array too large.' }) @@ -308,7 +309,7 @@ class Blockchain { const options = _this.routeUtils.getAxiosOptions() // Loop through each hash and creates an array of requests to call in parallel - const promises = hashes.map(async hash => { + const promises = hashes.map(async (hash) => { options.data.id = 'getblockheader' options.data.method = 'getblockheader' options.data.params = [hash, verbose] @@ -319,7 +320,7 @@ class Blockchain { const axiosResult = await _this.axios.all(promises) // Extract the data component from the axios response. - const result = axiosResult.map(x => x.data.result) + const result = axiosResult.map((x) => x.data.result) res.status(200) return res.json(result) @@ -456,7 +457,7 @@ class Blockchain { // Enforce array size rate limits if (!routeUtils.validateArraySize(req, txids)) { - res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330 return res.json({ error: 'Array too large.' }) @@ -481,7 +482,7 @@ class Blockchain { const options = _this.routeUtils.getAxiosOptions() // Loop through each txid and creates an array of requests to call in parallel - const promises = txids.map(async txid => { + const promises = txids.map(async (txid) => { options.data.id = 'getmempoolentry' options.data.method = 'getmempoolentry' options.data.params = [txid] @@ -492,7 +493,7 @@ class Blockchain { const axiosResult = await _this.axios.all(promises) // Extract the data component from the axios response. - const result = axiosResult.map(x => x.data.result) + const result = axiosResult.map((x) => x.data.result) res.status(200) return res.json(result) @@ -684,6 +685,60 @@ class Blockchain { } } + /** + * @api {post} /blockchain/getTxOut Validate a UTXO + * @apiName getTxOut + * @apiGroup Blockchain + * @apiDescription Returns details about an unspent transaction output (UTXO). + * + * @apiExample Example usage: + * curl -X POST "https://api.fullstack.cash/v3/blockchain/getTxOut" -H "Content-Type: application/json" -d '{"txid":"a402836c7ced7d7ad2df26d7ee5235f2605d59d65c5c567a750f9eaf186ebb47","vout": 0, "mempool": true}' + * + * @apiParam {String} txid Transaction id (required) + * @apiParam {Number} vout of transaction (required) + * @apiParam {Boolean} mempool Check mempool or not (optional) + * + */ + // Returns details about an unspent transaction output. + async getTxOutPost (req, res, next) { + try { + const txid = req.body.txid + let n = req.body.vout + const mempool = req.body.mempool ? req.body.mempool : true + + // Validate input parameter + if (!txid || txid === '') { + res.status(400) + return res.json({ error: 'txid can not be empty' }) + } + + if (n === undefined || n === '') { + res.status(400) + return res.json({ error: 'vout can not be empty' }) + } + n = parseInt(n) + + // Axios options + const options = _this.routeUtils.getAxiosOptions() + + options.data.id = 'gettxout' + options.data.method = 'gettxout' + options.data.params = [txid, n, mempool] + + // console.log(`requestConfig: ${JSON.stringify(requestConfig, null, 2)}`) + + const response = await _this.axios.request(options) + + return res.json(response.data.result) + } catch (err) { + // Write out error to error log. + // logger.error(`Error in rawtransactions/decodeRawTransaction: `, err) + wlogger.error('Error in blockchain.ts/getTxOutPost().', err) + + return _this.errorHandler(err, res) + } + } + /** * @api {get} /blockchain/getTxOutProofSingle/:txid Get Tx Out Proof * @apiName getTxOutProofSingle @@ -739,7 +794,7 @@ class Blockchain { // Enforce array size rate limits if (!routeUtils.validateArraySize(req, txids)) { - res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330 return res.json({ error: 'Array too large.' }) @@ -766,7 +821,7 @@ class Blockchain { ) // Loop through each txid and creates an array of requests to call in parallel - const promises = txids.map(async txid => { + const promises = txids.map(async (txid) => { options.data.id = 'gettxoutproof' options.data.method = 'gettxoutproof' options.data.params = [[txid]] @@ -778,7 +833,7 @@ class Blockchain { const axiosResult = await _this.axios.all(promises) // Extract the data component from the axios response. - const result = axiosResult.map(x => x.data.result) + const result = axiosResult.map((x) => x.data.result) res.status(200) return res.json(result) @@ -833,7 +888,7 @@ class Blockchain { // Enforce array size rate limits if (!routeUtils.validateArraySize(req, proofs)) { - res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330 return res.json({ error: 'Array too large.' }) @@ -858,7 +913,7 @@ class Blockchain { ) // Loop through each proof and creates an array of requests to call in parallel - const promises = proofs.map(async proof => { + const promises = proofs.map(async (proof) => { options.data.id = 'verifytxoutproof' options.data.method = 'verifytxoutproof' options.data.params = [proof] @@ -870,7 +925,7 @@ class Blockchain { const axiosResult = await _this.axios.all(promises) // Extract the data component from the axios response. - const result = axiosResult.map(x => x.data.result[0]) + const result = axiosResult.map((x) => x.data.result[0]) res.status(200) return res.json(result) diff --git a/test/v3/blockchain.js b/test/v3/blockchain.js index fcebf9e..cba83ad 100644 --- a/test/v3/blockchain.js +++ b/test/v3/blockchain.js @@ -1164,6 +1164,7 @@ describe('#BlockchainRouter', () => { assert.isArray(result) }) }) + describe('getTxOut()', () => { it('should throw 400 if txid is empty', async () => { const result = await uut.getTxOut(req, res) @@ -1226,6 +1227,7 @@ describe('#BlockchainRouter', () => { 'Error message expected' ) }) + it('returns proper error when downstream service is down', async () => { // Mock the timeout error. sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' }) @@ -1245,6 +1247,7 @@ describe('#BlockchainRouter', () => { 'Error message expected' ) }) + // This test can only run for unit tests. See TODO at the top of this file. it('should GET /getTxOut', async () => { // Mock the RPC call for unit tests. @@ -1279,6 +1282,124 @@ describe('#BlockchainRouter', () => { assert.isArray(result.scriptPubKey.addresses) }) }) + + describe('getTxOutPost()', () => { + it('should throw 400 if txid is empty', async () => { + const result = await uut.getTxOutPost(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'txid can not be empty') + }) + + it('should throw 400 if n is empty', async () => { + req.body.txid = 'sometxid' + const result = await uut.getTxOutPost(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'vout can not be empty') + }) + + 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/' + + req.body.txid = + 'd65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde' + req.body.vout = 0 + + const result = await uut.getTxOutPost(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 503 expected.') + assert.include( + result.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, 'request').throws({ code: 'ECONNABORTED' }) + + req.body.txid = + '197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d' + req.body.vout = 0 + req.body.mempool = true + + const result = await uut.getTxOutPost(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, 'request').throws({ code: 'ECONNREFUSED' }) + + req.body.txid = + '197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d' + req.body.vout = 0 + req.body.mempool = true + + const result = await uut.getTxOutPost(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 POST /getTxOut', async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === 'unit') { + sandbox + .stub(uut.axios, 'request') + .resolves({ data: { result: mockData.mockTxOut } }) + } + + req.body.txid = + '197dcda59864b1eee05498fd3c52cad787ec56ab7e635503cb39f9ab6f295d5d' + req.body.vout = 0 + req.body.mempool = true + + const result = await uut.getTxOutPost(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, [ + 'bestblock', + 'confirmations', + 'value', + 'scriptPubKey', + 'coinbase' + ]) + assert.hasAllKeys(result.scriptPubKey, [ + 'asm', + 'hex', + 'reqSigs', + 'type', + 'addresses' + ]) + assert.isArray(result.scriptPubKey.addresses) + }) + }) + describe('getTxOutProofSingle()', () => { it('should throw 400 if txid is empty', async () => { const result = await uut.getTxOutProofSingle(req, res) @@ -1311,6 +1432,7 @@ describe('#BlockchainRouter', () => { 'Error message expected' ) }) + it('returns proper error when downstream service stalls', async () => { // Mock the timeout error. sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' }) @@ -1330,6 +1452,7 @@ describe('#BlockchainRouter', () => { 'Error message expected' ) }) + it('returns proper error when downstream service is down', async () => { // Mock the timeout error. sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' }) @@ -1367,6 +1490,7 @@ describe('#BlockchainRouter', () => { assert.isString(result) }) }) + describe('#getTxOutProofBulk', () => { it('should throw an error for an empty body', async () => { req.body = {} @@ -1407,6 +1531,7 @@ describe('#BlockchainRouter', () => { 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(uut.axios, 'request').throws({ code: 'ECONNABORTED' }) @@ -1425,6 +1550,7 @@ describe('#BlockchainRouter', () => { 'Error message expected' ) }) + it('returns proper error when downstream service is down', async () => { // Mock the timeout error. sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })