diff --git a/package.json b/package.json index 335032a..ebe4819 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "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=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#UTXO' test/integration/chains/bchn/", - "test:temp2": "mocha --timeout=30000 -g '#utxo' test/unit/", + "test:temp2": "mocha --timeout=30000 -g '#Util' 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/util.js b/src/util.js index ab1a675..684a4c2 100644 --- a/src/util.js +++ b/src/util.js @@ -27,6 +27,129 @@ class Util { _this = this } + /** + * @api util.floor8() floor8() + * @apiName floor8 + * @apiGroup Util + * @apiDescription Round a number down to 8 decimal places. + * + * + * @apiExample Example usage: + * (async () => { + * try { + * const num = 1.234567891111 + * const result = bchjs.Util.floor8(num) + * console.log(result) + * } catch(error) { + * console.error(error) + * } + * })() + * + * // returns + * 1.23456789 + */ + // floor8 - round to 8 decimal places + // Takes a number and returns it, rounded to the nearest 8 decimal place. + floor8 (num) { + const thisNum = Number(num) + + if (isNaN(thisNum)) throw new Error('input must be a number') + + let tempNum = thisNum * 100000000 + tempNum = Math.floor(tempNum) + tempNum = tempNum / 100000000 + + return tempNum + } + + /** + * @api util.floor2() floor2() + * @apiName floor2 + * @apiGroup Util + * @apiDescription Round a number down to 2 decimal places. + * + * + * @apiExample Example usage: + * (async () => { + * try { + * const num = 1.234567891111 + * const result = bchjs.Util.floor2(num) + * console.log(result) + * } catch(error) { + * console.error(error) + * } + * })() + * + * // returns + * 1.23 + */ + // floor2 - round down to 2 decimal places + // Takes a number and returns it, rounded to the nearest 2 decimal place. + floor2 (num) { + const thisNum = Number(num) + + if (isNaN(thisNum)) throw new Error('input must be a number') + + let tempNum = thisNum * 100 + tempNum = Math.floor(tempNum) + tempNum = tempNum / 100 + + return tempNum + } + + /** + * @api util.chunk20() chunk20() + * @apiName chunk20 + * @apiGroup Util + * @apiDescription chunk up an array into multiple arrays of 20 elements each. + * Input: arrayToSlice - a one-dimensional array of elements. + * Returns a two-dimensional array. An array of 20-element arrays. + * + * @apiExample Example usage: + * (async () => { + * try { + * const bigArray = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26] + * + * const chunked = bchjs.Util.chunk20(bigArray) + * console.log(chunked) + * } catch(error) { + * console.error(error) + * } + * })() + * + * // returns + * [ + * [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19], + * [20,21,22,23,24,25,26] + * ] + */ + // chunk20 - chunk up an array into multiple arrays of 20 elements each. + // Input: arrayToSlice - a one-dimensional array of elements. + // Returns a two-dimensional array. An array of 20-element arrays. + chunk20 (arrayToSlice) { + try { + // Validate inputs + if (!Array.isArray(arrayToSlice)) { + throw new Error('input must be an array') + } + + let offset = 0 + const result = [] + + // Loop over the array and slice off chunks of 20 elements. + while (offset < arrayToSlice.length) { + const chunk = arrayToSlice.slice(offset, offset + 20) + result.push(chunk) + offset = offset + 20 + } + + return result + } catch (err) { + console.error('Error in chunk20()') + throw err + } + } + /** * @api util.validateAddress() validateAddress() * @apiName Validate Address. diff --git a/src/utxo.js b/src/utxo.js index 96d0da5..34c4149 100644 --- a/src/utxo.js +++ b/src/utxo.js @@ -279,6 +279,70 @@ class UTXO { throw err } } + + /** + * @api Utxo.findBiggestUtxo() findBiggestUtxo() + * @apiName findBiggestUtxo + * @apiGroup UTXO + * @apiDescription Get the biggest UTXO in an array. + * + * Given an array of BCH UTXOs, this method will return the biggest UTXO. + * This is often the simplest way to pick a UTXO for generating a transaction. + * + * @apiExample Example usage: + * (async () => { + * try { + * const utxos = await bchjs.Utxo.get('bitcoincash:qq54fgjn3hz0357n8a6guy4demw9xfkjk5jcj0xr0z'); + * const utxo = bchjs.Utxo.findBiggestUtxo(utxos[0].bchUtxos) + * console.log(utxo); + * } catch(error) { + * console.error(error) + * } + * })() + * + * // returns + * { + * "height": 655431, + * "tx_hash": "7a091716f8137e94f87e7760648cd34a17e32754ef95f7c7bda38a635c9b2b1b", + * "tx_pos": 0, + * "value": 800, + * "txid": "7a091716f8137e94f87e7760648cd34a17e32754ef95f7c7bda38a635c9b2b1b", + * "vout": 0, + * "isValid": false, + * "satoshis": 800 + * } + */ + // Returns the utxo with the biggest balance from an array of utxos. + findBiggestUtxo (utxos) { + let largestAmount = 0 + let largestIndex = 0 + + if (!Array.isArray(utxos)) { + throw new Error('utxos input to findBiggestUtxo() must be an array') + } + + for (let i = 0; i < utxos.length; i++) { + const thisUtxo = utxos[i] + + // Give Elecrumx utxos a satoshis property. + if (thisUtxo.value) { + if (!thisUtxo.satoshis) thisUtxo.satoshis = Number(thisUtxo.value) + } + + if (!thisUtxo.satoshis) { + throw new Error( + 'UTXOs require a satoshis or value property for findBiggestUtxo()' + ) + } + + if (thisUtxo.satoshis > largestAmount) { + largestAmount = thisUtxo.satoshis + largestIndex = i + } + } + + return utxos[largestIndex] + } } module.exports = UTXO diff --git a/test/integration/chains/bchn/utxo-integration.js b/test/integration/chains/bchn/utxo-integration.js index 771d67a..d8b6006 100644 --- a/test/integration/chains/bchn/utxo-integration.js +++ b/test/integration/chains/bchn/utxo-integration.js @@ -90,6 +90,34 @@ describe('#UTXO', () => { assert.isAbove(result[0].nullUtxos.length, 1) }) }) + + describe('#findBiggestUtxo', () => { + it('should sort UTXOs from Electrumx', async () => { + const addr = 'bitcoincash:qq54fgjn3hz0357n8a6guy4demw9xfkjk5jcj0xr0z' + + const electrumxUtxos = await bchjs.Electrumx.utxo(addr) + // console.log(`Electrumx utxos: ${JSON.stringify(electrumxUtxos, null, 2)}`) + + const result = bchjs.Utxo.findBiggestUtxo(electrumxUtxos.utxos) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'satoshis') + assert.equal(result.satoshis, 800) + }) + + it('should sort UTXOs from Utxos.get()', async () => { + const addr = 'bitcoincash:qq54fgjn3hz0357n8a6guy4demw9xfkjk5jcj0xr0z' + + const utxos = await bchjs.Utxo.get(addr) + // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) + + const result = bchjs.Utxo.findBiggestUtxo(utxos[0].bchUtxos) + console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'satoshis') + assert.equal(result.satoshis, 800) + }) + }) }) function sleep (ms) { diff --git a/test/unit/fixtures/utxo-mocks.js b/test/unit/fixtures/utxo-mocks.js index e261c59..72094c5 100644 --- a/test/unit/fixtures/utxo-mocks.js +++ b/test/unit/fixtures/utxo-mocks.js @@ -253,9 +253,37 @@ const mockEveryUtxoType = { ] } +const electrumxUtxos = { + success: true, + utxos: [ + { + height: 0, + tx_hash: + '192f5037bb3822afd92d6b6ab51842a5dcfbe6bff783290057342da1f27ed414', + tx_pos: 0, + value: 600 + }, + { + height: 0, + tx_hash: + 'f913646f7c180f8de020cb1387951272e8a3f764a0f88e68f7fc1145a0bf02e9', + tx_pos: 0, + value: 700 + }, + { + height: 0, + tx_hash: + '7a091716f8137e94f87e7760648cd34a17e32754ef95f7c7bda38a635c9b2b1b', + tx_pos: 0, + value: 800 + } + ] +} + module.exports = { mockUtxoData, mockHydratedUtxos, mockTwoHydratedAddrs, - mockEveryUtxoType + mockEveryUtxoType, + electrumxUtxos } diff --git a/test/unit/util.js b/test/unit/util.js index dda4500..f40b4c6 100644 --- a/test/unit/util.js +++ b/test/unit/util.js @@ -1,4 +1,5 @@ const assert = require('assert') +const assert2 = require('chai').assert const axios = require('axios') const BCHJS = require('../../src/bch-js') const bchjs = new BCHJS() @@ -32,4 +33,143 @@ describe('#Util', () => { .then(done, done) }) }) + + describe('#floor8', () => { + it('should floor a number to 8 decimals', () => { + const num = 1.234567891111 + + const result = bchjs.Util.floor8(num) + // console.log(result) + + assert2.equal(result, 1.23456789) + }) + + it('should throw an error for non-number input', () => { + try { + const num = 'string' + + bchjs.Util.floor8(num) + + assert2.equal(true, false, 'Unexpected result') + } catch (err) { + // console.log(err) + assert2.include(err.message, 'input must be a number') + } + }) + + it('should not effect a number with less than 8 decimals', () => { + const num = 1.23 + + const result = bchjs.Util.floor8(num) + // console.log(result) + + assert2.equal(result, 1.23) + }) + }) + + describe('#floor2', () => { + it('should round a number to 2 decimals', () => { + const num = 1.234567891111 + + const result = bchjs.Util.floor2(num) + // console.log(result) + + assert2.equal(result, 1.23) + }) + + it('should throw an error for non-number input', () => { + try { + const num = 'string' + + bchjs.Util.floor2(num) + + assert2.equal(true, false, 'Unexpected result') + } catch (err) { + // console.log(err) + assert2.include(err.message, 'input must be a number') + } + }) + + it('should not effect a number with less than 8 decimals', () => { + const num = 1.2 + + const result = bchjs.Util.floor2(num) + // console.log(result) + + assert2.equal(result, 1.2) + }) + }) + + describe('#chunk20', () => { + const thirtyFiveElements = [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34 + ] + + it('should split 35 elements into 2 arrays', () => { + const result = bchjs.Util.chunk20(thirtyFiveElements) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert2.isArray(result) + assert2.isArray(result[0]) + assert2.equal(result.length, 2) + assert2.equal(result[0].length, 20) + }) + + it('should return accurately with an array of less than 20 elements', () => { + const elems = [0, 1, 2, 3, 4, 5] + + const result = bchjs.Util.chunk20(elems) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert2.isArray(result) + assert2.isArray(result[0]) + assert2.equal(result.length, 1) + assert2.equal(result[0].length, 6) + }) + + it('should throw an error for non-array input', () => { + try { + bchjs.Util.chunk20('string') + + assert2.equal(true, false, 'Unexpected result') + } catch (err) { + // console.log(err) + assert2.include(err.message, 'input must be an array') + } + }) + }) }) diff --git a/test/unit/utxo-unit.js b/test/unit/utxo-unit.js index a331f38..3992f65 100644 --- a/test/unit/utxo-unit.js +++ b/test/unit/utxo-unit.js @@ -145,4 +145,47 @@ describe('#utxo', () => { assert.isArray(result[0].nullUtxos) }) }) + + describe('#findBiggestUtxo', () => { + it('should throw error for non-array input', async () => { + try { + await bchjs.Utxo.findBiggestUtxo({}) + + assert.equal(true, false, 'Unexpected result!') + } catch (err) { + assert.include( + err.message, + 'utxos input to findBiggestUtxo() must be an array' + ) + } + }) + + it('should throw an error if input does not have a value or satoshis property', async () => { + try { + const badUtxos = [ + { + height: 0, + tx_hash: + '192f5037bb3822afd92d6b6ab51842a5dcfbe6bff783290057342da1f27ed414', + tx_pos: 0 + } + ] + + await bchjs.Utxo.findBiggestUtxo(badUtxos) + } catch (err) { + assert.include( + err.message, + 'UTXOs require a satoshis or value property for findBiggestUtxo()' + ) + } + }) + + it('should sort UTXOs from Electrumx', async () => { + const result = bchjs.Utxo.findBiggestUtxo(mockData.electrumxUtxos.utxos) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, 'satoshis') + assert.equal(result.satoshis, 800) + }) + }) })