From da3af3656a8d0a2afc1b0ca2521d638f3705462b Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 9 Feb 2021 12:34:25 -0800 Subject: [PATCH] feat(hydrateUtxosWL): hydrateUtxo but uses SLPDB with whitelist filter --- package.json | 2 +- src/slp/utils.js | 39 ++++++++++++++++- test/integration/slp.js | 93 +++++++++++++++++++++++++++++++++++++++++ test/unit/slp-utils.js | 15 +++++++ 4 files changed, 147 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index aaeee1c..36b875a 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "test:integration:local": "RESTURL=http://localhost:3000/v4/ mocha --timeout 30000 test/integration", "test:integration:local:bchn": "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/testnet", - "test:temp": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#tokenUtxoDetailsWL' test/integration/", + "test:temp": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#hydrateUtxosWL' test/integration/", "test:temp2": "mocha --timeout=30000 -g '#tokenUtxoDetailsWL' test/unit/", "coverage": "nyc report --reporter=text-lcov | coveralls", "coverage:report": "nyc --reporter=html mocha --timeout 25000 test/unit/", diff --git a/src/slp/utils.js b/src/slp/utils.js index 40325e4..23ff3e5 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -602,7 +602,8 @@ class Utils { * @api SLP.Utils.validateTxid3() validateTxid3() * @apiName validateTxid3 * @apiGroup SLP Utils - * @apiDescription Validate that txid is an SLP transaction using the SLPDB whitelist server. + * @apiDescription + * Validate that txid is an SLP transaction using the SLPDB whitelist server. * Same exact functionality as the validateTxid() function, but this function * calls the whitelist SLPDB. It will only validate SLP tokens that are in the * whitelist. You can retrieve the whitelist with the SLP.Utils.whitelist() @@ -1728,6 +1729,42 @@ class Utils { } } + /** + * @api SLP.Utils.hydrateUtxosWL() hydrateUtxosWL() + * @apiName hydrateUtxosWL + * @apiGroup SLP Utils + * @apiDescription + * This call is exactly the same as `hydrateUtxos()`. This version hydrate a + * UTXO with SLP token metadata, but only uses the whitelist SLPDB for + * validation. + * + * Whitelist SLPDBs will return `isValid: null` for any token not in the + * 'whitelist' filter. Filtered SLPDBs are much smaller and more reliable + * to operate. + * + */ + // Same as tokenUtxoDetailsWL(), but reduces API calls by having bch-api server + // do the heavy lifting. + async hydrateUtxosWL (utxos) { + try { + // Throw error if input is not an array. + if (!Array.isArray(utxos)) throw new Error('Input must be an array.') + + const response = await _this.axios.post( + `${this.restURL}slp/hydrateUtxosWL`, + { + utxos: utxos + }, + _this.axiosOptions + ) + + return response.data + } catch (error) { + if (error.response && error.response.data) throw error.response.data + else throw error + } + } + /** * @api SLP.Utils.getStatus() getStatus() * @apiName getStatus diff --git a/test/integration/slp.js b/test/integration/slp.js index d77adc2..80dc629 100644 --- a/test/integration/slp.js +++ b/test/integration/slp.js @@ -766,6 +766,99 @@ describe('#SLP', () => { }) }) + describe('#hydrateUtxosWL', () => { + it('should hydrate UTXOs', async () => { + const utxos = [ + { + utxos: [ + { + txid: + '89b3f0c84efe8b01b24e2d7ac08636de5781f31dbb84478e3de868ca0a7ed93a', + vout: 1, + value: 546 + } + ] + } + ] + + const result = await bchjs.SLP.Utils.hydrateUtxosWL(utxos) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // Test the general structure of the output. + assert.isArray(result.slpUtxos) + assert.equal(result.slpUtxos.length, 1) + assert.equal(result.slpUtxos[0].utxos.length, 1) + + // Test the non-slp UTXO. + assert.property(result.slpUtxos[0].utxos[0], 'txid') + assert.property(result.slpUtxos[0].utxos[0], 'vout') + assert.property(result.slpUtxos[0].utxos[0], 'value') + assert.property(result.slpUtxos[0].utxos[0], 'isValid') + assert.equal(result.slpUtxos[0].utxos[0].isValid, true) + }) + + it('should process data directly from Electrumx', async () => { + const addrs = [ + 'bitcoincash:qqnt53cfw990u6y38xgezm0lfa8hknw0wueezcpagp', + 'bitcoincash:qrh3qgtax6adegzc7zxm6m4sgrv9wq28yqnfn33ce5', + 'bitcoincash:qqcaee5w4ws77n8s2gzy6fwtma6uxd7ctq8553e495' + ] + + const utxos = await bchjs.Electrumx.utxo(addrs) + // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) + + const result = await bchjs.SLP.Utils.hydrateUtxosWL(utxos.utxos) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // Test the general structure of the output. + assert.isArray(result.slpUtxos) + assert.equal(result.slpUtxos.length, 3) + assert.equal(result.slpUtxos[0].utxos.length, 1) + assert.equal(result.slpUtxos[1].utxos.length, 1) + assert.equal(result.slpUtxos[2].utxos.length, 2) + + try { + // Test the expected values. + assert.equal(result.slpUtxos[0].utxos[0].isValid, false) + assert.equal(result.slpUtxos[1].utxos[0].isValid, true) + assert.equal(result.slpUtxos[1].utxos[0].tokenTicker, 'TROUT') + assert.equal(result.slpUtxos[2].utxos[0].isValid, false) + assert.equal(result.slpUtxos[2].utxos[1].isValid, null) + assert.equal(result.slpUtxos[2].utxos[1].tokenTicker, 'VALENTINE') + } catch (err) { + console.error( + 'The hydrateUtxos call may hitting rate limits, or SLPDB may be having issues if "isValid" results are "null"' + ) + console.log('Error: ', err) + } + }) + + it('should handle null SLPDB validations', async () => { + const utxos = [ + { + height: 665577, + tx_hash: + '4b89405c54d1c0bde8aa476a47561a42a6e7a5e927daa2ec69d428810eae3419', + tx_pos: 1, + value: 546 + }, + { + height: 665577, + tx_hash: + 'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a', + tx_pos: 1, + value: 546 + } + ] + + const data = await bchjs.SLP.Utils.hydrateUtxosWL([{ utxos: utxos }]) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert.isArray(data.slpUtxos) + assert.equal(data.slpUtxos[0].isValid, null) + }) + }) + describe('#validateTxid', () => { // This test is not necessary. it('should handle a null response from SLPDB', async () => { diff --git a/test/unit/slp-utils.js b/test/unit/slp-utils.js index 4eb7b31..8da8632 100644 --- a/test/unit/slp-utils.js +++ b/test/unit/slp-utils.js @@ -1716,6 +1716,21 @@ describe('#SLP Utils', () => { }) }) + describe('#hydrateUtxosWL', () => { + it('should throw an error if input is not an array', async () => { + try { + const utxos = 1234 + + await uut.Utils.hydrateUtxosWL(utxos) + + assert.equal(true, false, 'Uh oh. Code path should not end here.') + } catch (err) { + // console.log(`Error: `, err) + assert.include(err.message, 'Input must be an array.') + } + }) + }) + describe('#validateTxid2', () => { it('should throw an error if the input is an array', async () => { try {