From b4d29a58566389433a6089b01fc0f5c5e2ecb06d Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Mon, 1 Mar 2021 11:35:07 -0800 Subject: [PATCH] fix(Util.sweepWif): Replaced blockbook with electrumx --- src/app.js | 6 +-- src/routes/v4/util.js | 58 ++++++++++++------------ test/v4/mocks/util-mocks.js | 89 +++++++++++++++++-------------------- test/v4/util.js | 76 ++++++++++++++++++------------- 4 files changed, 117 insertions(+), 112 deletions(-) diff --git a/src/app.js b/src/app.js index ce2265d..1863839 100644 --- a/src/app.js +++ b/src/app.js @@ -52,7 +52,7 @@ const electrumxv4 = new ElectrumXV4() electrumxv4.connect() const encryptionv4 = new EncryptionV4() const pricev4 = new PriceV4() -const utilV4 = new UtilV4() +const utilV4 = new UtilV4({ electrumx: electrumxv4 }) const app = express() @@ -208,11 +208,11 @@ function onError (error) { case 'EACCES': console.error(`${bind} requires elevated privileges`) process.exit(1) - // break + // break case 'EADDRINUSE': console.error(`${bind} is already in use`) process.exit(1) - // break + // break default: throw error } diff --git a/src/routes/v4/util.js b/src/routes/v4/util.js index 986f3d6..a50ee24 100644 --- a/src/routes/v4/util.js +++ b/src/routes/v4/util.js @@ -7,9 +7,6 @@ const axios = require('axios') const routeUtils = require('./route-utils') const wlogger = require('../../util/winston-logging') -const Blockbook = require('./blockbook') -const blockbook = new Blockbook() - const util = require('util') util.inspect.defaultOptions = { depth: 1 } @@ -38,9 +35,22 @@ const bchjs = new BCHJS() let _this class UtilRoute { - constructor () { + constructor (utilConfig) { this.bchjs = bchjs - this.blockbook = blockbook + // this.blockbook = blockbook + + if (!utilConfig) { + throw new Error( + 'Must pass a config object when instantiating the Util library.' + ) + } + if (!utilConfig.electrumx) { + throw new Error( + 'Must pass an instance of Electrumx when instantiating the Util library.' + ) + } + + this.electrumx = utilConfig.electrumx this.router = router this.router.get('/', this.root) @@ -172,7 +182,7 @@ class UtilRoute { } = routeUtils.setEnvVars() // Loop through each address and creates an array of requests to call in parallel - const promises = addresses.map(async address => { + const promises = addresses.map(async (address) => { requestConfig.data.id = 'validateaddress' requestConfig.data.method = 'validateaddress' requestConfig.data.params = [address] @@ -184,7 +194,7 @@ class UtilRoute { const axiosResult = await axios.all(promises) // Retrieve the data part of the result. - const result = axiosResult.map(x => x.data.result) + const result = axiosResult.map((x) => x.data.result) res.status(200) return res.json(result) @@ -250,12 +260,11 @@ class UtilRoute { const fromAddr = bchjs.ECPair.toCashAddress(ecPair) // Get a balance on the public address - const balances = await _this.blockbook.balanceFromBlockbook(fromAddr) + const balances = await _this.electrumx._balanceFromElectrumx(fromAddr) // console.log(`balances: ${JSON.stringify(balances, null, 2)}`) // Total balance is the sum of the confirmed and unconfirmed balance. - const totalBalance = - Number(balances.balance) + Number(balances.unconfirmedBalance) + const totalBalance = balances.confirmed + balances.unconfirmed // Exit if balance is zero. if (isNaN(totalBalance) || totalBalance === 0) { @@ -270,7 +279,7 @@ class UtilRoute { } // Get all UTXOs help by the address. - const utxos = await _this.blockbook.utxosFromBlockbook(fromAddr) + const utxos = await _this.electrumx._utxosFromElectrumx(fromAddr) // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) const tokenUtxos = [] @@ -375,7 +384,7 @@ class UtilRoute { let utxos = options.utxos // Ensure all utxos have the satoshis property. - utxos = utxos.map(x => { + utxos = utxos.map((x) => { x.satoshis = Number(x.value) return x }) @@ -393,9 +402,9 @@ class UtilRoute { for (let i = 0; i < utxos.length; i++) { const utxo = utxos[i] - originalAmount = originalAmount + utxo.satoshis + originalAmount = originalAmount + utxo.value - transactionBuilder.addInput(utxo.txid, utxo.vout) + transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos) } if (originalAmount < 546) { @@ -430,7 +439,7 @@ class UtilRoute { ecPair, redeemScript, transactionBuilder.hashTypes.SIGHASH_ALL, - utxo.satoshis + utxo.value ) } @@ -468,7 +477,7 @@ class UtilRoute { // Ensure there is only one class of token in the wallet. Throw an error if // there is more than one. const tokenId = tokenUtxos[0].tokenId - const otherTokens = tokenUtxos.filter(x => x.tokenId !== tokenId) + const otherTokens = tokenUtxos.filter((x) => x.tokenId !== tokenId) if (otherTokens.length > 0) { throw new Error( 'Multiple token classes detected. This function only supports a single class of token.' @@ -490,9 +499,9 @@ class UtilRoute { for (let i = 0; i < allUtxos.length; i++) { const utxo = allUtxos[i] - originalAmount = originalAmount + utxo.satoshis + originalAmount = originalAmount + utxo.value - transactionBuilder.addInput(utxo.txid, utxo.vout) + transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos) } if (originalAmount < 300) { @@ -572,7 +581,7 @@ class UtilRoute { ecPair, redeemScript, transactionBuilder.hashTypes.SIGHASH_ALL, - thisUtxo.satoshis + thisUtxo.value ) } @@ -592,15 +601,4 @@ class UtilRoute { } } -// module.exports = { -// router, -// // testableComponents: { -// // root, -// // validateAddressSingle, -// // validateAddressBulk, -// // sweepWif -// // } -// UtilRoute -// } - module.exports = UtilRoute diff --git a/test/v4/mocks/util-mocks.js b/test/v4/mocks/util-mocks.js index 30987a7..bb45273 100644 --- a/test/v4/mocks/util-mocks.js +++ b/test/v4/mocks/util-mocks.js @@ -13,75 +13,70 @@ const mockAddress = { isscript: false } +// const mockBalance = { +// page: 1, +// totalPages: 1, +// itemsOnPage: 1000, +// address: 'bitcoincash:qzp7gdl52edm24xlpkyqnza9rv43u3mdxyc77j3u6k', +// balance: '2546', +// totalReceived: '2546', +// totalSent: '0', +// unconfirmedBalance: '0', +// unconfirmedTxs: 0, +// txs: 2, +// txids: [ +// 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56', +// '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6' +// ] +// } + const mockBalance = { - page: 1, - totalPages: 1, - itemsOnPage: 1000, - address: 'bitcoincash:qzp7gdl52edm24xlpkyqnza9rv43u3mdxyc77j3u6k', - balance: '2546', - totalReceived: '2546', - totalSent: '0', - unconfirmedBalance: '0', - unconfirmedTxs: 0, - txs: 2, - txids: [ - 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56', - '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6' - ] + confirmed: 2546, + unconfirmed: 0 } const mockUtxos = [ { - txid: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56', - vout: 0, - value: '2000', - height: 605873, - confirmations: 298, - satoshis: 2000 + tx_hash: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56', + tx_pos: 0, + value: 2000, + height: 605873 }, { - txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6', - vout: 1, - value: '546', - height: 605873, - confirmations: 298, - satoshis: 546 + tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6', + tx_pos: 1, + value: 546, + height: 605873 } ] const mockThreeUtxos = [ { - txid: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56', - vout: 0, - value: '2000', - height: 605873, - confirmations: 298, - satoshis: 2000 + tx_hash: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56', + tx_pos: 0, + value: 2000, + height: 605873 }, { - txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6', - vout: 1, - value: '546', - height: 605873, - confirmations: 298, - satoshis: 546 + tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6', + tx_pos: 1, + value: 546, + height: 605873 }, { - txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6', - vout: 1, - value: '546', - height: 605873, - confirmations: 298, - satoshis: 546 + tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6', + tx_pos: 1, + value: 546, + height: 605873 } ] const mockIsTokenUtxos = [ false, { - txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6', - vout: 1, - value: '546', + tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6', + tx_out: 1, + value: 546, height: 605873, confirmations: 298, satoshis: 546, diff --git a/test/v4/util.js b/test/v4/util.js index 75e309b..939936b 100644 --- a/test/v4/util.js +++ b/test/v4/util.js @@ -4,15 +4,22 @@ 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. + + These tests use this private key: + L1wAGEN721LHDoiN8pLwwBb87bYrU6Gs21UPcCRR7LjKypQyVaCq + Which corresponds to this address: + bitcoincash:qp2g4cnekxsjspccmtvh5k73mczz6273js4mjr353r */ 'use strict' -const chai = require('chai') -const assert = chai.assert +// Public npm libraries +const assert = require('chai').assert const nock = require('nock') // HTTP mocking const sinon = require('sinon') +// Local libraries +const Electrumx = require('../../src/routes/v4/electrumx') const UtilRoute = require('../../src/routes/v4/util') let originalEnvVars // Used during transition from integration to unit tests. @@ -25,13 +32,16 @@ const util = require('util') util.inspect.defaultOptions = { depth: 1 } // const UtilRoute = utilRoute.UtilRoute -const utilRouteInst = new UtilRoute() +// const electrumx = new Electrumx() +const utilRouteInst = new UtilRoute({ electrumx: {} }) describe('#Util', () => { let req, res let sandbox + let electrumx + let utilRoute - before(() => { + before(async () => { // Save existing environment variables. originalEnvVars = { BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, @@ -48,6 +58,9 @@ describe('#Util', () => { process.env.RPC_USERNAME = 'fakeusername' process.env.RPC_PASSWORD = 'fakepassword' } + + electrumx = new Electrumx() + // await electrumx.connect() }) // Setup the mocks before each test. @@ -65,6 +78,8 @@ describe('#Util', () => { if (!nock.isActive()) nock.activate() sandbox = sinon.createSandbox() + + utilRoute = new UtilRoute({ electrumx }) }) afterEach(() => { @@ -358,29 +373,29 @@ describe('#Util', () => { // Mock the RPC call for unit tests. sandbox - .stub(utilRouteInst.blockbook, 'balanceFromBlockbook') + .stub(utilRoute.electrumx, '_balanceFromElectrumx') .resolves(mockData.mockBalance) sandbox - .stub(utilRouteInst.blockbook, 'utxosFromBlockbook') + .stub(utilRoute.electrumx, '_utxosFromElectrumx') .resolves(mockData.mockUtxos) sandbox - .stub(utilRouteInst.bchjs.SLP.Utils, 'tokenUtxoDetails') + .stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails') .resolves(mockData.mockIsTokenUtxos) // Mock sendRawTransaction() so that the hex does not actually get broadcast // to the network. sandbox - .stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction') + .stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') .resolves('test-txid') req.body = { - wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt', - toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p' + wif: 'L1wAGEN721LHDoiN8pLwwBb87bYrU6Gs21UPcCRR7LjKypQyVaCq', + toAddr: 'bitcoincash:qp2g4cnekxsjspccmtvh5k73mczz6273js4mjr353r' } - const result = await utilRouteInst.sweepWif(req, res) + const result = await utilRoute.sweepWif(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.equal(result, 'test-txid') @@ -390,13 +405,13 @@ describe('#Util', () => { // Mock the RPC call for unit tests. sandbox - .stub(utilRouteInst.blockbook, 'balanceFromBlockbook') + .stub(utilRoute.electrumx, '_balanceFromElectrumx') .resolves(mockData.mockBalance) // Mock sendRawTransaction() so that the hex does not actually get broadcast // to the network. sandbox - .stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction') + .stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') .resolves('test-txid') req.body = { @@ -404,33 +419,30 @@ describe('#Util', () => { balanceOnly: true } - const result = await utilRouteInst.sweepWif(req, res) + const result = await utilRoute.sweepWif(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isNumber(result) }) - } - // Unit tests only - if (process.env.TEST === 'unit') { it('should generate transaction for valid BCH-only sweep', async () => { sandbox - .stub(utilRouteInst.blockbook, 'balanceFromBlockbook') + .stub(utilRoute.electrumx, '_balanceFromElectrumx') .resolves(mockData.mockBalance) sandbox - .stub(utilRouteInst.blockbook, 'utxosFromBlockbook') + .stub(utilRoute.electrumx, '_utxosFromElectrumx') .resolves(mockData.mockUtxos) // Force token utxo to appear as regular BCH utxo. sandbox - .stub(utilRouteInst.bchjs.SLP.Utils, 'tokenUtxoDetails') + .stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails') .resolves([false, false]) // Mock sendRawTransaction() so that the hex does not actually get broadcast // to the network. sandbox - .stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction') + .stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') .resolves('test-txid') req.body = { @@ -438,7 +450,7 @@ describe('#Util', () => { toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p' } - const result = await utilRouteInst.sweepWif(req, res) + const result = await utilRoute.sweepWif(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.equal(result, 'test-txid') @@ -446,22 +458,22 @@ describe('#Util', () => { it('should throw 422 error if no non-token UTXOs', async () => { sandbox - .stub(utilRouteInst.blockbook, 'balanceFromBlockbook') + .stub(utilRoute.electrumx, '_balanceFromElectrumx') .resolves(mockData.mockBalance) sandbox - .stub(utilRouteInst.blockbook, 'utxosFromBlockbook') + .stub(utilRoute.electrumx, '_utxosFromElectrumx') .resolves(mockData.mockUtxos) // Force token utxo to appear as regular BCH utxo. sandbox - .stub(utilRouteInst.bchjs.SLP.Utils, 'tokenUtxoDetails') + .stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails') .resolves(mockData.tokensOnly) // Mock sendRawTransaction() so that the hex does not actually get broadcast // to the network. sandbox - .stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction') + .stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') .resolves('test-txid') req.body = { @@ -469,7 +481,7 @@ describe('#Util', () => { toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p' } - const result = await utilRouteInst.sweepWif(req, res) + const result = await utilRoute.sweepWif(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.equal(res.statusCode, 422) @@ -482,22 +494,22 @@ describe('#Util', () => { it('should detect and throw error for multiple token classes', async () => { sandbox - .stub(utilRouteInst.blockbook, 'balanceFromBlockbook') + .stub(utilRoute.electrumx, '_balanceFromElectrumx') .resolves(mockData.mockBalance) sandbox - .stub(utilRouteInst.blockbook, 'utxosFromBlockbook') + .stub(utilRoute.electrumx, '_utxosFromElectrumx') .resolves(mockData.mockThreeUtxos) // Force token utxo to appear as regular BCH utxo. sandbox - .stub(utilRouteInst.bchjs.SLP.Utils, 'tokenUtxoDetails') + .stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails') .resolves(mockData.multipleTokens) // Mock sendRawTransaction() so that the hex does not actually get broadcast // to the network. sandbox - .stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction') + .stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') .resolves('test-txid') req.body = { @@ -505,7 +517,7 @@ describe('#Util', () => { toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p' } - const result = await utilRouteInst.sweepWif(req, res) + const result = await utilRoute.sweepWif(req, res) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.equal(res.statusCode, 422)