diff --git a/package.json b/package.json index db86e77..335032a 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,8 @@ "test:integration:local:abc": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 test/integration && mocha --timeout 30000 test/integration/chains/abc/", "test:integration:local:bchn": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 test/integration/ && mocha --timeout 30000 test/integration/chains/bchn/", "test:integration:local:testnet": "RESTURL=http://localhost:4000/v4/ mocha --timeout 30000 test/integration/chains/testnet", - "test:temp": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#hydrateUtxos' test/integration/", - "test:temp2": "mocha --timeout=30000 -g '#tokenUtxoDetailsWL' test/unit/", - "test:temp3": "RESTURL=http://localhost:3000/v4/ mocha --timeout 30000 test/integration/chains/abc/", + "test:temp": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#UTXO' test/integration/chains/bchn/", + "test:temp2": "mocha --timeout=30000 -g '#utxo' test/unit/", "coverage": "nyc report --reporter=text-lcov | coveralls", "coverage:report": "nyc --reporter=html mocha --timeout 25000 test/unit/", "docs": "./node_modules/.bin/apidoc -i src/ -o docs", diff --git a/src/bch-js.js b/src/bch-js.js index 6dbb615..2bfaa53 100644 --- a/src/bch-js.js +++ b/src/bch-js.js @@ -31,6 +31,7 @@ const Schnorr = require('./schnorr') const SLP = require('./slp/slp') const IPFS = require('./ipfs') const Encryption = require('./encryption') +const Utxo = require('./utxo') // Indexers const Ninsight = require('./ninsight') @@ -113,6 +114,7 @@ class BCHJS { this.SLP.HDNode = this.HDNode this.IPFS = new IPFS() + this.Utxo = new Utxo(libConfig) } } diff --git a/src/utxo.js b/src/utxo.js new file mode 100644 index 0000000..85bd058 --- /dev/null +++ b/src/utxo.js @@ -0,0 +1,109 @@ +/* + High-level functions for working with UTXOs + + TODO: + - Make a getWL() clone of get(), but uses hydrateUtxosWL() +*/ + +// Local libraries +const Electrumx = require('./electrumx') +const Slp = require('./slp/slp') + +class UTXO { + constructor (config) { + // Encapsulate dependencies for easier mocking. + this.electrumx = new Electrumx(config) + this.slp = new Slp(config) + } + + /** + * @api Utxo.get() get() + * @apiName get + * @apiGroup UTXO + * @apiDescription Get UTXOs for an address + * + * Given an address, this function will return an object with three arrays: + * - bchUtxos - UTXOs confirmed to be spendable as normal BCH + * - slpUtxos - UTXOs confirmed to be colored as valid SLP tokens + * - nullUtxo - UTXOs that did not pass SLP validation. Should be ignored and + * not spent, to be safe. + * + * @apiExample Example usage: + * (async () => { + * try { + * let utxos = await bchjs.Utxo.get('simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'); + * console.log(utxos); + * } catch(error) { + * console.error(error) + * } + * })() + * + * // returns + * { + * "bchUtxos": [ + * { + * "height": 674331, + * "tx_hash": "5e86cd911110e4f5db0cc3e8f459d5e8850b49adf57059a71daee674b2867b31", + * "tx_pos": 0, + * "value": 1000, + * "txid": "5e86cd911110e4f5db0cc3e8f459d5e8850b49adf57059a71daee674b2867b31", + * "vout": 0, + * "isValid": false + * } + * ], + * "slpUtxos": [ + * { + * "height": 569108, + * "tx_hash": "384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c", + * "tx_pos": 1, + * "value": 546, + * "txid": "384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c", + * "vout": 1, + * "utxoType": "token", + * "transactionType": "send", + * "tokenId": "a436c8e1b6bee3d701c6044d190f76f774be83c36de8d34a988af4489e86dd37", + * "tokenTicker": "sleven", + * "tokenName": "sleven", + * "tokenDocumentUrl": "sleven", + * "tokenDocumentHash": "", + * "decimals": 7, + * "tokenType": 1, + * "tokenQty": "1", + * "isValid": true + * } + * ], + * "nullUtxos": [] + * } + * + * + */ + async get (address) { + try { + const addr = this.slp.Address.toCashAddress(address) + + // Get the UTXOs associated with the address. + const utxoData = await this.electrumx.utxo([addr]) + // console.log(`utxoData: ${JSON.stringify(utxoData, null, 2)}`) + + // Hydate the utxos with token information. + const hydratedUtxos = await this.slp.Utils.hydrateUtxos(utxoData.utxos) + // console.log(`hydratedUtxos: ${JSON.stringify(hydratedUtxos, null, 2)}`) + + const retObj = {} // return object + + // Filter out the different types of UTXOs. + retObj.bchUtxos = hydratedUtxos.slpUtxos[0].utxos.filter(elem => elem.isValid === false) + retObj.slpUtxos = hydratedUtxos.slpUtxos[0].utxos.filter(elem => elem.isValid === true) + retObj.nullUtxos = hydratedUtxos.slpUtxos[0].utxos.filter(elem => elem.isValid === null) + // Note: true, false, and null should be only values. An element with + // isValid set to any other value should be ignored. + + return retObj + } catch (err) { + console.error('Error in bchjs.utxo.get()') + throw err + } + } +} + +module.exports = UTXO diff --git a/test/integration/chains/abc/utxo-integration.js b/test/integration/chains/abc/utxo-integration.js new file mode 100644 index 0000000..ab856fc --- /dev/null +++ b/test/integration/chains/abc/utxo-integration.js @@ -0,0 +1,34 @@ +/* + Integration tests for the utxo.js library. +*/ + +const assert = require('chai').assert + +const BCHJS = require('../../../../src/bch-js') +const bchjs = new BCHJS() + +describe('#UTXO', () => { + beforeEach(async () => { + // sandbox = sinon.createSandbox() + + if (process.env.IS_USING_FREE_TIER) await sleep(1000) + }) + + describe('#get', () => { + it('should get hydrated and filtered UTXOs for an address', async () => { + // const addr = 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9' + const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' + + const result = await bchjs.Utxo.get(addr) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result.bchUtxos) + assert.isArray(result.slpUtxos) + assert.isArray(result.nullUtxos) + }) + }) +}) + +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/test/integration/chains/bchn/utxo-integration.js b/test/integration/chains/bchn/utxo-integration.js new file mode 100644 index 0000000..ab856fc --- /dev/null +++ b/test/integration/chains/bchn/utxo-integration.js @@ -0,0 +1,34 @@ +/* + Integration tests for the utxo.js library. +*/ + +const assert = require('chai').assert + +const BCHJS = require('../../../../src/bch-js') +const bchjs = new BCHJS() + +describe('#UTXO', () => { + beforeEach(async () => { + // sandbox = sinon.createSandbox() + + if (process.env.IS_USING_FREE_TIER) await sleep(1000) + }) + + describe('#get', () => { + it('should get hydrated and filtered UTXOs for an address', async () => { + // const addr = 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9' + const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' + + const result = await bchjs.Utxo.get(addr) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result.bchUtxos) + assert.isArray(result.slpUtxos) + assert.isArray(result.nullUtxos) + }) + }) +}) + +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/test/integration/implementations/sweet/rawtransaction.js b/test/integration/implementations/sweet/rawtransaction.js deleted file mode 100644 index 0c4f251..0000000 --- a/test/integration/implementations/sweet/rawtransaction.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - Integration tests for the bchjs. Only covers calls made to - rest.bitcoin.com. - - TODO -*/ - -const chai = require('chai') -const assert = chai.assert -const BCHJS = require('../../../../src/bch-js') -const bchjs = new BCHJS() - -// Inspect utility used for debugging. -const util = require('util') -util.inspect.defaultOptions = { - showHidden: true, - colors: true, - depth: 3 -} - -describe('#rawtransaction', () => { - beforeEach(async () => { - if (process.env.IS_USING_FREE_TIER) await sleep(1000) - }) - - /* - Testing sentRawTransaction isn't really possible with an integration test, - as the endpoint really needs an e2e test to be properly tested. The tests - below expect error messages returned from the server, but at least test - that the server is responding on those endpoints, and responds consistently. - */ - describe('sendRawTransaction', () => { - it('should send a single transaction hex', async () => { - try { - const hex = - '01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000' - - await bchjs.RawTransactions.sendRawTransaction(hex) - // console.log(`result ${JSON.stringify(result, null, 2)}`) - - assert.equal(true, false, 'Unexpected result!') - } catch (err) { - // console.log(`err: ${util.inspect(err)}`) - - assert.hasAllKeys(err, ['error']) - assert.include(err.error, 'Missing inputs') - } - }) - - it('should send an array of tx hexes', async () => { - try { - const hexes = [ - '01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000', - '01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000' - ] - - const result = await bchjs.RawTransactions.sendRawTransaction(hexes) - console.log(`result ${JSON.stringify(result, null, 2)}`) - } catch (err) { - // console.log(`err: ${util.inspect(err)}`) - - assert.hasAllKeys(err, ['error']) - assert.include(err.error, 'Missing inputs') - } - }) - }) -}) - -function sleep (ms) { - return new Promise(resolve => setTimeout(resolve, ms)) -} diff --git a/test/integration/implementations/sweet/slp.js b/test/integration/implementations/sweet/slp.js deleted file mode 100644 index f25fb4f..0000000 --- a/test/integration/implementations/sweet/slp.js +++ /dev/null @@ -1,246 +0,0 @@ -/* - Integration tests for the bchjs covering SLP tokens. - These tests are specific to the ABC chain. -*/ - -const chai = require('chai') -const assert = chai.assert - -const BCHJS = require('../../../../src/bch-js') -let bchjs - -// Inspect utility used for debugging. -const util = require('util') -util.inspect.defaultOptions = { - showHidden: true, - colors: true, - depth: 1 -} - -describe('#SLP', () => { - // before(() => { - // console.log(`bchjs.SLP.restURL: ${bchjs.SLP.restURL}`) - // console.log(`bchjs.SLP.apiToken: ${bchjs.SLP.apiToken}`) - // }) - - beforeEach(async () => { - // Introduce a delay so that the BVT doesn't trip the rate limits. - if (process.env.IS_USING_FREE_TIER) await sleep(1000) - - bchjs = new BCHJS() - }) - - describe('#util', () => { - describe('#tokenUtxoDetails', () => { - it('should handle a range of UTXO types', async () => { - const utxos = [ - // Malformed SLP tx - { - note: 'Malformed SLP tx', - tx_hash: - 'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a', - tx_pos: 1, - value: 546 - }, - // Normal TX (non-SLP) - { - note: 'Normal TX (non-SLP)', - tx_hash: - '01cdaec2f8b311fc2d6ecc930247bd45fa696dc204ab684596e281fe1b06c1f0', - tx_pos: 0, - value: 400000 - }, - // Valid PSF SLP tx - { - note: 'Valid PSF SLP tx', - tx_hash: - 'daf4d8b8045e7a90b7af81bfe2370178f687da0e545511bce1c9ae539eba5ffd', - tx_pos: 1, - value: 546 - }, - // Valid SLP token not in whitelist - { - note: 'Valid SLP token not in whitelist', - tx_hash: - '3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488', - tx_pos: 1, - value: 546 - }, - // Token send on BCHN network. - { - note: 'Token send on BCHN network', - tx_hash: - '402c663379d9699b6e2dd38737061e5888c5a49fca77c97ab98e79e08959e019', - tx_pos: 1, - value: 546 - }, - // Token send on ABC network. - { - note: 'Token send on ABC network', - tx_hash: - '336bfe2168aac4c3303508a9e8548a0d33797a83b85b76a12d845c8d6674f79d', - tx_pos: 1, - value: 546 - }, - // Known invalid SLP token send of PSF tokens. - { - note: 'Known invalid SLP token send of PSF tokens', - tx_hash: - '2bf691ad3679d928fef880b8a45b93b233f8fa0d0a92cf792313dbe77b1deb74', - tx_pos: 1, - value: 546 - } - ] - - const data = await bchjs.SLP.Utils.tokenUtxoDetails(utxos) - // console.log(`data: ${JSON.stringify(data, null, 2)}`) - - // Malformed SLP tx - assert.equal(data[0].tx_hash, utxos[0].tx_hash) - assert.equal(data[0].isValid, false) - - // Normal TX (non-SLP) - assert.equal(data[1].tx_hash, utxos[1].tx_hash) - assert.equal(data[1].isValid, false) - - // Valid PSF SLP tx - assert.equal(data[2].tx_hash, utxos[2].tx_hash) - assert.equal(data[2].isValid, true) - - // Valid SLP token not in whitelist - assert.equal(data[3].tx_hash, utxos[3].tx_hash) - assert.equal(data[3].isValid, true) - - // Token send on BCHN network - assert.equal(data[4].tx_hash, utxos[4].tx_hash) - assert.equal(data[4].isValid, true) - - // Token send on ABC network - assert.equal(data[5].tx_hash, utxos[5].tx_hash) - assert.equal(data[5].isValid, null) - - // Known invalid SLP token send of PSF tokens - assert.equal(data[6].tx_hash, utxos[6].tx_hash) - assert.equal(data[6].isValid, false) - }) - }) - - describe('#validateTxid3', () => { - it('should invalidate a known invalid TXID', async () => { - const txid = - 'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a' - - const result = await bchjs.SLP.Utils.validateTxid3(txid) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - - assert.property(result[0], 'txid') - assert.equal(result[0].txid, txid) - - assert.property(result[0], 'valid') - assert.equal(result[0].valid, null) - }) - - it('should validate a known valid TXID', async () => { - const txid = - 'daf4d8b8045e7a90b7af81bfe2370178f687da0e545511bce1c9ae539eba5ffd' - - const result = await bchjs.SLP.Utils.validateTxid3(txid) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - - assert.property(result[0], 'txid') - assert.equal(result[0].txid, txid) - - assert.property(result[0], 'valid') - assert.equal(result[0].valid, true) - }) - - it('should handle a mix of valid, invalid, and non-SLP txs', async () => { - const txids = [ - // Malformed SLP tx - 'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a', - // Normal TX (non-SLP) - '01cdaec2f8b311fc2d6ecc930247bd45fa696dc204ab684596e281fe1b06c1f0', - // Valid PSF SLP tx - 'daf4d8b8045e7a90b7af81bfe2370178f687da0e545511bce1c9ae539eba5ffd', - // Valid SLP token not in whitelist - '3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488', - // Unprocessed SLP TX - '402c663379d9699b6e2dd38737061e5888c5a49fca77c97ab98e79e08959e019' - ] - - const result = await bchjs.SLP.Utils.validateTxid3(txids) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - - assert.equal(result[0].txid, txids[0]) - assert.equal(result[0].valid, null) - - assert.equal(result[1].txid, txids[1]) - assert.equal(result[1].valid, null) - - assert.equal(result[2].txid, txids[2]) - assert.equal(result[2].valid, true) - - // True in validateTxid() - assert.equal(result[3].txid, txids[3]) - assert.equal(result[3].valid, true) - - assert.equal(result[4].txid, txids[4]) - assert.equal(result[4].valid, true) - }) - }) - - describe('#validateTxid', () => { - it('should handle a mix of valid, invalid, and non-SLP txs', async () => { - const txids = [ - // Malformed SLP tx - 'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a', - // Normal TX (non-SLP) - '01cdaec2f8b311fc2d6ecc930247bd45fa696dc204ab684596e281fe1b06c1f0', - // Valid PSF SLP tx - 'daf4d8b8045e7a90b7af81bfe2370178f687da0e545511bce1c9ae539eba5ffd', - // Valid SLP token not in whitelist - '3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488', - // Token send on BCHN network. - '402c663379d9699b6e2dd38737061e5888c5a49fca77c97ab98e79e08959e019', - // Token send on ABC network. - '336bfe2168aac4c3303508a9e8548a0d33797a83b85b76a12d845c8d6674f79d' - ] - - const result = await bchjs.SLP.Utils.validateTxid(txids) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - - assert.equal(result[0].txid, txids[0]) - assert.equal(result[0].valid, null) - - assert.equal(result[1].txid, txids[1]) - assert.equal(result[1].valid, null) - - assert.equal(result[2].txid, txids[2]) - assert.equal(result[2].valid, true) - - // True in validateTxid() but null in validateTxid3() - assert.equal(result[3].txid, txids[3]) - assert.equal(result[3].valid, true) - - assert.equal(result[4].txid, txids[4]) - assert.equal(result[4].valid, true) - - assert.equal(result[5].txid, txids[5]) - assert.equal(result[5].valid, null) - }) - }) - }) -}) - -// Promise-based sleep function -function sleep (ms) { - return new Promise(resolve => setTimeout(resolve, ms)) -} diff --git a/test/unit/control.js b/test/unit/control.js index 2c8f866..bbe557e 100644 --- a/test/unit/control.js +++ b/test/unit/control.js @@ -5,11 +5,11 @@ const bchjs = new BCHJS() const sinon = require('sinon') describe('#Control', () => { - describe('#getNetworkInfo', () => { - let sandbox - beforeEach(() => (sandbox = sinon.createSandbox())) - afterEach(() => sandbox.restore()) + let sandbox + beforeEach(() => (sandbox = sinon.createSandbox())) + afterEach(() => sandbox.restore()) + describe('#getNetworkInfo', () => { it('should get info', done => { const data = { version: 170000, @@ -36,10 +36,6 @@ describe('#Control', () => { }) describe('#getMemoryInfo', () => { - let sandbox - beforeEach(() => (sandbox = sinon.createSandbox())) - afterEach(() => sandbox.restore()) - it('should get memory info', done => { const data = { locked: { diff --git a/test/unit/fixtures/utxo-mocks.js b/test/unit/fixtures/utxo-mocks.js new file mode 100644 index 0000000..994a74d --- /dev/null +++ b/test/unit/fixtures/utxo-mocks.js @@ -0,0 +1,72 @@ +const mockUtxoData = { + success: true, + utxos: [ + { + utxos: [ + { + height: 569108, + tx_hash: + '384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c', + tx_pos: 1, + value: 546 + }, + { + height: 674331, + tx_hash: + '5e86cd911110e4f5db0cc3e8f459d5e8850b49adf57059a71daee674b2867b31', + tx_pos: 0, + value: 1000 + } + ], + address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m' + } + ] +} + +const mockHydratedUtxos = { + slpUtxos: [ + { + utxos: [ + { + height: 569108, + tx_hash: + '384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c', + tx_pos: 1, + value: 546, + txid: + '384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'a436c8e1b6bee3d701c6044d190f76f774be83c36de8d34a988af4489e86dd37', + tokenTicker: 'sleven', + tokenName: 'sleven', + tokenDocumentUrl: 'sleven', + tokenDocumentHash: '', + decimals: 7, + tokenType: 1, + tokenQty: '1', + isValid: true + }, + { + height: 674331, + tx_hash: + '5e86cd911110e4f5db0cc3e8f459d5e8850b49adf57059a71daee674b2867b31', + tx_pos: 0, + value: 1000, + txid: + '5e86cd911110e4f5db0cc3e8f459d5e8850b49adf57059a71daee674b2867b31', + vout: 0, + isValid: false + } + ], + address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m' + } + ] +} + +module.exports = { + mockUtxoData, + mockHydratedUtxos +} diff --git a/test/unit/utxo-unit.js b/test/unit/utxo-unit.js new file mode 100644 index 0000000..579788f --- /dev/null +++ b/test/unit/utxo-unit.js @@ -0,0 +1,49 @@ +/* + Unit tests for the utxo.js library. +*/ + +const sinon = require('sinon') +const assert = require('chai').assert + +const BCHJS = require('../../src/bch-js') +const bchjs = new BCHJS() + +const mockData = require('./fixtures/utxo-mocks') + +describe('#utxo', () => { + let sandbox + beforeEach(() => (sandbox = sinon.createSandbox())) + afterEach(() => sandbox.restore()) + + describe('#get', () => { + it('should get hydrated and filtered UTXOs for an address', async () => { + // Mock dependencies. + sandbox.stub(bchjs.Utxo.electrumx, 'utxo').resolves(mockData.mockUtxoData) + sandbox.stub(bchjs.Utxo.slp.Utils, 'hydrateUtxos').resolves(mockData.mockHydratedUtxos) + + const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' + + const result = await bchjs.Utxo.get(addr) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result.bchUtxos) + assert.isArray(result.slpUtxos) + assert.isArray(result.nullUtxos) + }) + + it('should catch and throw an error', async () => { + try { + // Force an error + sandbox.stub(bchjs.Utxo.electrumx, 'utxo').rejects(new Error('test error')) + + const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' + + await bchjs.Utxo.get(addr) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) +})