From 708488d92869967a9011dc7adcccc06982e7df76 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Mon, 6 Apr 2020 13:47:05 -0700 Subject: [PATCH] Created first unit tests for the elctrumx lib --- package-lock.json | 83 ++++++++ package.json | 2 + src/routes/v3/{electrum.js => electrumx.js} | 78 ++++++- test/v3/electrumx.js | 221 ++++++++++++++++++++ 4 files changed, 376 insertions(+), 8 deletions(-) rename src/routes/v3/{electrum.js => electrumx.js} (70%) create mode 100644 test/v3/electrumx.js diff --git a/package-lock.json b/package-lock.json index 5f5fbd5..231e80c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2255,6 +2255,76 @@ "varuint-bitcoin": "^1.0.1" } }, + "bitcore-lib": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-8.16.2.tgz", + "integrity": "sha512-wyiys24QYbj8CjeQvV0D0oMXJAEeIBJYf+ubob0d2sIno1P4hLkTkj/Yu4RGnUeePLVVWXcjnSR2micHimvU8w==", + "requires": { + "bech32": "=1.1.3", + "bn.js": "=4.11.8", + "bs58": "^4.0.1", + "buffer-compare": "=1.1.1", + "elliptic": "=6.4.0", + "inherits": "=2.0.1", + "lodash": "=4.17.15" + }, + "dependencies": { + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + } + } + }, + "bitcore-lib-cash": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/bitcore-lib-cash/-/bitcore-lib-cash-8.16.2.tgz", + "integrity": "sha512-WmREw2XkoEFKmIbJjhSmRgQUCh9XNfmJz6fdoVMReuqpQuzNWJtTueXmxDYMBvYDOmS4RNZq8SbBfkUOEqLTUQ==", + "requires": { + "bitcore-lib": "^8.16.2", + "bn.js": "=4.11.8", + "bs58": "^4.0.1", + "buffer-compare": "=1.1.1", + "elliptic": "=6.4.0", + "inherits": "=2.0.1", + "lodash": "=4.17.15" + }, + "dependencies": { + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + } + } + }, "bl": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", @@ -2503,6 +2573,11 @@ "ieee754": "^1.1.4" } }, + "buffer-compare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-compare/-/buffer-compare-1.1.1.tgz", + "integrity": "sha1-W+e+hTr4kZjR9N3AkNHWakiu9ZY=" + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -3912,6 +3987,14 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "electrum-cash": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/electrum-cash/-/electrum-cash-1.0.1.tgz", + "integrity": "sha512-snMgRt6JzsHdCdf+Un1rLj4RXjje1WuFVTx3Xx+6mitEibYalmD+x0ts146VF4Ki3+42DQKXtQXl0bzCAY7Jtw==", + "requires": { + "debug": "^4.1.1" + } + }, "elliptic": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz", diff --git a/package.json b/package.json index 544f1cd..c48e584 100644 --- a/package.json +++ b/package.json @@ -30,11 +30,13 @@ "@chris.troutner/bch-js": "^2.0.0", "apidoc": "^0.20.0", "axios": "^0.19.0", + "bitcore-lib-cash": "^8.16.2", "body-parser": "^1.18.3", "cookie-parser": "~1.4.3", "cors": "^2.8.3", "debug": "~4.1.1", "dotenv": "^8.0.0", + "electrum-cash": "^1.0.1", "express": "^4.15.5", "express-basic-auth": "^1.1.3", "express-rate-limit": "^5.0.0", diff --git a/src/routes/v3/electrum.js b/src/routes/v3/electrumx.js similarity index 70% rename from src/routes/v3/electrum.js rename to src/routes/v3/electrumx.js index 919cf0e..fbb7a18 100644 --- a/src/routes/v3/electrum.js +++ b/src/routes/v3/electrumx.js @@ -5,18 +5,16 @@ 'use strict' const express = require('express') +const router = express.Router() const axios = require('axios') +const util = require('util') +const bitcore = require('bitcore-lib-cash') + const wlogger = require('../../util/winston-logging') const RouteUtils = require('../../util/route-utils') const routeUtils = new RouteUtils() -const router = express.Router() - -// Used for processing error messages before sending them to the user. -const util = require('util') -util.inspect.defaultOptions = { depth: 1 } - const BCHJS = require('@chris.troutner/bch-js') const bchjs = new BCHJS() @@ -29,6 +27,7 @@ class Electrum { _this.axios = axios _this.routeUtils = routeUtils _this.bchjs = bchjs + _this.bitcore = bitcore _this.router = router _this.router.get('/', _this.root) @@ -55,7 +54,7 @@ class Electrum { // Root API endpoint. Simply acknowledges that it exists. root (req, res, next) { - return res.json({ status: 'address' }) + return res.json({ status: 'electrumx' }) } // Query the Blockbook Node API for a balance on a single BCH address. @@ -90,7 +89,7 @@ class Electrum { } /** - * @api {get} /blockbook/balance/{addr} Get balance for a single address. + * @api {get} /electrumx/balance/{addr} Get balance for a single address. * @apiName Balance for a single address * @apiGroup Blockbook * @apiDescription Returns an object with balance and details about an address. @@ -157,6 +156,69 @@ class Electrum { return _this.errorHandler(err, res) } } + + async getUtxos (req, res, next) { + try { + let scripthash = '' // Default value + + scripthash = _this.addressToScripthash(req.params.address) + + res.status(200) + return res.json(scripthash) + } 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. + 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/electrumx.js b/test/v3/electrumx.js new file mode 100644 index 0000000..ffdbe40 --- /dev/null +++ b/test/v3/electrumx.js @@ -0,0 +1,221 @@ +/* + 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/blockbook-mock') + +// Used for debugging. +const util = require('util') +util.inspect.defaultOptions = { depth: 1 } + +describe('#Blockbook Router', () => { + let req, res + let sandbox + before(() => { + // console.log(`Testing type is: ${process.env.TEST}`) + + if (!process.env.NETWORK) process.env.NETWORK = 'testnet' + }) + + // 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', () => { + // details route handler. + // const balanceSingle = blockbookRoute.balanceSingle + + // it('should throw 400 if address is empty', async () => { + // const result = await blockbookRoute.balanceSingle(req, res) + // // console.log(`result: ${util.inspect(result)}`) + // + // assert.hasAllKeys(result, ['error']) + // assert.include(result.error, 'address can not be empty') + // }) + + // it('should error on an array', async () => { + // req.params.address = ['qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'] + // + // const result = await blockbookRoute.balanceSingle(req, res) + // + // assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + // assert.include( + // result.error, + // 'address can not be an array', + // 'Proper error message' + // ) + // }) + + // it('should throw an error for an invalid address', async () => { + // req.params.address = + // '02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' + // + // const result = await blockbookRoute.balanceSingle(req, res) + // + // assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + // assert.include( + // result.error, + // 'Invalid BCH address', + // 'Proper error message' + // ) + // }) + + // it('should detect a network mismatch', async () => { + // req.params.address = + // 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4' + // + // const result = await blockbookRoute.balanceSingle(req, res) + // + // assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.') + // assert.include(result.error, 'Invalid network', 'Proper error message') + // }) + + // 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' + + // console.log(`process.env.BLOCKBOOK_URL: ${process.env.BLOCKBOOK_URL}`) + + // Mock the Insight URL for unit tests. + // if (process.env.TEST === 'unit') { + // sandbox.stub(blockbookRoute.axios, 'request').resolves({ + // data: mockData.mockBalance + // }) + // } + + // Call the details API. + const result = await electrumxRoute.getUtxos(req, res) + console.log(`result: ${util.inspect(result)}`) + }) + }) +})