diff --git a/config/electrumx.js b/config/electrumx.js new file mode 100644 index 0000000..17e9dce --- /dev/null +++ b/config/electrumx.js @@ -0,0 +1,29 @@ +/* + 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 + } +} + +module.exports = config diff --git a/config/index.js b/config/index.js index e26ad1a..0f7087d 100644 --- a/config/index.js +++ b/config/index.js @@ -2,6 +2,11 @@ Common configuration settings. */ -module.exports = { - apiTokenSecret: process.env.TOKENSECRET ? process.env.TOKENSECRET : 'secret-jwt-token' +const electrumxConfig = require('./electrumx') + +const config = { + apiTokenSecret: process.env.TOKENSECRET ? process.env.TOKENSECRET : 'secret-jwt-token', + electrumx: electrumxConfig } + +module.exports = config diff --git a/src/routes/v3/electrumx.js b/src/routes/v3/electrumx.js index fbb7a18..2cef7ac 100644 --- a/src/routes/v3/electrumx.js +++ b/src/routes/v3/electrumx.js @@ -9,8 +9,10 @@ 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 wlogger = require('../../util/winston-logging') +const config = require('../../../config') const RouteUtils = require('../../util/route-utils') const routeUtils = new RouteUtils() @@ -24,11 +26,30 @@ class Electrum { constructor () { _this = this + _this.config = config _this.axios = axios _this.routeUtils = routeUtils _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, + config.electrumx.electrum.serverUrl, + config.electrumx.electrum.serverPort + ) + + _this.isReady = false + // _this.connectToServers() + _this.router = router _this.router.get('/', _this.root) // _this.router.get('/balance/:address', _this.balanceSingle) @@ -39,6 +60,50 @@ class Electrum { // _this.router.post('/tx', _this.txBulk) } + // Initializes a connection to electrum servers. + async connect () { + try { + console.log('Entering connectToServers()') + + // Return immediately if a connection has already been established. + if (_this.isReady) return true + + // Connect to the server. + await _this.electrumx.connect() + + // Set the connection flag. + _this.isReady = true + + // console.log(`_this.isReady: ${_this.isReady}`) + return _this.isReady + } catch (err) { + // console.log(`err: `, err) + wlogger.error('Error in electrumx.js/connect()') + throw err + } + } + + // Disconnect from the ElectrumX server. + async disconnect () { + try { + // Return immediately if the isReady flag is false. + if (!_this.isReady) return true + + // Disconnect from the server. + await _this.electrumx.disconnect() + + // Clear the isReady flag. + _this.isReady = false + + // Return true to signal that the disconnection happened successfully. + return true + } catch (err) { + // console.log(`err: `, err) + wlogger.error('Error in electrumx.js/disconnect()') + throw err + } + } + // DRY error handler. errorHandler (err, res) { // Attempt to decode the error message. @@ -159,42 +224,48 @@ class Electrum { async getUtxos (req, res, next) { try { - let scripthash = '' // Default value + const address = _this.bchjs.Address.toCashAddress(req.params.address) - scripthash = _this.addressToScripthash(req.params.address) + wlogger.debug('Executing electrumx/getUtxos with this address: ', address) + + // 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. + var electrumResponse = await _this.electrumx.request( + 'blockchain.scripthash.listunspent', + scripthash + ) + // console.log( + // `electrumResponse: ${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(scripthash) + return res.json({ + success: true, + utxos: electrumResponse + }) } catch (err) { // Write out error to error log. wlogger.error('Error in elecrumx.js/getUtxos().', err) return _this.errorHandler(err, res) } - - // try { - // var electrumResponse = await electrum.request( - // 'blockchain.scripthash.listunspent', - // scripthash - // ) - // } catch (e) { - // return res.status(500).send({ - // success: false, - // message: e.message - // }) - // } - // - // if (electrumResponse.hasOwnProperty('code')) { - // return res.status(400).send({ - // success: false, - // message: electrumResponse.message - // }) - // } - // - // return res.send({ - // success: true, - // utxos: electrumResponse - // }) } // Convert a 'bitcoincash:...' address to a script hash used by ElectrumX. diff --git a/test/v3/electrumx.js b/test/v3/electrumx.js index ffdbe40..f8eb715 100644 --- a/test/v3/electrumx.js +++ b/test/v3/electrumx.js @@ -29,19 +29,34 @@ const electrumxRoute = new ElecrumxRoute() // Mocking data. const { mockReq, mockRes } = require('./mocks/express-mocks') -// const mockData = require('./mocks/blockbook-mock') +const mockData = require('./mocks/electrumx-mock') // Used for debugging. const util = require('util') util.inspect.defaultOptions = { depth: 1 } -describe('#Blockbook Router', () => { +describe('#ElectrumX Router', () => { let req, res let sandbox - before(() => { + + before(async () => { // console.log(`Testing type is: ${process.env.TEST}`) if (!process.env.NETWORK) process.env.NETWORK = 'testnet' + + // Connect to electrumx servers if this is an integration test. + if (process.env.TEST === 'integration') { + await electrumxRoute.connect() + console.log('Connected to ElectrumX server') + } + }) + + after(async () => { + // Disconnect from the electrumx server if this is an integration test. + if (process.env.TEST === 'integration') { + await electrumxRoute.disconnect() + console.log('Disconnected from ElectrumX server') + } }) // Setup the mocks before each test. @@ -83,7 +98,8 @@ describe('#Blockbook Router', () => { const scripthash = electrumxRoute.addressToScripthash(addr) - const expectedOutput = 'bce4d5f2803bd1ed7c1ba00dcb3edffcbba50524af7c879d6bb918d04f138965' + const expectedOutput = + 'bce4d5f2803bd1ed7c1ba00dcb3edffcbba50524af7c879d6bb918d04f138965' assert.equal(scripthash, expectedOutput) }) @@ -204,18 +220,29 @@ describe('#Blockbook Router', () => { req.params.address = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' - // console.log(`process.env.BLOCKBOOK_URL: ${process.env.BLOCKBOOK_URL}`) + // Mock unit tests to prevent live network calls. + if (process.env.TEST === 'unit') { + electrumxRoute.isReady = true // Force flag. - // Mock the Insight URL for unit tests. - // if (process.env.TEST === 'unit') { - // sandbox.stub(blockbookRoute.axios, 'request').resolves({ - // data: mockData.mockBalance - // }) - // } + sandbox + .stub(electrumxRoute.electrumx, 'request') + .resolves(mockData.utxos) + } // Call the details API. const result = await electrumxRoute.getUtxos(req, res) - console.log(`result: ${util.inspect(result)}`) + // 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], 'height') + assert.property(result.utxos[0], 'tx_hash') + assert.property(result.utxos[0], 'tx_pos') + assert.property(result.utxos[0], 'value') }) }) }) diff --git a/test/v3/mocks/electrumx-mock.js b/test/v3/mocks/electrumx-mock.js new file mode 100644 index 0000000..76aaea1 --- /dev/null +++ b/test/v3/mocks/electrumx-mock.js @@ -0,0 +1,18 @@ +/* + Mocking data for electrumx unit tests +*/ + +'use strict' + +const utxos = [ + { + height: 604392, + tx_hash: '7774e449c5a3065144cefbc4c0c21e6b69c987f095856778ef9f45ddd8ae1a41', + tx_pos: 0, + value: 1000 + } +] + +module.exports = { + utxos +}