diff --git a/package.json b/package.json index 34c8506..aae710d 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "coverage": "nyc report --reporter=text-lcov | coveralls", "coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/", "docs": "./node_modules/.bin/apidoc -i src/routes/v4 -o docs", - "test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha -g '#getBalance' --exit --timeout 30000 test/v5/", - "test:temp2": "export NETWORK=mainnet && mocha -g '#getBalance' --exit --timeout 30000 test/v5/" + "test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha -g '#balanceBulk' --exit --timeout 30000 test/v5/", + "test:temp2": "export NETWORK=mainnet && mocha -g '#balanceBulk' --exit --timeout 30000 test/v5/" }, "engines": { "node": ">=10.15.1" diff --git a/src/routes/v5/electrumx.js b/src/routes/v5/electrumx.js index 2ee0d66..a78b69e 100644 --- a/src/routes/v5/electrumx.js +++ b/src/routes/v5/electrumx.js @@ -26,15 +26,13 @@ let _this class Electrum { constructor () { - _this = this - - _this.config = config - _this.axios = axios - _this.routeUtils = routeUtils - _this.bchjs = bchjs + this.config = config + this.axios = axios + this.routeUtils = routeUtils + this.bchjs = bchjs // _this.bitcore = bitcore - _this.fulcrumApi = 'http://fulcrum-api.fullstackbch.nl/v1/' + this.fulcrumApi = 'http://fulcrum-api.fullstackbch.nl/v1/' // _this.electrumx = new ElectrumCash( // 'bch-api', @@ -48,21 +46,23 @@ class Electrum { // _this.isReady = false // _this.connectToServers() - _this.router = router - _this.router.get('/', _this.root) - _this.router.get('/balance/:address', _this.getBalance) - // _this.router.post('/balance', _this.balanceBulk) - // _this.router.get('/utxos/:address', _this.getUtxos) - // _this.router.post('/utxos', _this.utxosBulk) - // _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('/transactions/:address', _this.getTransactions) - // _this.router.post('/transactions', _this.transactionsBulk) - // _this.router.get('/unconfirmed/:address', _this.getMempool) - // _this.router.post('/unconfirmed', _this.mempoolBulk) + this.router = router + this.router.get('/', this.root) + this.router.get('/balance/:address', this.getBalance) + this.router.post('/balance', this.balanceBulk) + // this.router.get('/utxos/:address', this.getUtxos) + // this.router.post('/utxos', this.utxosBulk) + // 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('/transactions/:address', this.getTransactions) + // this.router.post('/transactions', this.transactionsBulk) + // this.router.get('/unconfirmed/:address', this.getMempool) + // this.router.post('/unconfirmed', this.mempoolBulk) + + _this = this } /** @@ -73,7 +73,7 @@ class Electrum { * * * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v4/electrumx/balance/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" + * curl -X GET "https://api.fullstack.cash/v5/electrumx/balance/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" * */ // GET handler for single balance @@ -109,8 +109,8 @@ class Electrum { cashAddr ) - const response = await this.axios.get( - `${this.fulcrumApi}electrumx/balance/${address}` + const response = await _this.axios.get( + `${_this.fulcrumApi}electrumx/balance/${address}` ) res.status(200) @@ -131,87 +131,78 @@ class Electrum { * Limited to 20 items per request. * * @apiExample Example usage: - * curl -X POST "https://api.fullstack.cash/v4/electrumx/balance" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' + * curl -X POST "https://api.fullstack.cash/v5/electrumx/balance" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' * * */ // POST handler for bulk queries on address balance - // async balanceBulk (req, res, next) { - // try { - // let addresses = req.body.addresses - // - // // Reject if addresses is not an array. - // if (!Array.isArray(addresses)) { - // res.status(400) - // return res.json({ - // error: 'addresses needs to be an array. Use GET for single address.' - // }) - // } - // - // // Enforce array size rate limits - // if (!_this.routeUtils.validateArraySize(req, addresses)) { - // res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 - // return res.json({ - // error: 'Array too large.' - // }) - // } - // - // wlogger.debug( - // 'Executing electrumx.js/balanceBulk with these addresses: ', - // addresses - // ) - // - // // Validate each element in the address array. - // for (let i = 0; i < addresses.length; i++) { - // const thisAddress = addresses[i] - // - // // Ensure the input is a valid BCH address. - // try { - // _this.bchjs.Address.toLegacyAddress(thisAddress) - // } catch (err) { - // res.status(400) - // return res.json({ - // error: `Invalid BCH address. Double check your address is valid: ${thisAddress}` - // }) - // } - // - // // Prevent a common user error. Ensure they are using the correct network address. - // const networkIsValid = _this.routeUtils.validateNetwork(thisAddress) - // if (!networkIsValid) { - // res.status(400) - // return res.json({ - // error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.` - // }) - // } - // } - // - // // Loops through each address and creates an array of Promises, querying - // // ElectrumX API in parallel. - // addresses = addresses.map(async (address, index) => { - // // console.log(`address: ${address}`) - // const balance = await _this._balanceFromElectrumx(address) - // - // return { - // balance, - // address - // } - // }) - // - // // Wait for all parallel Insight requests to return. - // const result = await Promise.all(addresses) - // - // // Return the array of retrieved address information. - // res.status(200) - // return res.json({ - // success: true, - // balances: result - // }) - // } catch (err) { - // wlogger.error('Error in electrumx.js/balanceBulk().', err) - // - // return _this.errorHandler(err, res) - // } - // } + async balanceBulk (req, res, next) { + try { + const addresses = req.body.addresses + + // Reject if addresses is not an array. + if (!Array.isArray(addresses)) { + res.status(400) + return res.json({ + success: false, + error: 'addresses needs to be an array. Use GET for single address.' + }) + } + + // Enforce array size rate limits + if (!_this.routeUtils.validateArraySize(req, addresses)) { + 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/balanceBulk with these addresses: ', + addresses + ) + + // Validate each element in the address array. + for (let i = 0; i < addresses.length; i++) { + const thisAddress = addresses[i] + + // Ensure the input is a valid BCH address. + try { + _this.bchjs.Address.toLegacyAddress(thisAddress) + } catch (err) { + res.status(400) + return res.json({ + success: false, + error: `Invalid BCH address. Double check your address is valid: ${thisAddress}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const networkIsValid = _this.routeUtils.validateNetwork(thisAddress) + if (!networkIsValid) { + res.status(400) + return res.json({ + success: false, + error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.` + }) + } + } + + const response = await _this.axios.post( + `${_this.fulcrumApi}electrumx/balance/`, + { addresses } + ) + + res.status(200) + return res.json(response.data) + } catch (err) { + // Write out error to error log. + wlogger.error('Error in electrumx.js/balanceBulk().', err) + + return _this.errorHandler(err, res) + } + } // Returns a promise that resolves to UTXO data for an address. Expects input // to be a cash address, and input validation to have already been done by @@ -254,7 +245,7 @@ class Electrum { * * * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v4/electrumx/utxos/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" + * curl -X GET "https://api.fullstack.cash/v5/electrumx/utxos/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" * */ // GET handler for single balance @@ -323,7 +314,7 @@ class Electrum { * Limited to 20 items per request. * * @apiExample Example usage: - * curl -X POST "https://api.fullstack.cash/v4/electrumx/utxos" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' + * curl -X POST "https://api.fullstack.cash/v5/electrumx/utxos" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' * * */ @@ -448,7 +439,7 @@ class Electrum { * * * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v4/electrumx/tx/data/a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d" -H "accept: application/json" + * curl -X GET "https://api.fullstack.cash/v5/electrumx/tx/data/a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d" -H "accept: application/json" * */ // GET handler for single transaction @@ -508,7 +499,7 @@ class Electrum { * Limited to 20 items per request. * * @apiExample Example usage: - * curl -X POST "https://api.fullstack.cash/v4/electrumx/tx/data" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d","a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"], "verbose":false}' + * curl -X POST "https://api.fullstack.cash/v5/electrumx/tx/data" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d","a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"], "verbose":false}' * * */ @@ -609,7 +600,7 @@ class Electrum { * @apiDescription Broadcast a raw transaction and return the transaction ID on success or error on failure. * * @apiExample Example usage: - * curl -X POST "https://api.fullstack.cash/v4/electrumx/tx/broadcast" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txHex":"020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000"}' + * curl -X POST "https://api.fullstack.cash/v5/electrumx/tx/broadcast" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txHex":"020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000"}' * */ // POST handler for broadcasting a single transaction @@ -700,7 +691,7 @@ class Electrum { * * * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v4/electrumx/block/header/42?count=2" -H "accept: application/json" + * curl -X GET "https://api.fullstack.cash/v5/electrumx/block/header/42?count=2" -H "accept: application/json" * */ // GET handler for single block headers @@ -766,7 +757,7 @@ class Electrum { * Limited to 20 items per request. * * @apiExample Example usage: - * curl -X POST "https://api.fullstack.cash/v4/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 @@ -865,7 +856,7 @@ class Electrum { * * * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v4/electrumx/transactions/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" + * curl -X GET "https://api.fullstack.cash/v5/electrumx/transactions/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" * */ // GET handler for single balance @@ -935,7 +926,7 @@ class Electrum { * Limited to 20 items per request. * * @apiExample Example usage: - * curl -X POST "https://api.fullstack.cash/v4/electrumx/transactions" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' + * curl -X POST "https://api.fullstack.cash/v5/electrumx/transactions" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' * * */ @@ -1058,7 +1049,7 @@ class Electrum { * * * @apiExample Example usage: - * curl -X GET "https://api.fullstack.cash/v4/electrumx/unconfirmed/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" + * curl -X GET "https://api.fullstack.cash/v5/electrumx/unconfirmed/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" * */ // GET handler for single balance @@ -1128,7 +1119,7 @@ class Electrum { * Limited to 20 items per request. * * @apiExample Example usage: - * curl -X POST "https://api.fullstack.cash/v4/electrumx/unconfirmed" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' + * curl -X POST "https://api.fullstack.cash/v5/electrumx/unconfirmed" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' * * */ diff --git a/test/v5/a01-electrumx.js b/test/v5/a01-electrumx.js index 3fa8e19..3080133 100644 --- a/test/v5/a01-electrumx.js +++ b/test/v5/a01-electrumx.js @@ -215,6 +215,147 @@ describe('#Electrumx', () => { assert.property(result.balance, 'unconfirmed') }) }) + describe('#balanceBulk', () => { + it('should throw 400 if addresses is empty', async () => { + const result = await electrumxRoute.balanceBulk(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, 'addresses needs to be an array') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should throw 400 if input provided is not array', async () => { + req.body.addresses = 'qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' + + const result = await electrumxRoute.balanceBulk(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, 'addresses needs to be an array') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should throw 400 error if addresses array is too large', async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push('') + + req.body.addresses = testArray + + const result = await electrumxRoute.balanceBulk(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 throw an error for an invalid address', async () => { + req.body.addresses = ['02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'] + + const result = await electrumxRoute.balanceBulk(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, 'Invalid BCH address') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should detect a network mismatch', async () => { + req.body.addresses = [ + 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' + ] + + const result = await electrumxRoute.balanceBulk(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, 'Invalid network', 'Proper error message') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should pass errors from electrum-cash to user', async () => { + // Address has invalid checksum. + req.body.addresses = [ + 'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2' + ] + + // Call the details API. + const result = await electrumxRoute.balanceBulk(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, 'Invalid BCH address') + }) + it('should handle error', async () => { + req.body.addresses = [ + 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7', + 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + ] + // Force error + sandbox.stub(electrumxRoute.axios, 'post').throws(new Error('Test error')) + + // Call the details API. + const result = await electrumxRoute.balanceBulk(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 balance for an array of addresses', async () => { + req.body.addresses = [ + 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7', + 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + ] + + // 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.balances }) + } + + // Call the details API. + const result = await electrumxRoute.balanceBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'success') + assert.equal(result.success, true) + + assert.property(result, 'balances') + assert.property(result.balances[0], 'balance') + assert.property(result.balances[0], 'address') + + assert.property(result.balances[0].balance, 'confirmed') + assert.property(result.balances[0].balance, 'unconfirmed') + }) + }) // describe('#_utxosFromElectrumx', () => { // it('should throw error for invalid address', async () => { diff --git a/test/v5/mocks/electrumx-mock.js b/test/v5/mocks/electrumx-mock.js index a4d37a5..9769942 100644 --- a/test/v5/mocks/electrumx-mock.js +++ b/test/v5/mocks/electrumx-mock.js @@ -26,7 +26,19 @@ const balance = { unconfirmed: 0 } } - +const balances = { + success: true, + balances: [ + { + balance: { confirmed: 7000, unconfirmed: 0 }, + address: 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7' + }, + { + balance: { confirmed: 7000, unconfirmed: 0 }, + address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + } + ] +} const txHistory = [ { height: 601861, @@ -128,5 +140,6 @@ module.exports = { txHistory, mempool, txDetails, - blockHeaders + blockHeaders, + balances }