diff --git a/src/routes/v5/electrumx.js b/src/routes/v5/electrumx.js index dc40a36..13ecc4f 100644 --- a/src/routes/v5/electrumx.js +++ b/src/routes/v5/electrumx.js @@ -55,8 +55,8 @@ class Electrum { this.router.get('/tx/data/:txid', this.getTransactionDetails) this.router.post('/tx/data', this.transactionDetailsBulk) this.router.post('/tx/broadcast', this.broadcastTransaction) - // this.router.get('/block/headers/:height', this.getBlockHeaders) - // this.router.post('/block/headers', this.blockHeadersBulk) + this.router.get('/block/headers/:height', this.getBlockHeaders) + this.router.post('/block/headers', this.blockHeadersBulk) // this.router.get('/transactions/:address', this.getTransactions) // this.router.post('/transactions', this.transactionsBulk) // this.router.get('/unconfirmed/:address', this.getMempool) @@ -633,63 +633,51 @@ class Electrum { * * * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v5/electrumx/block/header/42?count=2" -H "accept: application/json" + * curl -X GET "https://api.fullstack.cash/v5/electrumx/block/headers/42?count=2" -H "accept: application/json" * */ // GET handler for single block headers - // async getBlockHeaders (req, res, next) { - // try { - // const height = Number(req.params.height) - // const count = req.query.count === undefined ? 1 : Number(req.query.count) - // - // // Reject if height is not a number - // if (Number.isNaN(height) || height < 0) { - // res.status(400) - // return res.json({ - // success: false, - // error: 'height must be a positive number' - // }) - // } - // - // // Reject if height is not a number - // if (Number.isNaN(count) || count < 0) { - // res.status(400) - // return res.json({ - // success: false, - // error: 'count must be a positive number' - // }) - // } - // - // wlogger.debug( - // 'Executing electrumx/getBlockHeaders with this height: ', - // height - // ) - // - // // Get data from ElectrumX server. - // const electrumResponse = await _this._blockHeadersFromElectrum(height, count) - // // console.log(`_transactionDetailsFromElectrum(): ${JSON.stringify(electrumResponse, null, 2)}`) - // - // // Pass the error message if ElectrumX reports an error. - // if (electrumResponse instanceof Error) { - // res.status(400) - // return res.json({ - // success: false, - // error: electrumResponse.message - // }) - // } - // - // res.status(200) - // return res.json({ - // success: true, - // headers: electrumResponse - // }) - // } catch (err) { - // // Write out error to error log. - // wlogger.error('Error in elecrumx.js/getBlockHeader().', err) - // - // return _this.errorHandler(err, res) - // } - // } + async getBlockHeaders (req, res, next) { + try { + const height = Number(req.params.height) + const count = req.query.count === undefined ? 1 : Number(req.query.count) + + // Reject if height is not a number + if (Number.isNaN(height) || height < 0) { + res.status(400) + return res.json({ + success: false, + error: 'height must be a positive number' + }) + } + + // Reject if height is not a number + if (Number.isNaN(count) || count < 0) { + res.status(400) + return res.json({ + success: false, + error: 'count must be a positive number' + }) + } + + wlogger.debug( + 'Executing electrumx/getBlockHeaders with this height: ', + height + ) + + const response = await _this.axios.get( + `${_this.fulcrumApi}electrumx/block/headers/${height}?count=${count}` + ) + + res.status(200) + return res.json(response.data) + } catch (err) { + // Write out error to error log. + wlogger.error('Error in elecrumx.js/getBlockHeader().', err) + + return _this.errorHandler(err, res) + } + } /** * @api {post} /electrumx/block/headers Get block headers for an array of height + count pairs @@ -699,63 +687,50 @@ class Electrum { * Limited to 20 items per request. * * @apiExample Example usage: - * curl -X POST "https://api.fullstack.cash/v5/electrumx/block/headers" -H "accept: application/json" -H "Content-Type: application/json" -d '{"heights":[{ "height": 42, count: 2 }, { "height": 100, count: 5 }]}' + * curl -X POST "https://api.fullstack.cash/v5/electrumx/block/headers" -H "accept: application/json" -H "Content-Type: application/json" -d '{"heights":[{ "height": 42, "count": 2 }, { "height": 100, "count": 5 }]}' * */ // POST handler for bulk queries on block headers - // async blockHeadersBulk (req, res, next) { - // try { - // const heights = req.body.heights - // - // // Reject if heights is not an array. - // if (!Array.isArray(heights)) { - // res.status(400) - // return res.json({ - // success: false, - // error: 'heights needs to be an array. Use GET for single height.' - // }) - // } - // - // // Enforce array size rate limits - // if (!_this.routeUtils.validateArraySize(req, heights)) { - // res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 - // return res.json({ - // success: false, - // error: 'Array too large.' - // }) - // } - // - // wlogger.debug( - // 'Executing electrumx.js/blockHeadersBulk with these txids: ', - // heights - // ) - // - // // Loops through each address and creates an array of Promises, querying - // // the Electrum server in parallel. - // const transactions = heights.map(async (obj) => { - // const headers = await _this._blockHeadersFromElectrum( - // obj.height, - // obj.count - // ) - // - // return { headers } - // }) - // - // // Wait for all parallel Electrum requests to return. - // const result = await Promise.all(transactions) - // - // // Return the array of retrieved transaction details. - // res.status(200) - // return res.json({ - // success: true, - // headers: result - // }) - // } catch (err) { - // wlogger.error('Error in electrumx.js/blockHeadersBulk().', err) - // - // return _this.errorHandler(err, res) - // } - // } + async blockHeadersBulk (req, res, next) { + try { + const heights = req.body.heights + + // Reject if heights is not an array. + if (!Array.isArray(heights)) { + res.status(400) + return res.json({ + success: false, + error: 'heights needs to be an array. Use GET for single height.' + }) + } + + // Enforce array size rate limits + if (!_this.routeUtils.validateArraySize(req, heights)) { + res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + success: false, + error: 'Array too large.' + }) + } + + wlogger.debug( + 'Executing electrumx.js/blockHeadersBulk with these txids: ', + heights + ) + + const response = await _this.axios.post( + `${_this.fulcrumApi}electrumx/block/headers`, + { heights } + ) + + res.status(200) + return res.json(response.data) + } catch (err) { + wlogger.error('Error in electrumx.js/blockHeadersBulk().', err) + + return _this.errorHandler(err, res) + } + } // Returns a promise that resolves an array of transaction history for an // address. Expects input to be a cash address, and input validation to have diff --git a/test/v5/a01-electrumx.js b/test/v5/a01-electrumx.js index c38d426..e21603c 100644 --- a/test/v5/a01-electrumx.js +++ b/test/v5/a01-electrumx.js @@ -752,22 +752,7 @@ describe('#Electrumx', () => { assert.property(result, 'error') assert.include(result.error, 'Test error') }) - /* it('should pass errors from electrum-cash to user', async () => { - req.body.txids = [ - '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c', - '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' - ] - // Call the details API. - const result = await electrumxRoute.transactionDetailsBulk(req, res) - console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.property(result, 'success') - assert.equal(result.success, false) - - assert.property(result, 'error') - assert.include(result.error, 'Test error') - }) */ it('should get details for an array of tx', async () => { req.body.txids = [ 'a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d' @@ -877,6 +862,247 @@ describe('#Electrumx', () => { }) }) + describe('#getBlockHeaders', () => { + it('should throw 400 if height is empty', async () => { + const result = await electrumxRoute.getBlockHeaders(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, 'Expect 400 status code') + + assert.property(result, 'error') + assert.include(result.error, 'height must be a positive number') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + it('should throw 400 if height is not a number', async () => { + req.params.height = 'wrong type' + + const result = await electrumxRoute.getBlockHeaders(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, 'Expect 400 status code') + + assert.property(result, 'error') + assert.include(result.error, 'height must be a positive number') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + it('should throw 400 if height is negative', async () => { + req.params.height = -1 + + const result = await electrumxRoute.getBlockHeaders(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, 'Expect 400 status code') + + assert.property(result, 'error') + assert.include(result.error, 'height must be a positive number') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should throw 400 if count is not a number', async () => { + req.params.height = 2 + req.query.count = 'wrong type' + + const result = await electrumxRoute.getBlockHeaders(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, 'Expect 400 status code') + + assert.property(result, 'error') + assert.include(result.error, 'count must be a positive number') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should throw 400 if count is negative', async () => { + req.params.height = 2 + req.query.count = -1 + + const result = await electrumxRoute.getBlockHeaders(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, 'Expect 400 status code') + + assert.property(result, 'error') + assert.include(result.error, 'count must be a positive number') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should pass errors from electrum-cash to user', async () => { + req.params.height = 99999999999999 + req.query.count = 99999999999999 + + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox.stub(electrumxRoute.axios, 'get').resolves({ + data: { success: false, error: { error: 'Invalid height' } } + }) + } + // Call the details API. + const result = await electrumxRoute.getBlockHeaders(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'success') + assert.equal(result.success, false) + + assert.property(result, 'error') + assert.include(result.error.error, 'Invalid height') + }) + it('should handle error', async () => { + req.params.height = 42 + req.query.count = 2 + // Force error + sandbox.stub(electrumxRoute.axios, 'get').throws(new Error('Test error')) + + // Call the details API. + const result = await electrumxRoute.getBlockHeaders(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'success') + assert.equal(result.success, false) + + assert.property(result, 'error') + assert.include(result.error, 'Test error') + }) + + it('should get headers for a single block height with count 2', async () => { + req.params.height = 42 + req.query.count = 2 + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox.stub(electrumxRoute.axios, 'get').resolves({ + data: { success: true, headers: mockData.blockHeaders } + }) + } + + // Call the details API. + const result = await electrumxRoute.getBlockHeaders(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'success') + assert.equal(result.success, true) + + assert.property(result, 'headers') + assert.isArray(result.headers) + assert.deepEqual(result.headers, mockData.blockHeaders) + }) + }) + describe('#blockHeadersBulk', () => { + it('should throw 400 for an empty body', async () => { + const result = await electrumxRoute.blockHeadersBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, 'Expect 400 status code') + + assert.property(result, 'error') + assert.include(result.error, 'heights needs to be an array') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should NOT throw 400 error for an invalid height', async () => { + req.body = { + heights: [{ height: -10, count: 2 }] + } + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox.stub(electrumxRoute.axios, 'post').resolves({ + data: { success: true, headers: [{ headers: {} }] } + }) + } + const result = await electrumxRoute.blockHeadersBulk(req, res) + console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 200, 'Expect 200 status code') + + assert.property(result, 'success') + assert.equal(result.success, true) + + assert.property(result, 'headers') + assert.isArray(result.headers) + assert.property(result.headers[0], 'headers') + }) + + it('should throw 400 error if heights array is too large', async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push('') + + req.body.heights = testArray + + const result = await electrumxRoute.blockHeadersBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.property(result, 'error') + assert.include(result.error, 'Array too large') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should handle error', async () => { + req.body = { + heights: [ + { height: 42, count: 2 }, + { height: 42, count: 2 } + ] + } + + // Force error + sandbox.stub(electrumxRoute.axios, 'post').throws(new Error('Test error')) + + // Call the details API. + const result = await electrumxRoute.blockHeadersBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'success') + assert.equal(result.success, false) + + assert.property(result, 'error') + assert.include(result.error, 'Test error') + }) + it('should get block heights', async () => { + req.body = { + heights: [ + { height: 42, count: 2 }, + { height: 42, count: 2 } + ] + } + + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + sandbox + .stub(electrumxRoute.axios, 'post') + .resolves({ data: mockData.blockHeadersBulk }) + } + + // Call the details API. + const result = await electrumxRoute.blockHeadersBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'success') + assert.equal(result.success, true) + + assert.property(result, 'headers') + assert.isArray(result.headers) + assert.property(result.headers[0], 'headers') + }) + }) + // describe('#_utxosFromElectrumx', () => { // it('should throw error for invalid address', async () => { // try { diff --git a/test/v5/mocks/electrumx-mock.js b/test/v5/mocks/electrumx-mock.js index 37c73a4..e5bc3a4 100644 --- a/test/v5/mocks/electrumx-mock.js +++ b/test/v5/mocks/electrumx-mock.js @@ -176,6 +176,10 @@ const blockHeaders = [ '01000000f528fac1bcb685d0cd6c792320af0300a5ce15d687c7149548904e31000000004e8985a786d864f21e9cbb7cbdf4bc9265fe681b7a0893ac55a8e919ce035c2f85de6849ffff001d385ccb7c' ] +const blockHeadersBulk = { + success: true, + headers: [{ headers: blockHeaders }, { headers: blockHeaders }] +} module.exports = { utxos, utxosArray, @@ -185,5 +189,6 @@ module.exports = { txDetails, txDetailsBulk, blockHeaders, + blockHeadersBulk, balances }