From fef05124f1165efcc105b6a1c38fe90d20e8c753 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 12 Feb 2021 15:58:16 -0800 Subject: [PATCH] feat(utxo.get()): Changes to accept arrays of up to 20 addresses --- src/utxo.js | 36 ++++++++--- .../chains/bchn/utxo-integration.js | 30 ++++++++- test/unit/fixtures/utxo-mocks.js | 62 ++++++++++++++++++- test/unit/utxo-unit.js | 62 +++++++++++++++++-- 4 files changed, 171 insertions(+), 19 deletions(-) diff --git a/src/utxo.js b/src/utxo.js index 85bd058..82438e7 100644 --- a/src/utxo.js +++ b/src/utxo.js @@ -79,26 +79,42 @@ class UTXO { */ async get (address) { try { - const addr = this.slp.Address.toCashAddress(address) + // Convert address to an array if it is a string. + if (typeof address === 'string') address = [address] + + // Throw an error if there are more than 20 addresses passed in at a time. + if (address.length > 20) throw new Error('Too many elements, 20 max.') + + // Covert each address to a BCH address. + const addr = address.map(elem => this.slp.Address.toCashAddress(elem)) // Get the UTXOs associated with the address. - const utxoData = await this.electrumx.utxo([addr]) + 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 + const retAry = [] // Return array - // 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. + // Loop through each address. + for (let i = 0; i < hydratedUtxos.slpUtxos.length; i++) { + const thisAddr = hydratedUtxos.slpUtxos[i] - return retObj + const addrObj = { + address: thisAddr.address + } + + // Filter out the different types of UTXOs. + addrObj.bchUtxos = thisAddr.utxos.filter(elem => elem.isValid === false) + addrObj.nullUtxos = thisAddr.utxos.filter(elem => elem.isValid === null) + addrObj.slpUtxos = thisAddr.utxos.filter(elem => elem.isValid === true) + + retAry.push(addrObj) + } + + return retAry } catch (err) { console.error('Error in bchjs.utxo.get()') throw err diff --git a/test/integration/chains/bchn/utxo-integration.js b/test/integration/chains/bchn/utxo-integration.js index ab856fc..0c78ce9 100644 --- a/test/integration/chains/bchn/utxo-integration.js +++ b/test/integration/chains/bchn/utxo-integration.js @@ -22,9 +22,33 @@ describe('#UTXO', () => { 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) + assert.isArray(result) + assert.property(result[0], 'address') + assert.property(result[0], 'bchUtxos') + assert.property(result[0], 'nullUtxos') + assert.property(result[0], 'slpUtxos') + assert.isArray(result[0].bchUtxos) + assert.isArray(result[0].slpUtxos) + assert.isArray(result[0].nullUtxos) + }) + + it('should handle an array of addresses', async () => { + const addr = [ + 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9', + 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9' + ] + + const result = await bchjs.Utxo.get(addr) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.property(result[0], 'address') + assert.property(result[0], 'bchUtxos') + assert.property(result[0], 'nullUtxos') + assert.property(result[0], 'slpUtxos') + assert.isArray(result[0].bchUtxos) + assert.isArray(result[0].slpUtxos) + assert.isArray(result[0].nullUtxos) }) }) }) diff --git a/test/unit/fixtures/utxo-mocks.js b/test/unit/fixtures/utxo-mocks.js index 994a74d..aeb519d 100644 --- a/test/unit/fixtures/utxo-mocks.js +++ b/test/unit/fixtures/utxo-mocks.js @@ -66,7 +66,67 @@ const mockHydratedUtxos = { ] } +const mockTwoHydratedAddrs = { + 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' + }, + { + utxos: [ + { + height: 602405, + tx_hash: + '2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7', + tx_pos: 0, + value: 1000, + txid: + '2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7', + vout: 0, + isValid: false + } + ], + address: 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9' + } + ] +} + module.exports = { mockUtxoData, - mockHydratedUtxos + mockHydratedUtxos, + mockTwoHydratedAddrs } diff --git a/test/unit/utxo-unit.js b/test/unit/utxo-unit.js index 579788f..4b83dbd 100644 --- a/test/unit/utxo-unit.js +++ b/test/unit/utxo-unit.js @@ -19,22 +19,31 @@ describe('#utxo', () => { 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) + 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) + assert.isArray(result) + assert.property(result[0], 'address') + assert.property(result[0], 'bchUtxos') + assert.property(result[0], 'nullUtxos') + assert.property(result[0], 'slpUtxos') + assert.isArray(result[0].bchUtxos) + assert.isArray(result[0].slpUtxos) + assert.isArray(result[0].nullUtxos) }) it('should catch and throw an error', async () => { try { // Force an error - sandbox.stub(bchjs.Utxo.electrumx, 'utxo').rejects(new Error('test error')) + sandbox + .stub(bchjs.Utxo.electrumx, 'utxo') + .rejects(new Error('test error')) const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' @@ -45,5 +54,48 @@ describe('#utxo', () => { assert.include(err.message, 'test error') } }) + + it('should handle an array of addresses', async () => { + // Mock dependencies. + sandbox.stub(bchjs.Utxo.electrumx, 'utxo').resolves(mockData.mockUtxoData) + sandbox + .stub(bchjs.Utxo.slp.Utils, 'hydrateUtxos') + .resolves(mockData.mockTwoHydratedAddrs) + + const addr = [ + 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9', + 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9' + ] + + const result = await bchjs.Utxo.get(addr) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.equal(result.length, 2) + assert.property(result[0], 'address') + assert.property(result[0], 'bchUtxos') + assert.property(result[0], 'nullUtxos') + assert.property(result[0], 'slpUtxos') + assert.isArray(result[0].bchUtxos) + assert.isArray(result[0].slpUtxos) + assert.isArray(result[0].nullUtxos) + }) + + it('should throw an error for array of 21 elements', async () => { + try { + const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' + + const addrs = [] + for (let i = 0; i < 21; i++) { + addrs.push(addr) + } + + await bchjs.Utxo.get(addrs) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'Too many elements, 20 max.') + } + }) }) })