From fe15354b9be448b43bcf4bb82d46e35cf2da36f0 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 7 Apr 2020 20:49:23 -0700 Subject: [PATCH 01/12] fix(electrumx): Added hooks for testnet --- config/electrumx.js | 5 +++++ src/routes/v3/electrumx.js | 26 ++++++++++++++------------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/config/electrumx.js b/config/electrumx.js index 7f6f36e..2dddaed 100644 --- a/config/electrumx.js +++ b/config/electrumx.js @@ -27,4 +27,9 @@ const config = { } } +if (process.env.NETWORK === 'testnet') { + config.electrum.serverUrl = 'blackie.c3-soft.com' + config.electrum.serverPort = '60002' +} + module.exports = config diff --git a/src/routes/v3/electrumx.js b/src/routes/v3/electrumx.js index 6e60920..a8a8bfb 100644 --- a/src/routes/v3/electrumx.js +++ b/src/routes/v3/electrumx.js @@ -32,14 +32,6 @@ class Electrum { _this.bchjs = bchjs _this.bitcore = bitcore - // Configure the ElectrumX/Fulcrum server. - // _this.electrumx = new ElectrumCash( - // config.electrumx.application, - // config.electrumx.version, - // config.electrumx.confidence, - // config.electrumx.distribution, - // ElectrumCash.ORDER.PRIORITY - // ) _this.electrumx = new ElectrumCash( config.electrumx.electrum.application, config.electrumx.electrum.version, @@ -58,7 +50,7 @@ class Electrum { // Initializes a connection to electrum servers. async connect () { try { - console.log('Entering connectToServers()') + console.log('Attempting to connect to ElectrumX server...)') // Return immediately if a connection has already been established. if (_this.isReady) return true @@ -69,12 +61,14 @@ class Electrum { // Set the connection flag. _this.isReady = true + console.log(`...Successfully connected to ElectrumX server.`) + // console.log(`_this.isReady: ${_this.isReady}`) return _this.isReady } catch (err) { - // console.log(`err: `, err) - wlogger.error('Error in electrumx.js/connect()') - throw err + console.log(`err: `, err) + wlogger.error('Error in electrumx.js/connect(): ', err) + // throw err } } @@ -124,6 +118,14 @@ class Electrum { return res.json({ status: 'electrumx' }) } + async utxosFromElectrumx (address) { + try { + + } catch (err) { + + } + } + /** * @api {get} /electrumx/utxos/{addr} Get utxos for a single address. * @apiName UTXOs for a single address From 769fd5919fe74d4fd6cc461d573b6f1606ec7ad4 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 7 Apr 2020 20:57:37 -0700 Subject: [PATCH 02/12] miner changes --- config/electrumx.js | 4 +++- src/routes/v3/electrumx.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/electrumx.js b/config/electrumx.js index 2dddaed..a49e502 100644 --- a/config/electrumx.js +++ b/config/electrumx.js @@ -18,7 +18,6 @@ const config = { // 'electron.jochen-hoenicke.de:51002' // ] serverUrl: 'fulcrum.fountainhead.cash', - // serverUrl: 'badurl.com', serverPort: '50002' }, ratelimit: { @@ -30,6 +29,9 @@ const config = { if (process.env.NETWORK === 'testnet') { config.electrum.serverUrl = 'blackie.c3-soft.com' config.electrum.serverPort = '60002' + + // config.electrum.serverUrl = '192.168.0.6' + // config.electrum.serverPort = '50001' } module.exports = config diff --git a/src/routes/v3/electrumx.js b/src/routes/v3/electrumx.js index a8a8bfb..ae40442 100644 --- a/src/routes/v3/electrumx.js +++ b/src/routes/v3/electrumx.js @@ -50,7 +50,7 @@ class Electrum { // Initializes a connection to electrum servers. async connect () { try { - console.log('Attempting to connect to ElectrumX server...)') + console.log('Attempting to connect to ElectrumX server...') // Return immediately if a connection has already been established. if (_this.isReady) return true From 4eadea6d884ceca29a3328b3a4a9d3a778bff55a Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 12 Apr 2020 18:14:02 -0700 Subject: [PATCH 03/12] fix(electrumx config): Passing electrum server info as env var --- config/electrumx.js | 37 ---------- config/index.js | 5 +- docker/mainnet/start-local-mainnet.sh.save | 28 ++++++++ package.json | 2 +- src/routes/v3/electrumx.js | 61 +++++++++-------- start-dev-example.sh | 6 ++ test/v3/electrumx.js | 78 +++++----------------- 7 files changed, 86 insertions(+), 131 deletions(-) delete mode 100644 config/electrumx.js create mode 100755 docker/mainnet/start-local-mainnet.sh.save diff --git a/config/electrumx.js b/config/electrumx.js deleted file mode 100644 index a49e502..0000000 --- a/config/electrumx.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - Config settings for working with an ElectrumX or Fulcrum server. -*/ - -const config = { - port: 8000, - electrum: { - application: 'bch-api', - version: '1.4.1', - confidence: 2, - distribution: 3, - // servers: [ - // 'fulcrum.fountainhead.cash:50002', - // 'electrum.imaginary.cash:50002', - // 'bch.imaginary.cash:50002', - // 'electroncash.de:50002', - // 'electroncash.dk:50002', - // 'electron.jochen-hoenicke.de:51002' - // ] - serverUrl: 'fulcrum.fountainhead.cash', - serverPort: '50002' - }, - ratelimit: { - windowMs: 1 * 60 * 1000, - max: 100 - } -} - -if (process.env.NETWORK === 'testnet') { - config.electrum.serverUrl = 'blackie.c3-soft.com' - config.electrum.serverPort = '60002' - - // config.electrum.serverUrl = '192.168.0.6' - // config.electrum.serverPort = '50001' -} - -module.exports = config diff --git a/config/index.js b/config/index.js index 0f7087d..2f47cea 100644 --- a/config/index.js +++ b/config/index.js @@ -2,11 +2,8 @@ Common configuration settings. */ -const electrumxConfig = require('./electrumx') - const config = { - apiTokenSecret: process.env.TOKENSECRET ? process.env.TOKENSECRET : 'secret-jwt-token', - electrumx: electrumxConfig + apiTokenSecret: process.env.TOKENSECRET ? process.env.TOKENSECRET : 'secret-jwt-token' } module.exports = config diff --git a/docker/mainnet/start-local-mainnet.sh.save b/docker/mainnet/start-local-mainnet.sh.save new file mode 100755 index 0000000..6eb943a --- /dev/null +++ b/docker/mainnet/start-local-mainnet.sh.save @@ -0,0 +1,28 @@ +#!/bin/bash + +# test + +# Full node +export RPC_BASEURL=http://142.93.13.2:8332/ +export RPC_USERNAME=bitcoin +export RPC_PASSWORD=password +export NETWORK=mainnet + +# SLPDB +#export SLPDB_URL=https://slpdb.bitcoin.com/ +#export SLPDB_URL=http://172.17.0.1:12300/ +export SLPDB_URL=https://slpdb2.bchtest.net/ +export SLPDB_PASS=owmvgnsksoapwhrnvu + +# Blockbook Indexer +export BLOCKBOOK_URL=https://157.230.214.175:9131/ +# Allow node.js to make network calls to https using self-signed certificate. +export NODE_TLS_REJECT_UNAUTHORIZED=0 + +export JWT_AUTH_SERVER=http://172.17.0.1:5001/ + +# Redis DB +export REDIS_PORT=6379 +export REDIS_HOST=172.17.0.1 + +npm start diff --git a/package.json b/package.json index 48cbd24..879f52c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dev": "nodemon ./dist/app.js", "test": "npm run lint && npm run test-v3", "lint": "standard --env mocha --fix", - "test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/", + "test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/electrumx.js", "test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/blockchain.js", "test:integration": "mocha test/v3/integration", "coverage": "nyc report --reporter=text-lcov | coveralls", diff --git a/src/routes/v3/electrumx.js b/src/routes/v3/electrumx.js index ae40442..0bf400e 100644 --- a/src/routes/v3/electrumx.js +++ b/src/routes/v3/electrumx.js @@ -33,10 +33,10 @@ class Electrum { _this.bitcore = bitcore _this.electrumx = new ElectrumCash( - config.electrumx.electrum.application, - config.electrumx.electrum.version, - config.electrumx.electrum.serverUrl, - config.electrumx.electrum.serverPort + 'bch-api', + '1.4.1', + process.env.FULCRUM_URL, + process.env.FULCRUM_PORT ) _this.isReady = false @@ -61,12 +61,12 @@ class Electrum { // Set the connection flag. _this.isReady = true - console.log(`...Successfully connected to ElectrumX server.`) + console.log('...Successfully connected to ElectrumX server.') // console.log(`_this.isReady: ${_this.isReady}`) return _this.isReady } catch (err) { - console.log(`err: `, err) + console.log('err: ', err) wlogger.error('Error in electrumx.js/connect(): ', err) // throw err } @@ -118,11 +118,34 @@ class Electrum { return res.json({ status: 'electrumx' }) } - async utxosFromElectrumx (address) { + // 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 + // calling function. + async _utxosFromElectrumx (address) { try { + // Convert the address to a scripthash. + const scripthash = _this.addressToScripthash(address) + if (!_this.isReady) { + throw new Error( + 'ElectrumX server connection is not ready. Call await connectToServer() first.' + ) + } + + // Query the utxos from the ElectrumX server. + const electrumResponse = await _this.electrumx.request( + 'blockchain.scripthash.listunspent', + scripthash + ) + // console.log( + // `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}` + // ) + + return electrumResponse } catch (err) { - + // Write out error to error log. + wlogger.error('Error in elecrumx.js/_utxosFromElectrumx().') + throw err } } @@ -164,25 +187,11 @@ class Electrum { }) } - wlogger.debug('Executing electrumx/getUtxos with this address: ', address) + wlogger.debug('Executing electrumx/getUtxos with this address: ', cashAddr) - // Convert the address to a scripthash. - const scripthash = _this.addressToScripthash(cashAddr) - - if (!_this.isReady) { - throw new Error( - 'ElectrumX server connection is not ready. Call await connectToServer() first.' - ) - } - - // Query the utxos from the ElectrumX server. - var electrumResponse = await _this.electrumx.request( - 'blockchain.scripthash.listunspent', - scripthash - ) - // console.log( - // `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}` - // ) + // Get data from ElectrumX server. + const electrumResponse = await _this._utxosFromElectrumx(cashAddr) + console.log(`electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}`) // Pass the error message if ElectrumX reports an error. if (Object.prototype.hasOwnProperty.call(electrumResponse, 'code')) { diff --git a/start-dev-example.sh b/start-dev-example.sh index 4d0ae09..5034068 100755 --- a/start-dev-example.sh +++ b/start-dev-example.sh @@ -14,4 +14,10 @@ export BLOCKBOOK_URL=https://:9131/ # Allow node.js to make network calls to https using self-signed certificate. export NODE_TLS_REJECT_UNAUTHORIZED=0 +# Mainnet Fulcrum / ElectrumX +export FULCRUM_URL=192.168.0.6 +export FULCRUM_PORT=50002 + +export TOKENSECRET=somelongpassword + npm start diff --git a/test/v3/electrumx.js b/test/v3/electrumx.js index 15b19cd..244a58a 100644 --- a/test/v3/electrumx.js +++ b/test/v3/electrumx.js @@ -135,8 +135,7 @@ describe('#ElectrumX Router', () => { }) it('should throw an error for an invalid address', async () => { - req.params.address = - '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' + req.params.address = '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' const result = await electrumxRoute.getUtxos(req, res) // console.log(`result: ${util.inspect(result)}`) @@ -151,8 +150,7 @@ describe('#ElectrumX Router', () => { }) it('should detect a network mismatch', async () => { - req.params.address = - 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' + req.params.address = 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' const result = await electrumxRoute.getUtxos(req, res) // console.log(`result: ${util.inspect(result)}`) @@ -165,68 +163,22 @@ describe('#ElectrumX Router', () => { assert.property(result, 'success') assert.equal(result.success, false) }) + }) - // it('should throw 500 when network issues', async () => { - // const savedUrl = process.env.BLOCKBOOK_URL - // - // try { - // req.params.address = 'qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' - // - // // Switch the Insight URL to something that will error out. - // process.env.BLOCKBOOK_URL = 'http://fakeurl/api/' - // - // const result = await blockbookRoute.balanceSingle(req, res) - // - // // Restore the saved URL. - // process.env.BLOCKBOOK_URL = savedUrl - // - // assert.equal(res.statusCode, 500, 'HTTP status code 500 expected.') - // assert.include(result.error, 'ENOTFOUND', 'Error message expected') - // } catch (err) { - // // Restore the saved URL. - // process.env.BLOCKBOOK_URL = savedUrl - // } - // }) + describe('#_utxosFromElectrumx', () => { + // Unit test only. + if (process.env.TEST === 'unit') { + it('should pass errors from ElectrumX to user', async () => { + req.params.address = + 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' - // it('returns proper error when downstream service stalls', async () => { - // req.params.address = - // 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' - // - // // Mock the timeout error. - // sandbox.stub(blockbookRoute.axios, 'request').throws({ - // code: 'ECONNABORTED' - // }) - // - // const result = await blockbookRoute.balanceSingle(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' - // ) - // }) + electrumxRoute.isReady = true // Force flag. - // it('returns proper error when downstream service is down', async () => { - // req.params.address = - // 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' - // - // // Mock the timeout error. - // sandbox.stub(blockbookRoute.axios, 'request').throws({ - // code: 'ECONNREFUSED' - // }) - // - // const result = await blockbookRoute.balanceSingle(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' - // ) - // }) + sandbox + .stub(electrumxRoute.electrumx, 'request') + .resolves(mockData.utxos) + }) + } it('should get balance for a single address', async () => { req.params.address = From d43cf6cb8089aa27371028736d30c639241584eb Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 12 Apr 2020 19:15:45 -0700 Subject: [PATCH 04/12] Improved tests for electrumx lib --- package.json | 2 +- src/routes/v3/electrumx.js | 8 ++-- test/v3/electrumx.js | 90 +++++++++++++++++++++++++++++++++----- 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 879f52c..48cbd24 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dev": "nodemon ./dist/app.js", "test": "npm run lint && npm run test-v3", "lint": "standard --env mocha --fix", - "test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/electrumx.js", + "test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/", "test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/blockchain.js", "test:integration": "mocha test/v3/integration", "coverage": "nyc report --reporter=text-lcov | coveralls", diff --git a/src/routes/v3/electrumx.js b/src/routes/v3/electrumx.js index 0bf400e..bc2b2be 100644 --- a/src/routes/v3/electrumx.js +++ b/src/routes/v3/electrumx.js @@ -120,7 +120,7 @@ class Electrum { // 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 - // calling function. + // parent, calling function. async _utxosFromElectrumx (address) { try { // Convert the address to a scripthash. @@ -143,8 +143,10 @@ class Electrum { return electrumResponse } catch (err) { + // console.log('err: ', err) + // Write out error to error log. - wlogger.error('Error in elecrumx.js/_utxosFromElectrumx().') + wlogger.error('Error in elecrumx.js/_utxosFromElectrumx(): ', err) throw err } } @@ -191,7 +193,7 @@ class Electrum { // Get data from ElectrumX server. const electrumResponse = await _this._utxosFromElectrumx(cashAddr) - console.log(`electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}`) + // console.log(`electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}`) // Pass the error message if ElectrumX reports an error. if (Object.prototype.hasOwnProperty.call(electrumResponse, 'code')) { diff --git a/test/v3/electrumx.js b/test/v3/electrumx.js index 244a58a..bb174dd 100644 --- a/test/v3/electrumx.js +++ b/test/v3/electrumx.js @@ -163,22 +163,31 @@ describe('#ElectrumX Router', () => { assert.property(result, 'success') assert.equal(result.success, false) }) - }) - describe('#_utxosFromElectrumx', () => { - // Unit test only. - if (process.env.TEST === 'unit') { - it('should pass errors from ElectrumX to user', async () => { - req.params.address = - 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + it('should pass errors from ElectrumX to user', async () => { + // Address has invalid checksum. + req.params.address = + 'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2' + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { electrumxRoute.isReady = true // Force flag. sandbox .stub(electrumxRoute.electrumx, 'request') .resolves(mockData.utxos) - }) - } + } + + // Call the details API. + const result = await electrumxRoute.getUtxos(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, 'Unsupported address format') + }) it('should get balance for a single address', async () => { req.params.address = @@ -195,7 +204,7 @@ describe('#ElectrumX Router', () => { // Call the details API. const result = await electrumxRoute.getUtxos(req, res) - console.log(`result: ${JSON.stringify(result, null, 2)}`) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.property(result, 'success') assert.equal(result.success, true) @@ -209,4 +218,65 @@ describe('#ElectrumX Router', () => { assert.property(result.utxos[0], 'value') }) }) + + describe('#_utxosFromElectrumx', () => { + it('should throw error for invalid address', async () => { + try { + // Address has invalid checksum. + const address = 'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2' + + // Call the details API. + await electrumxRoute._utxosFromElectrumx(address) + + assert.equal(true, false, 'Unexpected code path') + } catch (err) { + assert.include(err.message, 'Invalid checksum') + } + }) + + it('should return empty array for address with no utxos', async () => { + // Address has invalid checksum. + const address = + 'bchtest:qqtmlpspjakqlvywae226esrcdrj9auynuwadh55uf' + + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute.electrumx, 'request') + .resolves([]) + } + + // Call the details API. + const result = await electrumxRoute._utxosFromElectrumx(address) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.equal(result.length, 0) + }) + + it('should get balance for a single address', async () => { + const address = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute.electrumx, 'request') + .resolves(mockData.utxos) + } + + // Call the details API. + const result = await electrumxRoute._utxosFromElectrumx(address) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.property(result[0], 'height') + assert.property(result[0], 'tx_hash') + assert.property(result[0], 'tx_pos') + assert.property(result[0], 'value') + }) + }) }) From f4e90a0518d56fb6d12a9f7d1d17dbb828dd7536 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 14 Apr 2020 09:55:36 -0700 Subject: [PATCH 05/12] fix(electrumx integration): Fixed the integration tests by renaming file --- src/routes/v3/electrumx.js | 6 ++++++ test/v3/{electrumx.js => a01-electrumx.js} | 25 +++++++++------------- test/v3/blockbook.js | 4 +++- test/v3/blockchain.js | 1 + 4 files changed, 20 insertions(+), 16 deletions(-) rename test/v3/{electrumx.js => a01-electrumx.js} (93%) diff --git a/src/routes/v3/electrumx.js b/src/routes/v3/electrumx.js index bc2b2be..a04d782 100644 --- a/src/routes/v3/electrumx.js +++ b/src/routes/v3/electrumx.js @@ -9,7 +9,9 @@ const router = express.Router() const axios = require('axios') const util = require('util') const bitcore = require('bitcore-lib-cash') + const ElectrumCash = require('electrum-cash').Client +// const ElectrumCash = require('/home/trout/work/personal/electrum-cash/electrum.js').Client // eslint-disable-line const wlogger = require('../../util/winston-logging') const config = require('../../../config') @@ -37,6 +39,8 @@ class Electrum { '1.4.1', process.env.FULCRUM_URL, process.env.FULCRUM_PORT + // '192.168.0.6', + // '50002' ) _this.isReady = false @@ -52,6 +56,8 @@ class Electrum { try { console.log('Attempting to connect to ElectrumX server...') + // console.log('_this.electrumx: ', _this.electrumx) + // Return immediately if a connection has already been established. if (_this.isReady) return true diff --git a/test/v3/electrumx.js b/test/v3/a01-electrumx.js similarity index 93% rename from test/v3/electrumx.js rename to test/v3/a01-electrumx.js index bb174dd..875a239 100644 --- a/test/v3/electrumx.js +++ b/test/v3/a01-electrumx.js @@ -1,6 +1,11 @@ /* TESTS FOR THE ELECTRUMX.JS LIBRARY + Named with a01 prefix so that these tests are run first. Something about running + the Blcokbook and Blockchain tests screws up these tests. Spend a couple hours + debugging and couldn't isolate the source of the issue, but renaming the file + was an easy fix. + This test file uses the environment variable TEST to switch between unit and integration tests. By default, TEST is set to 'unit'. Set this variable to 'integration' to run the tests against BCH mainnet. @@ -15,15 +20,6 @@ const assert = chai.assert const sinon = require('sinon') -let originalUrl // Used during transition from integration to unit tests. - -// Set default environment variables for unit tests. -if (!process.env.TEST) process.env.TEST = 'unit' -if (process.env.TEST === 'unit') { - process.env.BLOCKBOOK_URL = 'http://fakeurl/api/' -} - -// Only load blockbook library after setting BLOCKBOOK_URL env var. const ElecrumxRoute = require('../../src/routes/v3/electrumx') const electrumxRoute = new ElecrumxRoute() @@ -52,6 +48,8 @@ describe('#ElectrumX Router', () => { }) after(async () => { + // console.log(`electrumxRoute.electrumx: `, electrumxRoute.electrumx) + // Disconnect from the electrumx server if this is an integration test. if (process.env.TEST === 'integration') { await electrumxRoute.disconnect() @@ -78,7 +76,7 @@ describe('#ElectrumX Router', () => { }) after(() => { - process.env.BLOCKBOOK_URL = originalUrl + // }) describe('#root', () => { @@ -236,16 +234,13 @@ describe('#ElectrumX Router', () => { it('should return empty array for address with no utxos', async () => { // Address has invalid checksum. - const address = - 'bchtest:qqtmlpspjakqlvywae226esrcdrj9auynuwadh55uf' + const address = 'bchtest:qqtmlpspjakqlvywae226esrcdrj9auynuwadh55uf' // Mock unit tests to prevent live network calls. if (process.env.TEST === 'unit') { electrumxRoute.isReady = true // Force flag. - sandbox - .stub(electrumxRoute.electrumx, 'request') - .resolves([]) + sandbox.stub(electrumxRoute.electrumx, 'request').resolves([]) } // Call the details API. diff --git a/test/v3/blockbook.js b/test/v3/blockbook.js index 7b4ac71..eab7e70 100644 --- a/test/v3/blockbook.js +++ b/test/v3/blockbook.js @@ -15,7 +15,9 @@ const assert = chai.assert const sinon = require('sinon') -let originalUrl // Used during transition from integration to unit tests. +// Used during transition from integration to unit tests. +// let originalUrl +const originalUrl = process.env.BLOCKBOOK_URL // Set default environment variables for unit tests. if (!process.env.TEST) process.env.TEST = 'unit' diff --git a/test/v3/blockchain.js b/test/v3/blockchain.js index 0675499..fcebf9e 100644 --- a/test/v3/blockchain.js +++ b/test/v3/blockchain.js @@ -499,6 +499,7 @@ describe('#BlockchainRouter', () => { process.env.BITCOINCOM_BASEURL = savedUrl } }) + it('returns proper error when downstream service stalls', async () => { // Mock the timeout error. sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' }) From d6925164e02bccbf587cd435159447b5f7b759a0 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 16 Apr 2020 09:53:34 -0700 Subject: [PATCH 06/12] ElecrumX tests are passing --- package.json | 2 +- src/routes/v3/electrumx.js | 40 +++++++++++++++++++++-- test/v3/a01-electrumx.js | 57 ++++++++++++++++++++++++++++++--- test/v3/mocks/electrumx-mock.js | 16 +++++++-- 4 files changed, 106 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 48cbd24..12ee418 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dev": "nodemon ./dist/app.js", "test": "npm run lint && npm run test-v3", "lint": "standard --env mocha --fix", - "test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/", + "test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/a01-electrumx.js", "test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/blockchain.js", "test:integration": "mocha test/v3/integration", "coverage": "nyc report --reporter=text-lcov | coveralls", diff --git a/src/routes/v3/electrumx.js b/src/routes/v3/electrumx.js index a04d782..bcacb69 100644 --- a/src/routes/v3/electrumx.js +++ b/src/routes/v3/electrumx.js @@ -195,11 +195,14 @@ class Electrum { }) } - wlogger.debug('Executing electrumx/getUtxos with this address: ', cashAddr) + wlogger.debug( + 'Executing electrumx/getUtxos with this address: ', + cashAddr + ) // Get data from ElectrumX server. const electrumResponse = await _this._utxosFromElectrumx(cashAddr) - // console.log(`electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}`) + // console.log(`_utxosFromElectrumx(): ${JSON.stringify(electrumResponse, null, 2)}`) // Pass the error message if ElectrumX reports an error. if (Object.prototype.hasOwnProperty.call(electrumResponse, 'code')) { @@ -245,6 +248,39 @@ class Electrum { throw err } } + + // Returns a promise that resolves to a balance for an address. Expects input + // to be a cash address, and input validation to have already been done by + // parent, calling function. + async _balanceFromElectrumx (address) { + try { + // Convert the address to a scripthash. + const scripthash = _this.addressToScripthash(address) + + if (!_this.isReady) { + throw new Error( + 'ElectrumX server connection is not ready. Call await connectToServer() first.' + ) + } + + // Query the address balance from the ElectrumX server. + const electrumResponse = await _this.electrumx.request( + 'blockchain.scripthash.get_balance', + scripthash + ) + console.log( + `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}` + ) + + return electrumResponse + } catch (err) { + // console.log('err1: ', err) + + // Write out error to error log. + wlogger.error('Error in elecrumx.js/_utxosFromElectrumx(): ', err) + throw err + } + } } module.exports = Electrum diff --git a/test/v3/a01-electrumx.js b/test/v3/a01-electrumx.js index 875a239..9529d22 100644 --- a/test/v3/a01-electrumx.js +++ b/test/v3/a01-electrumx.js @@ -21,7 +21,6 @@ const assert = chai.assert const sinon = require('sinon') const ElecrumxRoute = require('../../src/routes/v3/electrumx') -const electrumxRoute = new ElecrumxRoute() // Mocking data. const { mockReq, mockRes } = require('./mocks/express-mocks') @@ -34,9 +33,11 @@ util.inspect.defaultOptions = { depth: 1 } describe('#ElectrumX Router', () => { let req, res let sandbox + const electrumxRoute = new ElecrumxRoute() before(async () => { - // console.log(`Testing type is: ${process.env.TEST}`) + if (!process.env.TEST) process.env.TEST = 'unit' + console.log(`Testing type is: ${process.env.TEST}`) if (!process.env.NETWORK) process.env.NETWORK = 'testnet' @@ -69,6 +70,8 @@ describe('#ElectrumX Router', () => { req.query = {} sandbox = sinon.createSandbox() + + // electrumxRoute = new ElecrumxRoute() }) afterEach(() => { @@ -103,7 +106,7 @@ describe('#ElectrumX Router', () => { }) }) - describe('#UTXO', () => { + describe('#getUtxos', () => { it('should throw 400 if address is empty', async () => { const result = await electrumxRoute.getUtxos(req, res) // console.log(`result: ${util.inspect(result)}`) @@ -196,7 +199,7 @@ describe('#ElectrumX Router', () => { electrumxRoute.isReady = true // Force flag. sandbox - .stub(electrumxRoute.electrumx, 'request') + .stub(electrumxRoute, '_utxosFromElectrumx') .resolves(mockData.utxos) } @@ -274,4 +277,50 @@ describe('#ElectrumX Router', () => { assert.property(result[0], 'value') }) }) + + describe('#_balanceFromElectrumx', () => { + it('should throw error for invalid address', async () => { + try { + // Address has invalid checksum. + const address = 'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2' + + // Mock unit tests to prevent live network calls. + // if (process.env.TEST === 'unit') { + // electrumxRoute.isReady = true // Force flag. + // + // sandbox + // .stub(electrumxRoute.electrumx, 'request') + // .throws('Invalid Argument: Invalid checksum:') + // } + + // Call the details API. + await electrumxRoute._balanceFromElectrumx(address) + + assert.equal(true, false, 'Unexpected code path') + } catch (err) { + // console.log('err2: ', err) + assert.include(err.message, 'Invalid checksum') + } + }) + + it('should get balance for a single address', async () => { + const address = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute.electrumx, 'request') + .resolves(mockData.balance) + } + + // Call the details API. + const result = await electrumxRoute._balanceFromElectrumx(address) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'confirmed') + assert.property(result, 'unconfirmed') + }) + }) }) diff --git a/test/v3/mocks/electrumx-mock.js b/test/v3/mocks/electrumx-mock.js index 76aaea1..067c83f 100644 --- a/test/v3/mocks/electrumx-mock.js +++ b/test/v3/mocks/electrumx-mock.js @@ -10,9 +10,21 @@ const utxos = [ tx_hash: '7774e449c5a3065144cefbc4c0c21e6b69c987f095856778ef9f45ddd8ae1a41', tx_pos: 0, value: 1000 + }, + { + height: 630834, + tx_hash: '4fe60a51e0d8f5134bfd8e5f872d6e502d7f01b28a6afebb27f4438a4f638d53', + tx_pos: 0, + value: 6000 } ] -module.exports = { - utxos +const balance = { + confirmed: 7000, + unconfirmed: 0 +} + +module.exports = { + utxos, + balance } From 7bb3741fa9d847e8d39f9e7f7d99b6d8908ee41a Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 16 Apr 2020 18:37:57 -0700 Subject: [PATCH 07/12] fix(ElectrumX): get methods for balance, utxos, and tx history compelte --- src/routes/v3/electrumx.js | 39 ++++++++++++++-- test/v3/a01-electrumx.js | 82 ++++++++++++++++++++++++++++++++- test/v3/mocks/electrumx-mock.js | 10 +++- 3 files changed, 125 insertions(+), 6 deletions(-) diff --git a/src/routes/v3/electrumx.js b/src/routes/v3/electrumx.js index bcacb69..9cdb111 100644 --- a/src/routes/v3/electrumx.js +++ b/src/routes/v3/electrumx.js @@ -268,9 +268,9 @@ class Electrum { 'blockchain.scripthash.get_balance', scripthash ) - console.log( - `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}` - ) + // console.log( + // `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}` + // ) return electrumResponse } catch (err) { @@ -281,6 +281,39 @@ class Electrum { throw err } } + + // 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 + // already been done by parent, calling function. + async _transactionsFromElectrumx (address) { + try { + // Convert the address to a scripthash. + const scripthash = _this.addressToScripthash(address) + + if (!_this.isReady) { + throw new Error( + 'ElectrumX server connection is not ready. Call await connectToServer() first.' + ) + } + + // Query the address transaction history from the ElectrumX server. + const electrumResponse = await _this.electrumx.request( + 'blockchain.scripthash.get_history', + scripthash + ) + // console.log( + // `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}` + // ) + + return electrumResponse + } catch (err) { + // console.log('err1: ', err) + + // Write out error to error log. + wlogger.error('Error in elecrumx.js/_transactionsFromElectrumx(): ', err) + throw err + } + } } module.exports = Electrum diff --git a/test/v3/a01-electrumx.js b/test/v3/a01-electrumx.js index 9529d22..eb6c984 100644 --- a/test/v3/a01-electrumx.js +++ b/test/v3/a01-electrumx.js @@ -255,7 +255,7 @@ describe('#ElectrumX Router', () => { }) it('should get balance for a single address', async () => { - const address = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + const address = 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7' // Mock unit tests to prevent live network calls. if (process.env.TEST === 'unit') { @@ -304,7 +304,27 @@ describe('#ElectrumX Router', () => { }) it('should get balance for a single address', async () => { - const address = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + const address = 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7' + + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute.electrumx, 'request') + .resolves(mockData.balance) + } + + // Call the details API. + const result = await electrumxRoute._balanceFromElectrumx(address) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'confirmed') + assert.property(result, 'unconfirmed') + }) + + it('should get balance for an address with no transaction history', async () => { + const address = 'bitcoincash:qp2ew6pvrs22jtsvtjyumjgas6jkvgn2hy3ad4wpw8' // Mock unit tests to prevent live network calls. if (process.env.TEST === 'unit') { @@ -323,4 +343,62 @@ describe('#ElectrumX Router', () => { assert.property(result, 'unconfirmed') }) }) + + describe('#_transactionsFromElectrumx', () => { + it('should throw error for invalid address', async () => { + try { + // Address has invalid checksum. + const address = 'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2' + + // Call the details API. + await electrumxRoute._transactionsFromElectrumx(address) + + assert.equal(true, false, 'Unexpected code path') + } catch (err) { + // console.log('err2: ', err) + assert.include(err.message, 'Invalid checksum') + } + }) + + it('should get transaction history for a single address', async () => { + const address = 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7' + + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute.electrumx, 'request') + .resolves(mockData.txHistory) + } + + // Call the details API. + const result = await electrumxRoute._transactionsFromElectrumx(address) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.property(result[0], 'height') + assert.property(result[0], 'tx_hash') + }) + + it('should get history for an address with no transaction history', async () => { + const address = 'bitcoincash:qp2ew6pvrs22jtsvtjyumjgas6jkvgn2hy3ad4wpw8' + + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute.electrumx, 'request') + .resolves([]) + } + + // Call the details API. + const result = await electrumxRoute._transactionsFromElectrumx(address) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.equal(result.length, 0) + }) + }) }) diff --git a/test/v3/mocks/electrumx-mock.js b/test/v3/mocks/electrumx-mock.js index 067c83f..65baf09 100644 --- a/test/v3/mocks/electrumx-mock.js +++ b/test/v3/mocks/electrumx-mock.js @@ -24,7 +24,15 @@ const balance = { unconfirmed: 0 } +const txHistory = [ + { + height: 601861, + tx_hash: '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d' + } +] + module.exports = { utxos, - balance + balance, + txHistory } From fb3641e7b247652e2d93396b3d092a83843f89a4 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 16 Apr 2020 18:52:40 -0700 Subject: [PATCH 08/12] fix(/electrumx/balance/:address): Implemented with unit and integration tests --- src/routes/v3/electrumx.js | 137 ++++++++++++++++------ test/v3/a01-electrumx.js | 226 +++++++++++++++++++++++++++---------- 2 files changed, 272 insertions(+), 91 deletions(-) diff --git a/src/routes/v3/electrumx.js b/src/routes/v3/electrumx.js index 9cdb111..586b1fb 100644 --- a/src/routes/v3/electrumx.js +++ b/src/routes/v3/electrumx.js @@ -49,6 +49,7 @@ class Electrum { _this.router = router _this.router.get('/', _this.root) _this.router.get('/utxos/:address', _this.getUtxos) + _this.router.get('/balance/:address', _this.getBalance) } // Initializes a connection to electrum servers. @@ -124,39 +125,6 @@ class Electrum { return res.json({ status: 'electrumx' }) } - // 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 - // parent, calling function. - async _utxosFromElectrumx (address) { - try { - // Convert the address to a scripthash. - const scripthash = _this.addressToScripthash(address) - - if (!_this.isReady) { - throw new Error( - 'ElectrumX server connection is not ready. Call await connectToServer() first.' - ) - } - - // Query the utxos from the ElectrumX server. - const electrumResponse = await _this.electrumx.request( - 'blockchain.scripthash.listunspent', - scripthash - ) - // console.log( - // `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}` - // ) - - return electrumResponse - } catch (err) { - // console.log('err: ', err) - - // Write out error to error log. - wlogger.error('Error in elecrumx.js/_utxosFromElectrumx(): ', err) - throw err - } - } - /** * @api {get} /electrumx/utxos/{addr} Get utxos for a single address. * @apiName UTXOs for a single address @@ -226,6 +194,109 @@ class Electrum { } } + /** + * @api {get} /electrumx/balance/{addr} Get balance for a single address. + * @apiName Balance for a single address + * @apiGroup ElectrumX / Fulcrum + * @apiDescription Returns an object with confirmed and unconfirmed balance associated with an address. + * + * + * @apiExample Example usage: + * curl -X GET "https://api.fullstack.cash/v3/electrumx/balance/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" + * + */ + // GET handler for single balance + async getBalance (req, res, next) { + try { + const address = req.params.address + + // Reject if address is an array. + if (Array.isArray(address)) { + res.status(400) + return res.json({ + success: false, + error: 'address can not be an array. Use POST for bulk upload.' + }) + } + + // Ensure the address is in cash address format. + const cashAddr = _this.bchjs.Address.toCashAddress(address) + + // Prevent a common user error. Ensure they are using the correct network address. + const networkIsValid = _this.routeUtils.validateNetwork(cashAddr) + if (!networkIsValid) { + res.status(400) + return res.json({ + success: false, + error: + 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' + }) + } + + wlogger.debug( + 'Executing electrumx/getBalance with this address: ', + cashAddr + ) + + // Get data from ElectrumX server. + const electrumResponse = await _this._balanceFromElectrumx(cashAddr) + // console.log(`_utxosFromElectrumx(): ${JSON.stringify(electrumResponse, null, 2)}`) + + // Pass the error message if ElectrumX reports an error. + if (Object.prototype.hasOwnProperty.call(electrumResponse, 'code')) { + res.status(400) + return res.json({ + success: false, + message: electrumResponse.message + }) + } + + res.status(200) + return res.json({ + success: true, + balance: electrumResponse + }) + } catch (err) { + // Write out error to error log. + wlogger.error('Error in elecrumx.js/getBalance().', 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 + // parent, calling function. + async _utxosFromElectrumx (address) { + try { + // Convert the address to a scripthash. + const scripthash = _this.addressToScripthash(address) + + if (!_this.isReady) { + throw new Error( + 'ElectrumX server connection is not ready. Call await connectToServer() first.' + ) + } + + // Query the utxos from the ElectrumX server. + const electrumResponse = await _this.electrumx.request( + 'blockchain.scripthash.listunspent', + scripthash + ) + // console.log( + // `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}` + // ) + + return electrumResponse + } catch (err) { + // console.log('err: ', err) + + // Write out error to error log. + wlogger.error('Error in elecrumx.js/_utxosFromElectrumx(): ', err) + throw err + } + } + // Convert a 'bitcoincash:...' address to a script hash used by ElectrumX. addressToScripthash (addrStr) { try { diff --git a/test/v3/a01-electrumx.js b/test/v3/a01-electrumx.js index eb6c984..7ddc945 100644 --- a/test/v3/a01-electrumx.js +++ b/test/v3/a01-electrumx.js @@ -106,6 +106,64 @@ describe('#ElectrumX Router', () => { }) }) + describe('#_utxosFromElectrumx', () => { + it('should throw error for invalid address', async () => { + try { + // Address has invalid checksum. + const address = 'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2' + + // Call the details API. + await electrumxRoute._utxosFromElectrumx(address) + + assert.equal(true, false, 'Unexpected code path') + } catch (err) { + assert.include(err.message, 'Invalid checksum') + } + }) + + it('should return empty array for address with no utxos', async () => { + // Address has invalid checksum. + const address = 'bchtest:qqtmlpspjakqlvywae226esrcdrj9auynuwadh55uf' + + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox.stub(electrumxRoute.electrumx, 'request').resolves([]) + } + + // Call the details API. + const result = await electrumxRoute._utxosFromElectrumx(address) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.equal(result.length, 0) + }) + + it('should get balance for a single address', async () => { + const address = 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7' + + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute.electrumx, 'request') + .resolves(mockData.utxos) + } + + // Call the details API. + const result = await electrumxRoute._utxosFromElectrumx(address) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.property(result[0], 'height') + assert.property(result[0], 'tx_hash') + assert.property(result[0], 'tx_pos') + assert.property(result[0], 'value') + }) + }) + describe('#getUtxos', () => { it('should throw 400 if address is empty', async () => { const result = await electrumxRoute.getUtxos(req, res) @@ -220,64 +278,6 @@ describe('#ElectrumX Router', () => { }) }) - describe('#_utxosFromElectrumx', () => { - it('should throw error for invalid address', async () => { - try { - // Address has invalid checksum. - const address = 'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2' - - // Call the details API. - await electrumxRoute._utxosFromElectrumx(address) - - assert.equal(true, false, 'Unexpected code path') - } catch (err) { - assert.include(err.message, 'Invalid checksum') - } - }) - - it('should return empty array for address with no utxos', async () => { - // Address has invalid checksum. - const address = 'bchtest:qqtmlpspjakqlvywae226esrcdrj9auynuwadh55uf' - - // Mock unit tests to prevent live network calls. - if (process.env.TEST === 'unit') { - electrumxRoute.isReady = true // Force flag. - - sandbox.stub(electrumxRoute.electrumx, 'request').resolves([]) - } - - // Call the details API. - const result = await electrumxRoute._utxosFromElectrumx(address) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.equal(result.length, 0) - }) - - it('should get balance for a single address', async () => { - const address = 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7' - - // Mock unit tests to prevent live network calls. - if (process.env.TEST === 'unit') { - electrumxRoute.isReady = true // Force flag. - - sandbox - .stub(electrumxRoute.electrumx, 'request') - .resolves(mockData.utxos) - } - - // Call the details API. - const result = await electrumxRoute._utxosFromElectrumx(address) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.property(result[0], 'height') - assert.property(result[0], 'tx_hash') - assert.property(result[0], 'tx_pos') - assert.property(result[0], 'value') - }) - }) - describe('#_balanceFromElectrumx', () => { it('should throw error for invalid address', async () => { try { @@ -344,6 +344,116 @@ describe('#ElectrumX Router', () => { }) }) + describe('#getBalance', () => { + it('should throw 400 if address is empty', async () => { + const result = await electrumxRoute.getBalance(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, 'Unsupported address format') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should throw 400 on array input', async () => { + req.params.address = ['qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'] + + const result = await electrumxRoute.getBalance(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, 'address can not be an array') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should throw an error for an invalid address', async () => { + req.params.address = '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' + + const result = await electrumxRoute.getBalance(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, 'Unsupported address format') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should detect a network mismatch', async () => { + req.params.address = 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' + + const result = await electrumxRoute.getBalance(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 ElectrumX to user', async () => { + // Address has invalid checksum. + req.params.address = + 'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2' + + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute.electrumx, 'request') + .resolves(mockData.balance) + } + + // Call the details API. + const result = await electrumxRoute.getBalance(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, 'Unsupported address format') + }) + + it('should get balance for a single address', async () => { + req.params.address = + 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7' + + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute, '_balanceFromElectrumx') + .resolves(mockData.balance) + } + + // Call the details API. + const result = await electrumxRoute.getBalance(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'success') + assert.equal(result.success, true) + + assert.property(result, 'balance') + assert.property(result.balance, 'confirmed') + assert.property(result.balance, 'unconfirmed') + }) + }) + describe('#_transactionsFromElectrumx', () => { it('should throw error for invalid address', async () => { try { From 373f538c8d08b9b52f8e192ec1eb05154d06fb6d Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 16 Apr 2020 19:09:58 -0700 Subject: [PATCH 09/12] fix(/electrumx/transactions/:address): Implemented with unit and integration tests --- src/routes/v3/electrumx.js | 203 +++++++++++++++++++++++++------------ test/v3/a01-electrumx.js | 113 +++++++++++++++++++-- 2 files changed, 240 insertions(+), 76 deletions(-) diff --git a/src/routes/v3/electrumx.js b/src/routes/v3/electrumx.js index 586b1fb..f7c4c2e 100644 --- a/src/routes/v3/electrumx.js +++ b/src/routes/v3/electrumx.js @@ -50,6 +50,7 @@ class Electrum { _this.router.get('/', _this.root) _this.router.get('/utxos/:address', _this.getUtxos) _this.router.get('/balance/:address', _this.getBalance) + _this.router.get('/transactions/:address', _this.getTransactions) } // Initializes a connection to electrum servers. @@ -125,6 +126,39 @@ class Electrum { return res.json({ status: 'electrumx' }) } + // 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 + // parent, calling function. + async _utxosFromElectrumx (address) { + try { + // Convert the address to a scripthash. + const scripthash = _this.addressToScripthash(address) + + if (!_this.isReady) { + throw new Error( + 'ElectrumX server connection is not ready. Call await connectToServer() first.' + ) + } + + // Query the utxos from the ElectrumX server. + const electrumResponse = await _this.electrumx.request( + 'blockchain.scripthash.listunspent', + scripthash + ) + // console.log( + // `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}` + // ) + + return electrumResponse + } catch (err) { + // console.log('err: ', err) + + // Write out error to error log. + wlogger.error('Error in elecrumx.js/_utxosFromElectrumx(): ', err) + throw err + } + } + /** * @api {get} /electrumx/utxos/{addr} Get utxos for a single address. * @apiName UTXOs for a single address @@ -194,6 +228,39 @@ class Electrum { } } + // Returns a promise that resolves to a balance for an address. Expects input + // to be a cash address, and input validation to have already been done by + // parent, calling function. + async _balanceFromElectrumx (address) { + try { + // Convert the address to a scripthash. + const scripthash = _this.addressToScripthash(address) + + if (!_this.isReady) { + throw new Error( + 'ElectrumX server connection is not ready. Call await connectToServer() first.' + ) + } + + // Query the address balance from the ElectrumX server. + const electrumResponse = await _this.electrumx.request( + 'blockchain.scripthash.get_balance', + scripthash + ) + // console.log( + // `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}` + // ) + + return electrumResponse + } catch (err) { + // console.log('err1: ', err) + + // Write out error to error log. + wlogger.error('Error in elecrumx.js/_utxosFromElectrumx(): ', err) + throw err + } + } + /** * @api {get} /electrumx/balance/{addr} Get balance for a single address. * @apiName Balance for a single address @@ -264,39 +331,6 @@ class Electrum { } } - // 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 - // parent, calling function. - async _utxosFromElectrumx (address) { - try { - // Convert the address to a scripthash. - const scripthash = _this.addressToScripthash(address) - - if (!_this.isReady) { - throw new Error( - 'ElectrumX server connection is not ready. Call await connectToServer() first.' - ) - } - - // Query the utxos from the ElectrumX server. - const electrumResponse = await _this.electrumx.request( - 'blockchain.scripthash.listunspent', - scripthash - ) - // console.log( - // `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}` - // ) - - return electrumResponse - } catch (err) { - // console.log('err: ', err) - - // Write out error to error log. - wlogger.error('Error in elecrumx.js/_utxosFromElectrumx(): ', err) - throw err - } - } - // Convert a 'bitcoincash:...' address to a script hash used by ElectrumX. addressToScripthash (addrStr) { try { @@ -320,39 +354,6 @@ class Electrum { } } - // Returns a promise that resolves to a balance for an address. Expects input - // to be a cash address, and input validation to have already been done by - // parent, calling function. - async _balanceFromElectrumx (address) { - try { - // Convert the address to a scripthash. - const scripthash = _this.addressToScripthash(address) - - if (!_this.isReady) { - throw new Error( - 'ElectrumX server connection is not ready. Call await connectToServer() first.' - ) - } - - // Query the address balance from the ElectrumX server. - const electrumResponse = await _this.electrumx.request( - 'blockchain.scripthash.get_balance', - scripthash - ) - // console.log( - // `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}` - // ) - - return electrumResponse - } catch (err) { - // console.log('err1: ', err) - - // Write out error to error log. - wlogger.error('Error in elecrumx.js/_utxosFromElectrumx(): ', err) - throw err - } - } - // 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 // already been done by parent, calling function. @@ -385,6 +386,76 @@ class Electrum { throw err } } + + /** + * @api {get} /electrumx/transactions/{addr} Get transaction history for a single address. + * @apiName Transaction history for a single address + * @apiGroup ElectrumX / Fulcrum + * @apiDescription Returns an array of historical transactions associated with an address. + * + * + * @apiExample Example usage: + * curl -X GET "https://api.fullstack.cash/v3/electrumx/transactions/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" + * + */ + // GET handler for single balance + async getTransactions (req, res, next) { + try { + const address = req.params.address + + // Reject if address is an array. + if (Array.isArray(address)) { + res.status(400) + return res.json({ + success: false, + error: 'address can not be an array. Use POST for bulk upload.' + }) + } + + // Ensure the address is in cash address format. + const cashAddr = _this.bchjs.Address.toCashAddress(address) + + // Prevent a common user error. Ensure they are using the correct network address. + const networkIsValid = _this.routeUtils.validateNetwork(cashAddr) + if (!networkIsValid) { + res.status(400) + return res.json({ + success: false, + error: + 'Invalid network. Trying to use a testnet address on mainnet, or vice versa.' + }) + } + + wlogger.debug( + 'Executing electrumx/getTransactions with this address: ', + cashAddr + ) + + // Get data from ElectrumX server. + const electrumResponse = await _this._transactionsFromElectrumx(cashAddr) + // console.log(`_utxosFromElectrumx(): ${JSON.stringify(electrumResponse, null, 2)}`) + + // Pass the error message if ElectrumX reports an error. + if (Object.prototype.hasOwnProperty.call(electrumResponse, 'code')) { + res.status(400) + return res.json({ + success: false, + message: electrumResponse.message + }) + } + + res.status(200) + return res.json({ + success: true, + transactions: electrumResponse + }) + } catch (err) { + // Write out error to error log. + wlogger.error('Error in elecrumx.js/getTransactions().', err) + + return _this.errorHandler(err, res) + } + } } module.exports = Electrum diff --git a/test/v3/a01-electrumx.js b/test/v3/a01-electrumx.js index 7ddc945..ed9ca4b 100644 --- a/test/v3/a01-electrumx.js +++ b/test/v3/a01-electrumx.js @@ -403,20 +403,11 @@ describe('#ElectrumX Router', () => { assert.equal(result.success, false) }) - it('should pass errors from ElectrumX to user', async () => { + it('should pass errors from electrum-cash to user', async () => { // Address has invalid checksum. req.params.address = 'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2' - // Mock unit tests to prevent live network calls. - if (process.env.TEST === 'unit') { - electrumxRoute.isReady = true // Force flag. - - sandbox - .stub(electrumxRoute.electrumx, 'request') - .resolves(mockData.balance) - } - // Call the details API. const result = await electrumxRoute.getBalance(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) @@ -511,4 +502,106 @@ describe('#ElectrumX Router', () => { assert.equal(result.length, 0) }) }) + + describe('#getTransactions', () => { + it('should throw 400 if address is empty', async () => { + const result = await electrumxRoute.getTransactions(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, 'Unsupported address format') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should throw 400 on array input', async () => { + req.params.address = ['qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'] + + const result = await electrumxRoute.getTransactions(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, 'address can not be an array') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should throw an error for an invalid address', async () => { + req.params.address = '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' + + const result = await electrumxRoute.getTransactions(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, 'Unsupported address format') + + assert.property(result, 'success') + assert.equal(result.success, false) + }) + + it('should detect a network mismatch', async () => { + req.params.address = 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' + + const result = await electrumxRoute.getTransactions(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 ElectrumX to user', async () => { + // Address has invalid checksum. + req.params.address = + 'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2' + + // Call the details API. + const result = await electrumxRoute.getTransactions(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, 'Unsupported address format') + }) + + it('should get transactions for a single address', async () => { + req.params.address = + 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7' + + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute, '_transactionsFromElectrumx') + .resolves(mockData.txHistory) + } + + // Call the details API. + const result = await electrumxRoute.getTransactions(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'success') + assert.equal(result.success, true) + + assert.property(result, 'transactions') + assert.isArray(result.transactions) + assert.property(result.transactions[0], 'height') + assert.property(result.transactions[0], 'tx_hash') + }) + }) }) From 9cb33f96f8916138892d7756a7ef945685e9d063 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 16 Apr 2020 19:57:41 -0700 Subject: [PATCH 10/12] fix(POST electrumx/utxos): Implemented with unit and integration tests --- src/routes/v3/blockbook.js | 2 +- src/routes/v3/electrumx.js | 92 +++++++++++++++++++++++++ test/v3/a01-electrumx.js | 134 +++++++++++++++++++++++++++++++++++++ test/v3/blockbook.js | 15 +++-- 4 files changed, 235 insertions(+), 8 deletions(-) diff --git a/src/routes/v3/blockbook.js b/src/routes/v3/blockbook.js index 8ddb68c..7b5bd07 100644 --- a/src/routes/v3/blockbook.js +++ b/src/routes/v3/blockbook.js @@ -192,7 +192,7 @@ class Blockbook { } // Enforce array size rate limits - if (!routeUtils.validateArraySize(req, addresses)) { + if (!_this.routeUtils.validateArraySize(req, addresses)) { res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 return res.json({ error: 'Array too large.' diff --git a/src/routes/v3/electrumx.js b/src/routes/v3/electrumx.js index f7c4c2e..976705a 100644 --- a/src/routes/v3/electrumx.js +++ b/src/routes/v3/electrumx.js @@ -49,6 +49,7 @@ class Electrum { _this.router = router _this.router.get('/', _this.root) _this.router.get('/utxos/:address', _this.getUtxos) + _this.router.post('/utxos', _this.utxosBulk) _this.router.get('/balance/:address', _this.getBalance) _this.router.get('/transactions/:address', _this.getTransactions) } @@ -228,6 +229,97 @@ class Electrum { } } + /** + * @api {post} /electrumx/utxo Get utxos for an array of addresses. + * @apiName UTXOs for an array of addresses + * @apiGroup ElectrumX / Fulcrum + * @apiDescription Returns an array of objects with UTXOs associated with an address. + * Limited to 20 items per request. + * + * @apiExample Example usage: + * curl -X POST "https://api.fullstack.cash/v3/electrumx/utxos" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' + * + * + */ + // POST handler for bulk queries on address details + async utxosBulk (req, res, next) { + try { + let addresses = req.body.addresses + // const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0 + + // 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(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: 'Array too large.' + }) + } + + wlogger.debug( + 'Executing electrumx.js/utxoBulk 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 + // Insight API in parallel. + addresses = addresses.map(async (address, index) => { + // console.log(`address: ${address}`) + const utxos = await _this._utxosFromElectrumx(address) + + return { + utxos, + 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, + utxos: result + }) + } catch (err) { + wlogger.error('Error in electrumx.js/utxoBulk().', err) + + return _this.errorHandler(err, res) + } + } + // Returns a promise that resolves to a balance for an address. Expects input // to be a cash address, and input validation to have already been done by // parent, calling function. diff --git a/test/v3/a01-electrumx.js b/test/v3/a01-electrumx.js index ed9ca4b..2099211 100644 --- a/test/v3/a01-electrumx.js +++ b/test/v3/a01-electrumx.js @@ -278,6 +278,140 @@ describe('#ElectrumX Router', () => { }) }) + describe('#utxosBulk', () => { + it('should throw an error for an empty body', async () => { + req.body = {} + + const result = await electrumxRoute.utxosBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + assert.include( + result.error, + 'addresses needs to be an array', + 'Proper error message' + ) + }) + + it('should error on non-array single address', async () => { + req.body = { + address: 'qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' + } + + const result = await electrumxRoute.utxosBulk(req, res) + + assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + assert.include( + result.error, + 'addresses needs to be an array', + 'Proper error message' + ) + }) + + it('should throw an error for an invalid address', async () => { + req.body = { + addresses: ['02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'] + } + + const result = await electrumxRoute.utxosBulk(req, res) + + assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + assert.include( + result.error, + 'Invalid BCH address', + 'Proper error message' + ) + }) + + 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.utxosBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'Array too large') + }) + + it('should detect a network mismatch', async () => { + req.body = { + addresses: ['bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4'] + } + + const result = await electrumxRoute.utxosBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + assert.include(result.error, 'Invalid network', 'Proper error message') + }) + + it('should get details for a single address', async () => { + req.body = { + addresses: ['bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute, '_utxosFromElectrumx') + .resolves(mockData.utxos) + } + + // Call the details API. + const result = await electrumxRoute.utxosBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'success') + assert.equal(result.success, true) + + assert.property(result, 'utxos') + assert.isArray(result.utxos) + + assert.property(result.utxos[0], 'address') + assert.property(result.utxos[0], 'utxos') + + assert.isArray(result.utxos[0].utxos) + assert.property(result.utxos[0].utxos[0], 'height') + assert.property(result.utxos[0].utxos[0], 'tx_hash') + assert.property(result.utxos[0].utxos[0], 'tx_pos') + assert.property(result.utxos[0].utxos[0], 'value') + }) + + it('should get utxos for multiple addresses', async () => { + req.body = { + addresses: [ + 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf', + 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute, '_utxosFromElectrumx') + .resolves(mockData.utxos) + } + + // Call the details API. + const result = await electrumxRoute.utxosBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'success') + assert.equal(result.success, true) + + assert.isArray(result.utxos) + assert.isArray(result.utxos[0].utxos) + assert.equal(result.utxos.length, 2, '2 outputs for 2 inputs') + }) + }) + describe('#_balanceFromElectrumx', () => { it('should throw error for invalid address', async () => { try { diff --git a/test/v3/blockbook.js b/test/v3/blockbook.js index eab7e70..90a970e 100644 --- a/test/v3/blockbook.js +++ b/test/v3/blockbook.js @@ -105,8 +105,7 @@ describe('#Blockbook Router', () => { }) it('should throw an error for an invalid address', async () => { - req.params.address = - '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' + req.params.address = '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' const result = await blockbookRoute.balanceSingle(req, res) @@ -119,8 +118,7 @@ describe('#Blockbook Router', () => { }) it('should detect a network mismatch', async () => { - req.params.address = - 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' + req.params.address = 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' const result = await blockbookRoute.balanceSingle(req, res) @@ -458,8 +456,7 @@ describe('#Blockbook Router', () => { }) it('should detect a network mismatch', async () => { - req.params.address = - 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' + req.params.address = 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' const result = await utxosSingle(req, res) @@ -654,6 +651,7 @@ describe('#Blockbook Router', () => { process.env.BLOCKBOOK_URL = savedUrl } }) + it('returns proper error when downstream service stalls', async () => { req.body = { addresses: ['bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'] @@ -674,6 +672,7 @@ describe('#Blockbook Router', () => { 'Error message expected' ) }) + it('returns proper error when downstream service is down', async () => { req.body = { addresses: ['bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'] @@ -694,6 +693,7 @@ describe('#Blockbook Router', () => { 'Error message expected' ) }) + it('should get details for a single address', async () => { req.body = { addresses: ['bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'] @@ -787,7 +787,8 @@ describe('#Blockbook Router', () => { const savedUrl = process.env.BLOCKBOOK_URL try { - req.params.txid = '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d' + req.params.txid = + '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d' // Switch the Insight URL to something that will error out. process.env.BLOCKBOOK_URL = 'http://fakeurl/api/' From 9172c5ceb0c753fa00b49b8bfa4a92ab05569f94 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 16 Apr 2020 20:26:54 -0700 Subject: [PATCH 11/12] fix(POST electrumx/balance): Implemented with unit and integration tests --- src/routes/v3/electrumx.js | 119 +++++++++++++++++++++++++++++---- test/v3/a01-electrumx.js | 130 +++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 14 deletions(-) diff --git a/src/routes/v3/electrumx.js b/src/routes/v3/electrumx.js index 976705a..79d9001 100644 --- a/src/routes/v3/electrumx.js +++ b/src/routes/v3/electrumx.js @@ -51,6 +51,7 @@ class Electrum { _this.router.get('/utxos/:address', _this.getUtxos) _this.router.post('/utxos', _this.utxosBulk) _this.router.get('/balance/:address', _this.getBalance) + _this.router.post('/balance', _this.balanceBulk) _this.router.get('/transactions/:address', _this.getTransactions) } @@ -423,26 +424,93 @@ class Electrum { } } - // Convert a 'bitcoincash:...' address to a script hash used by ElectrumX. - addressToScripthash (addrStr) { + /** + * @api {post} /electrumx/balance Get balances for an array of addresses. + * @apiName Balances for an array of addresses + * @apiGroup ElectrumX / Fulcrum + * @apiDescription Returns an array of balanes associated with an array of address. + * Limited to 20 items per request. + * + * @apiExample Example usage: + * curl -X POST "https://api.fullstack.cash/v3/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 { - // console.log(`addrStr: ${addrStr}`) + let addresses = req.body.addresses - const address = _this.bitcore.Address.fromString(addrStr) - // console.log(`address: ${address}`) + // 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.' + }) + } - const script = _this.bitcore.Script.buildPublicKeyHashOut(address) - // console.log(`script: ${script}`) + // Enforce array size rate limits + if (!_this.routeUtils.validateArraySize(req, addresses)) { + res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: 'Array too large.' + }) + } - const scripthash = _this.bitcore.crypto.Hash.sha256(script.toBuffer()) - .reverse() - .toString('hex') - // console.log(`scripthash: ${scripthash}`) + wlogger.debug( + 'Executing electrumx.js/balanceBulk with these addresses: ', + addresses + ) - return scripthash + // 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/addressToScripthash()') - throw err + wlogger.error('Error in electrumx.js/balanceBulk().', err) + + return _this.errorHandler(err, res) } } @@ -548,6 +616,29 @@ class Electrum { return _this.errorHandler(err, res) } } + + // Convert a 'bitcoincash:...' address to a script hash used by ElectrumX. + addressToScripthash (addrStr) { + try { + // console.log(`addrStr: ${addrStr}`) + + const address = _this.bitcore.Address.fromString(addrStr) + // console.log(`address: ${address}`) + + const script = _this.bitcore.Script.buildPublicKeyHashOut(address) + // console.log(`script: ${script}`) + + const scripthash = _this.bitcore.crypto.Hash.sha256(script.toBuffer()) + .reverse() + .toString('hex') + // console.log(`scripthash: ${scripthash}`) + + return scripthash + } catch (err) { + wlogger.error('Error in electrumx.js/addressToScripthash()') + throw err + } + } } module.exports = Electrum diff --git a/test/v3/a01-electrumx.js b/test/v3/a01-electrumx.js index 2099211..6fa8c79 100644 --- a/test/v3/a01-electrumx.js +++ b/test/v3/a01-electrumx.js @@ -579,6 +579,136 @@ describe('#ElectrumX Router', () => { }) }) + describe('#balanceBulk', () => { + it('should throw an error for an empty body', async () => { + req.body = {} + + const result = await electrumxRoute.balanceBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + assert.include( + result.error, + 'addresses needs to be an array', + 'Proper error message' + ) + }) + + it('should error on non-array single address', async () => { + req.body = { + address: 'qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' + } + + const result = await electrumxRoute.balanceBulk(req, res) + + assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + assert.include( + result.error, + 'addresses needs to be an array', + 'Proper error message' + ) + }) + + it('should throw an error for an invalid address', async () => { + req.body = { + addresses: ['02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'] + } + + const result = await electrumxRoute.balanceBulk(req, res) + + assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + assert.include( + result.error, + 'Invalid BCH address', + 'Proper error message' + ) + }) + + 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.hasAllKeys(result, ['error']) + assert.include(result.error, 'Array too large') + }) + + 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, 'HTTP status code 400 expected.') + assert.include(result.error, 'Invalid network', 'Proper error message') + }) + + it('should get details for a single address', async () => { + req.body = { + addresses: ['bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute, '_balanceFromElectrumx') + .resolves(mockData.balance) + } + + // 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.isArray(result.balances) + + assert.property(result.balances[0], 'address') + assert.property(result.balances[0], 'balance') + + assert.property(result.balances[0].balance, 'confirmed') + assert.property(result.balances[0].balance, 'unconfirmed') + }) + + it('should get utxos for multiple addresses', async () => { + req.body = { + addresses: [ + 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf', + 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute, '_balanceFromElectrumx') + .resolves(mockData.balance) + } + + // 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.isArray(result.balances) + assert.equal(result.balances.length, 2, '2 outputs for 2 inputs') + }) + }) + describe('#_transactionsFromElectrumx', () => { it('should throw error for invalid address', async () => { try { From 492e10b4406e809ffdfca9edeeb55ad240152d37 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 16 Apr 2020 20:41:08 -0700 Subject: [PATCH 12/12] fix(POST electrumx/transactions): Implemented with unit and integration tests --- package.json | 2 +- src/routes/v3/electrumx.js | 91 ++++++++++++++++++++++++++ test/v3/a01-electrumx.js | 131 +++++++++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 12ee418..48cbd24 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dev": "nodemon ./dist/app.js", "test": "npm run lint && npm run test-v3", "lint": "standard --env mocha --fix", - "test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/a01-electrumx.js", + "test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/", "test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/blockchain.js", "test:integration": "mocha test/v3/integration", "coverage": "nyc report --reporter=text-lcov | coveralls", diff --git a/src/routes/v3/electrumx.js b/src/routes/v3/electrumx.js index 79d9001..de0ae3b 100644 --- a/src/routes/v3/electrumx.js +++ b/src/routes/v3/electrumx.js @@ -53,6 +53,7 @@ class Electrum { _this.router.get('/balance/:address', _this.getBalance) _this.router.post('/balance', _this.balanceBulk) _this.router.get('/transactions/:address', _this.getTransactions) + _this.router.post('/transactions', _this.transactionsBulk) } // Initializes a connection to electrum servers. @@ -617,6 +618,96 @@ class Electrum { } } + /** + * @api {post} /electrumx/transactions Get the transaction history for an array of addresses. + * @apiName Transactions for an array of addresses + * @apiGroup ElectrumX / Fulcrum + * @apiDescription Returns an array of transactions associated with an array of address. + * Limited to 20 items per request. + * + * @apiExample Example usage: + * curl -X POST "https://api.fullstack.cash/v3/electrumx/transactions" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' + * + * + */ + // POST handler for bulk queries on transaction histories for addresses. + async transactionsBulk (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(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 + return res.json({ + error: 'Array too large.' + }) + } + + wlogger.debug( + 'Executing electrumx.js/transactionsBulk 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 transactions = await _this._transactionsFromElectrumx(address) + + return { + transactions, + 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, + transactions: result + }) + } catch (err) { + wlogger.error('Error in electrumx.js/transactionsBulk().', err) + + return _this.errorHandler(err, res) + } + } + // Convert a 'bitcoincash:...' address to a script hash used by ElectrumX. addressToScripthash (addrStr) { try { diff --git a/test/v3/a01-electrumx.js b/test/v3/a01-electrumx.js index 6fa8c79..17d539d 100644 --- a/test/v3/a01-electrumx.js +++ b/test/v3/a01-electrumx.js @@ -868,4 +868,135 @@ describe('#ElectrumX Router', () => { assert.property(result.transactions[0], 'tx_hash') }) }) + + describe('#balanceBulk', () => { + it('should throw an error for an empty body', async () => { + req.body = {} + + const result = await electrumxRoute.transactionsBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + assert.include( + result.error, + 'addresses needs to be an array', + 'Proper error message' + ) + }) + + it('should error on non-array single address', async () => { + req.body = { + address: 'qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' + } + + const result = await electrumxRoute.transactionsBulk(req, res) + + assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + assert.include( + result.error, + 'addresses needs to be an array', + 'Proper error message' + ) + }) + + it('should throw an error for an invalid address', async () => { + req.body = { + addresses: ['02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'] + } + + const result = await electrumxRoute.transactionsBulk(req, res) + + assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + assert.include( + result.error, + 'Invalid BCH address', + 'Proper error message' + ) + }) + + 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.transactionsBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ['error']) + assert.include(result.error, 'Array too large') + }) + + it('should detect a network mismatch', async () => { + req.body = { + addresses: ['bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4'] + } + + const result = await electrumxRoute.transactionsBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + assert.include(result.error, 'Invalid network', 'Proper error message') + }) + + it('should get details for a single address', async () => { + req.body = { + addresses: ['bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute, '_transactionsFromElectrumx') + .resolves(mockData.txHistory) + } + + // Call the details API. + const result = await electrumxRoute.transactionsBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'success') + assert.equal(result.success, true) + + assert.property(result, 'transactions') + assert.isArray(result.transactions) + + assert.property(result.transactions[0], 'address') + assert.property(result.transactions[0], 'transactions') + + assert.isArray(result.transactions[0].transactions) + assert.property(result.transactions[0].transactions[0], 'height') + assert.property(result.transactions[0].transactions[0], 'tx_hash') + }) + + it('should get utxos for multiple addresses', async () => { + req.body = { + addresses: [ + 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf', + 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. + + sandbox + .stub(electrumxRoute, '_transactionsFromElectrumx') + .resolves(mockData.txHistory) + } + + // Call the details API. + const result = await electrumxRoute.transactionsBulk(req, res) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'success') + assert.equal(result.success, true) + + assert.isArray(result.transactions) + assert.equal(result.transactions.length, 2, '2 outputs for 2 inputs') + }) + }) })