diff --git a/config/electrumx.js b/config/electrumx.js deleted file mode 100644 index 7f6f36e..0000000 --- a/config/electrumx.js +++ /dev/null @@ -1,30 +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', - // serverUrl: 'badurl.com', - serverPort: '50002' - }, - ratelimit: { - windowMs: 1 * 60 * 1000, - max: 100 - } -} - -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/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 6e60920..de0ae3b 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') @@ -32,19 +34,13 @@ 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, - config.electrumx.electrum.serverUrl, - config.electrumx.electrum.serverPort + 'bch-api', + '1.4.1', + process.env.FULCRUM_URL, + process.env.FULCRUM_PORT + // '192.168.0.6', + // '50002' ) _this.isReady = false @@ -53,12 +49,19 @@ 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.post('/balance', _this.balanceBulk) + _this.router.get('/transactions/:address', _this.getTransactions) + _this.router.post('/transactions', _this.transactionsBulk) } // Initializes a connection to electrum servers. async connect () { try { - console.log('Entering connectToServers()') + 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 @@ -69,12 +72,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 +129,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 @@ -162,25 +200,14 @@ class Electrum { }) } - wlogger.debug('Executing electrumx/getUtxos with this address: ', address) - - // 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 + wlogger.debug( + 'Executing electrumx/getUtxos with this address: ', + cashAddr ) - // console.log( - // `electrumResponse: ${JSON.stringify(electrumResponse, null, 2)}` - // ) + + // Get data from ElectrumX server. + const electrumResponse = await _this._utxosFromElectrumx(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')) { @@ -204,6 +231,483 @@ 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. + 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 + * @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) + } + } + + /** + * @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 { + 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/balanceBulk with these addresses: ', + addresses + ) + + // Validate each element in the address array. + for (let i = 0; i < addresses.length; i++) { + const thisAddress = addresses[i] + + // Ensure the input is a valid BCH address. + try { + _this.bchjs.Address.toLegacyAddress(thisAddress) + } catch (err) { + res.status(400) + return res.json({ + error: `Invalid BCH address. Double check your address is valid: ${thisAddress}` + }) + } + + // Prevent a common user error. Ensure they are using the correct network address. + const networkIsValid = _this.routeUtils.validateNetwork(thisAddress) + if (!networkIsValid) { + res.status(400) + return res.json({ + error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.` + }) + } + } + + // Loops through each address and creates an array of Promises, querying + // ElectrumX API in parallel. + addresses = addresses.map(async (address, index) => { + // console.log(`address: ${address}`) + const balance = await _this._balanceFromElectrumx(address) + + return { + balance, + address + } + }) + + // Wait for all parallel Insight requests to return. + const result = await Promise.all(addresses) + + // Return the array of retrieved address information. + res.status(200) + return res.json({ + success: true, + balances: result + }) + } catch (err) { + wlogger.error('Error in electrumx.js/balanceBulk().', err) + + return _this.errorHandler(err, res) + } + } + + // 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 + } + } + + /** + * @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) + } + } + + /** + * @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/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/a01-electrumx.js b/test/v3/a01-electrumx.js new file mode 100644 index 0000000..17d539d --- /dev/null +++ b/test/v3/a01-electrumx.js @@ -0,0 +1,1002 @@ +/* + 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. + + To-Do: +*/ + +'use strict' + +const chai = require('chai') +const assert = chai.assert + +const sinon = require('sinon') + +const ElecrumxRoute = require('../../src/routes/v3/electrumx') + +// Mocking data. +const { mockReq, mockRes } = require('./mocks/express-mocks') +const mockData = require('./mocks/electrumx-mock') + +// Used for debugging. +const util = require('util') +util.inspect.defaultOptions = { depth: 1 } + +describe('#ElectrumX Router', () => { + let req, res + let sandbox + const electrumxRoute = new ElecrumxRoute() + + before(async () => { + if (!process.env.TEST) process.env.TEST = 'unit' + 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 () => { + // 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() + console.log('Disconnected from ElectrumX server') + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + + sandbox = sinon.createSandbox() + + // electrumxRoute = new ElecrumxRoute() + }) + + afterEach(() => { + sandbox.restore() + }) + + after(() => { + // + }) + + describe('#root', () => { + // root route handler. + const root = electrumxRoute.root + + it('should respond to GET for base route', async () => { + const result = root(req, res) + + assert.equal(result.status, 'electrumx', 'Returns static string') + }) + }) + + describe('#addressToScripthash', () => { + it('should accurately return a scripthash', () => { + const addr = 'bitcoincash:qpr270a5sxphltdmggtj07v4nskn9gmg9yx4m5h7s4' + + const scripthash = electrumxRoute.addressToScripthash(addr) + + const expectedOutput = + 'bce4d5f2803bd1ed7c1ba00dcb3edffcbba50524af7c879d6bb918d04f138965' + + assert.equal(scripthash, expectedOutput) + }) + }) + + 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) + // 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.getUtxos(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.getUtxos(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.getUtxos(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.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 = + 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + + // Mock unit tests to prevent live network calls. + 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.getUtxos(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], 'height') + assert.property(result.utxos[0], 'tx_hash') + assert.property(result.utxos[0], 'tx_pos') + assert.property(result.utxos[0], 'value') + }) + }) + + 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 { + // 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: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') { + 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') + }) + }) + + 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 electrum-cash to user', async () => { + // Address has invalid checksum. + req.params.address = + 'bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur2' + + // 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('#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 { + // 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) + }) + }) + + 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') + }) + }) + + 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') + }) + }) +}) diff --git a/test/v3/blockbook.js b/test/v3/blockbook.js index 7b4ac71..90a970e 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' @@ -103,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) @@ -117,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) @@ -456,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) @@ -652,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'] @@ -672,6 +672,7 @@ describe('#Blockbook Router', () => { 'Error message expected' ) }) + it('returns proper error when downstream service is down', async () => { req.body = { addresses: ['bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'] @@ -692,6 +693,7 @@ describe('#Blockbook Router', () => { 'Error message expected' ) }) + it('should get details for a single address', async () => { req.body = { addresses: ['bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'] @@ -785,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/' 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' }) diff --git a/test/v3/electrumx.js b/test/v3/electrumx.js deleted file mode 100644 index 15b19cd..0000000 --- a/test/v3/electrumx.js +++ /dev/null @@ -1,260 +0,0 @@ -/* - TESTS FOR THE ELECTRUMX.JS LIBRARY - - 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. - - To-Do: -*/ - -'use strict' - -const chai = require('chai') -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() - -// Mocking data. -const { mockReq, mockRes } = require('./mocks/express-mocks') -const mockData = require('./mocks/electrumx-mock') - -// Used for debugging. -const util = require('util') -util.inspect.defaultOptions = { depth: 1 } - -describe('#ElectrumX Router', () => { - let req, res - let sandbox - - 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. - beforeEach(() => { - // Mock the req and res objects used by Express routes. - req = mockReq - res = mockRes - - // Explicitly reset the parmas and body. - req.params = {} - req.body = {} - req.query = {} - - sandbox = sinon.createSandbox() - }) - - afterEach(() => { - sandbox.restore() - }) - - after(() => { - process.env.BLOCKBOOK_URL = originalUrl - }) - - describe('#root', () => { - // root route handler. - const root = electrumxRoute.root - - it('should respond to GET for base route', async () => { - const result = root(req, res) - - assert.equal(result.status, 'electrumx', 'Returns static string') - }) - }) - - describe('#addressToScripthash', () => { - it('should accurately return a scripthash', () => { - const addr = 'bitcoincash:qpr270a5sxphltdmggtj07v4nskn9gmg9yx4m5h7s4' - - const scripthash = electrumxRoute.addressToScripthash(addr) - - const expectedOutput = - 'bce4d5f2803bd1ed7c1ba00dcb3edffcbba50524af7c879d6bb918d04f138965' - - assert.equal(scripthash, expectedOutput) - }) - }) - - describe('#UTXO', () => { - it('should throw 400 if address is empty', async () => { - const result = await electrumxRoute.getUtxos(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.getUtxos(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.getUtxos(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.getUtxos(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 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 - // } - // }) - - // 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' - // ) - // }) - - // 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' - // ) - // }) - - it('should get balance for a single address', async () => { - req.params.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.getUtxos(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], '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 index 76aaea1..65baf09 100644 --- a/test/v3/mocks/electrumx-mock.js +++ b/test/v3/mocks/electrumx-mock.js @@ -10,9 +10,29 @@ const utxos = [ tx_hash: '7774e449c5a3065144cefbc4c0c21e6b69c987f095856778ef9f45ddd8ae1a41', tx_pos: 0, value: 1000 + }, + { + height: 630834, + tx_hash: '4fe60a51e0d8f5134bfd8e5f872d6e502d7f01b28a6afebb27f4438a4f638d53', + tx_pos: 0, + value: 6000 + } +] + +const balance = { + confirmed: 7000, + unconfirmed: 0 +} + +const txHistory = [ + { + height: 601861, + tx_hash: '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d' } ] module.exports = { - utxos + utxos, + balance, + txHistory }