From 7acb778470991a674fc40a85405e75cc5859cdcd Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Thu, 25 Mar 2021 17:30:30 -0400 Subject: [PATCH] feat(getblock): Added getblock endpoint --- src/routes/v4/full-node/blockchain.js | 44 ++++++ test/v4/blockchain.js | 196 ++++++++++++++++++++++++++ test/v4/mocks/blockchain-mock.js | 84 ++++++++++- 3 files changed, 322 insertions(+), 2 deletions(-) diff --git a/src/routes/v4/full-node/blockchain.js b/src/routes/v4/full-node/blockchain.js index 3881c2d..c358de1 100644 --- a/src/routes/v4/full-node/blockchain.js +++ b/src/routes/v4/full-node/blockchain.js @@ -53,6 +53,7 @@ class Blockchain { this.router.post('/getTxOutProof', this.getTxOutProofBulk) this.router.get('/verifyTxOutProof/:proof', this.verifyTxOutProofSingle) this.router.post('/verifyTxOutProof', this.verifyTxOutProofBulk) + this.router.post('/getBlock', this.getBlock) } root (req, res, next) { @@ -937,6 +938,49 @@ class Blockchain { return _this.errorHandler(err, res) } } + + /** + * @api {post} /blockchain/getBlock/ Get block details + * @apiName getBlock + * @apiGroup Blockchain + * @apiDescription Returns block details + * + * @apiExample Example usage: + * curl "https://api.fullstack.cash/v4/blockchain/getblock/" -X POST -H "Content-Type: application/json" --data-binary '{"blockhash":"000000000000000002a5fe0bdd6e3f04342a975c0f55e57f97e73bb90041676b","verbosity":0 }' + * + * @apiParam {String} blockhash Block hash (required) + * @apiParam {Number} verbosity Default 1 (optional) + * + */ + async getBlock (req, res, next) { + try { + // Validate input parameter + const blockhash = req.body.blockhash + const verbosity = req.body.verbosity || 1 // default + + if (!blockhash || blockhash === '') { + res.status(400) + return res.json({ error: 'blockhash can not be empty' }) + } + + // Axios options + const options = _this.routeUtils.getAxiosOptions() + + options.data.id = 'getblock' + options.data.method = 'getblock' + options.data.params = [blockhash, verbosity] + + 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/getMempoolEntrySingle().', err) + + return _this.errorHandler(err, res) + } + } } module.exports = Blockchain diff --git a/test/v4/blockchain.js b/test/v4/blockchain.js index 1d71270..bde0fbe 100644 --- a/test/v4/blockchain.js +++ b/test/v4/blockchain.js @@ -1823,4 +1823,200 @@ describe('#BlockchainRouter', () => { assert.equal(result.length, 2) }) }) + describe('getBlock()', () => { + it('returns proper error when downstream service stalls', async () => { + // Mock the timeout error. + sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' }) + + req.body.blockhash = + '000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700' + + const result = await uut.getBlock(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.blockhash = + '000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700' + const result = await uut.getBlock(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 throw 400 if blockhash is empty', async () => { + const result = await uut.getBlock(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'blockhash can not be empty') + }) + it('return block info with verbosity 0', async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === 'unit') { + sandbox + .stub(uut.axios, 'request') + .resolves({ data: { result: mockData.mockBlockInfo.verbosity0 } }) + } + req.body.blockhash = + '000000000000000002a5fe0bdd6e3f04342a975c0f55e57f97e73bb90041676b' + req.body.verbosity = 0 + + const result = await uut.getBlock(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isString(result) + }) + it('return block info with verbosity 1', async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === 'unit') { + sandbox + .stub(uut.axios, 'request') + .resolves({ data: { result: mockData.mockBlockInfo.verbosity1 } }) + } + req.body.blockhash = + '000000000000000002a5fe0bdd6e3f04342a975c0f55e57f97e73bb90041676b' + req.body.verbosity = 1 + + const result = await uut.getBlock(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'hash', 'hash property expected') + assert.property( + result, + 'confirmations', + 'confirmations property expected' + ) + assert.property(result, 'size', 'size property expected') + assert.property(result, 'strippedsize', 'strippedsize property expected') + assert.property(result, 'weight', 'weight property expected') + assert.property(result, 'height', 'height property expected') + assert.property(result, 'version', 'version property expected') + assert.property(result, 'versionHex', 'versionHex property expected') + assert.property(result, 'merkleroot', 'merkleroot property expected') + assert.property(result, 'tx', 'tx property expected') + assert.property(result, 'time', 'time property expected') + assert.property(result, 'mediantime', 'mediantime property expected') + assert.property(result, 'nonce', 'nonce property expected') + assert.property(result, 'bits', 'bits property expected') + assert.property(result, 'difficulty', 'difficulty property expected') + assert.property(result, 'chainwork', 'chainwork property expected') + assert.property(result, 'nTx', 'nTx property expected') + assert.property( + result, + 'previousblockhash', + 'previousblockhash property expected' + ) + assert.property( + result, + 'nextblockhash', + 'nextblockhash property expected' + ) + }) + + it('return block info with verbosity 2', async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === 'unit') { + sandbox + .stub(uut.axios, 'request') + .resolves({ data: { result: mockData.mockBlockInfo.verbosity1 } }) + } + req.body.blockhash = + '000000000000000002a5fe0bdd6e3f04342a975c0f55e57f97e73bb90041676b' + req.body.verbosity = 1 + + const result = await uut.getBlock(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'hash', 'hash property expected') + assert.property( + result, + 'confirmations', + 'confirmations property expected' + ) + assert.property(result, 'size', 'size property expected') + assert.property(result, 'strippedsize', 'strippedsize property expected') + assert.property(result, 'weight', 'weight property expected') + assert.property(result, 'height', 'height property expected') + assert.property(result, 'version', 'version property expected') + assert.property(result, 'versionHex', 'versionHex property expected') + assert.property(result, 'merkleroot', 'merkleroot property expected') + assert.property(result, 'tx', 'tx property expected') + assert.property(result, 'time', 'time property expected') + assert.property(result, 'mediantime', 'mediantime property expected') + assert.property(result, 'nonce', 'nonce property expected') + assert.property(result, 'bits', 'bits property expected') + assert.property(result, 'difficulty', 'difficulty property expected') + assert.property(result, 'chainwork', 'chainwork property expected') + assert.property(result, 'nTx', 'nTx property expected') + assert.property( + result, + 'previousblockhash', + 'previousblockhash property expected' + ) + assert.property( + result, + 'nextblockhash', + 'nextblockhash property expected' + ) + }) + it('return block info without verbosity especified', async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === 'unit') { + sandbox + .stub(uut.axios, 'request') + .resolves({ data: { result: mockData.mockBlockInfo.verbosity1 } }) + } + req.body.blockhash = + '000000000000000002a5fe0bdd6e3f04342a975c0f55e57f97e73bb90041676b' + + const result = await uut.getBlock(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'hash', 'hash property expected') + assert.property( + result, + 'confirmations', + 'confirmations property expected' + ) + assert.property(result, 'size', 'size property expected') + assert.property(result, 'strippedsize', 'strippedsize property expected') + assert.property(result, 'weight', 'weight property expected') + assert.property(result, 'height', 'height property expected') + assert.property(result, 'version', 'version property expected') + assert.property(result, 'versionHex', 'versionHex property expected') + assert.property(result, 'merkleroot', 'merkleroot property expected') + assert.property(result, 'tx', 'tx property expected') + assert.property(result, 'time', 'time property expected') + assert.property(result, 'mediantime', 'mediantime property expected') + assert.property(result, 'nonce', 'nonce property expected') + assert.property(result, 'bits', 'bits property expected') + assert.property(result, 'difficulty', 'difficulty property expected') + assert.property(result, 'chainwork', 'chainwork property expected') + assert.property(result, 'nTx', 'nTx property expected') + assert.property( + result, + 'previousblockhash', + 'previousblockhash property expected' + ) + assert.property( + result, + 'nextblockhash', + 'nextblockhash property expected' + ) + }) + }) }) diff --git a/test/v4/mocks/blockchain-mock.js b/test/v4/mocks/blockchain-mock.js index 21a699e..af9eed0 100644 --- a/test/v4/mocks/blockchain-mock.js +++ b/test/v4/mocks/blockchain-mock.js @@ -285,7 +285,86 @@ const mockAncestors = [ 'e68dad4a7292105cfa84fcaef7f99e5d4f2ece9613ca625d4d2ebf61efa84118', 'fe94caf5da672be3772d2304a6272eb8bc3d3f5cb4a886f39b7981e1485cf74b' ] - +const mockBlockInfo = { + verbosity0: '20000000', + verbosity1: { + hash: '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09', + confirmations: 1, + size: 3725, + strippedsize: 3725, + weight: 3725, + height: 6725, + version: 1, + versionHex: '00000000', + merkleroot: 'xxxx', + tx: [ + '2afb8264508e2bf3e5288ccad01ed2ab766745b6b9747666b519d59212012c01', + '18f40b1ae56bba3fa1934b737fbe46ed8d5ca40fa9aed95073eeb5a119530cd3', + '349720d878547752607a69eb19e330592fee271fb5376cdfd811bee423558ed8', + '35571c80e7d0e9247b467454ef147d1d5833c775bc2d4164b1bebd4c1f69164f', + '480937e8efacdafeeb97d401ff0dd9ea8e8ddb27244cefa67a03621315bdb0e6', + '523327469d0b90c0de9a905c2fe6e227278fc5b55b9d9911ca151e1c26647065', + '61de4af971d94dbc741762f21dcef08b74b62863a56a1cb3496becdd8d47a858', + '69f70a288403b5bba23030ccd05d2e5cb00394620fdae3b050cff9432cc590ce', + '94472a90fbdba2eb2cba68308415181faedf3585c66b89efcbdb927a0c10ba23', + 'bc6f781f9e2f2df460f89995c5e1c7224e48ccbe1aa2a575961f3f5330259864', + 'f2d945a79bec5454a9ab4d570a150d81124daa308f1c81106a977d6413476944' + ], + time: 111, + mediantime: 111, + nonce: 111, + bits: '1d00ffff', + difficulty: 99.999, + chainwork: 'xxxx', + nTx: 1, + previousblockhash: + '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09', + nextblockhash: + '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09' + }, + verbosity2: { + hash: '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09', + confirmations: 1, + size: 3725, + strippedsize: 3725, + weight: 3725, + height: 6725, + version: 1, + versionHex: '00000000', + merkleroot: 'xxxx', + tx: [ + { + hex: + '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000', + txid: + '0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098', + hash: + '0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098', + size: 134, + version: 1, + locktime: 0, + vin: [], + vout: [], + blockhash: + '00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048', + confirmations: 581882, + time: 1231469665, + blocktime: 1231469665 + } + ], + time: 111, + mediantime: 111, + nonce: 111, + bits: '1d00ffff', + difficulty: 99.999, + chainwork: 'xxxx', + nTx: 1, + previousblockhash: + '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09', + nextblockhash: + '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09' + } +} module.exports = { mockBlockHash, mockBlockchainInfo, @@ -296,5 +375,6 @@ module.exports = { mockBlockHeader, mockTxOut, mockTxOutProof, - mockAncestors + mockAncestors, + mockBlockInfo }