From b80697500cfb31b52484ac7f32b7241500881e0e Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Mon, 8 Feb 2021 09:43:25 -0800 Subject: [PATCH 1/3] Adding integration test script to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1d6db25..d63c7bc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules/* wallet-info.txt wallet.json integration-test-sweet.sh +integration-test-ss.sh docs/ coverage/ From 687b5bca02f2688b5674fbdf612acc0adc0db4ff Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Mon, 8 Feb 2021 10:45:24 -0800 Subject: [PATCH 2/3] feat(waterfallValidateTxid): Adding a new waterfall SLP validation method --- package.json | 2 +- src/slp/utils.js | 109 ++++++++++++++++++++++++++++++++++++++++ test/integration/slp.js | 21 ++++++++ 3 files changed, 131 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8406a97..3bd5509 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 '#getTxDataSlp' test/integration/", + "test:temp": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#waterfallValidateTxid' test/integration/", "test:temp2": "mocha --timeout=30000 -g '#getTxData' 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 b11a597..37090ff 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -1426,6 +1426,115 @@ class Utils { } } + // This function aggregates all the available SLP token validation sources. + // It starts with the fastest, most-efficient source first, and continues + // to other validation sources until the txid is validated (true or false). + // If the txid goes through all sources and can't be validated, it will + // return null. + // + // Validation sources from most efficient to least efficient: + // - SLPDB whitelist + // - SLPDB general + // - slp-api + // + // Currently only supports a single txid at a time. + async waterfallValidateTxid (txid) { + try { + const cachedTxValidation = {} + + // If the value has been cached, use the cached version first. + let isValid = cachedTxValidation[txid] + if (!isValid) { + isValid = null + } else { + return isValid + } + + // There are two possible responses from SLPDB. If SLPDB is functioning + // correctly, then validateTxid() will return this: + // isValid: [ + // { + // "txid": "ff0c0354f8d3ddb34fa36f73494eb58ea24f8b8da6904aa8ed43b7a74886c583", + // "valid": true + // } + // ] + // + // If SLPDB has fallen behind real-time processing, it will return this: + // isValid: [ + // null + // ] + // + // Note: validateTxid3() has the same output as validateTxid(). + // validateTxid2() uses slp-validate, which has a different output format. + + // Validate against the whitelist SLPDB first. + const whitelistResult = await this.validateTxid3(txid) + // console.log( + // `whitelist-SLPDB for ${txid}: ${JSON.stringify( + // isValid, + // null, + // 2 + // )}` + // ) + + // Safely retrieve the returned value. + if (whitelistResult[0] !== null) isValid = whitelistResult[0].valid + + // Exit if isValid is not null. + if (isValid !== null) { + // Save to the cache. + cachedTxValidation[txid] = isValid + + return isValid + } + + // Try the general SLPDB, if the whitelist returned null. + const generalResult = await this.validateTxid(txid) + // console.log( + // `validateTxid() isValid: ${JSON.stringify(isValid, null, 2)}` + // ) + + // Safely retrieve the returned value. + if (generalResult[0] === null) isValid = generalResult[0].valid + + // Exit if isValid is not null. + if (isValid !== null) { + // Save to the cache. + cachedTxValidation[txid] = isValid + + return isValid + } + + // If still null, as a last resort, check it against slp-validate + let slpValidateResult = null + try { + slpValidateResult = await this.validateTxid2(txid) + } catch (err) { + /* exit quietly */ + } + // console.log( + // `slp-validate isValid: ${JSON.stringify(isValid, null, 2)}` + // ) + + // Exit if isValid is not null. + if (slpValidateResult !== null) { + isValid = slpValidateResult.isValid + + // Save to the cache. + cachedTxValidation[txid] = isValid + + return isValid + } + + // If isValid is still null, return that value, signaling that the txid + // could not be validated. + return isValid + } catch (error) { + if (error.response && error.response.data) throw error.response.data + throw error + } + } + /** * @api SLP.Utils.hydrateUtxos() hydrateUtxos() * @apiName hydrateUtxos diff --git a/test/integration/slp.js b/test/integration/slp.js index 405fcd7..3b03253 100644 --- a/test/integration/slp.js +++ b/test/integration/slp.js @@ -735,6 +735,27 @@ describe('#SLP', () => { assert.property(result, 'slpProcessedBlockHeight') }) }) + + describe('#waterfallValidateTxid', () => { + it('should validate known good txid not in whitelist', async () => { + const txid = '3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488' + + const result = await bchjs.SLP.Utils.waterfallValidateTxid(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result, true) + }) + + it('should invalidate a known invalid TXID', async () => { + const txid = + 'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a' + + const result = await bchjs.SLP.Utils.waterfallValidateTxid(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result, false) + }) + }) }) describe('#tokentype1', () => { From 3b873b6064a6fe720c779a789567b863b93107f0 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Mon, 8 Feb 2021 17:10:32 -0800 Subject: [PATCH 3/3] feat(waterfallValidateTxid): SLP.Utils.waterfallValidateTxid(txid) --- package.json | 2 +- src/slp/utils.js | 57 ++++++++++++++++++++-------- test/unit/slp-utils.js | 86 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 126 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 3bd5509..ff7b565 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "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 '#waterfallValidateTxid' test/integration/", - "test:temp2": "mocha --timeout=30000 -g '#getTxData' test/unit/", + "test:temp2": "mocha --timeout=30000 -g '#waterfallValidateTxid' 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/slp/utils.js b/src/slp/utils.js index 37090ff..f62575e 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -1426,18 +1426,43 @@ class Utils { } } - // This function aggregates all the available SLP token validation sources. - // It starts with the fastest, most-efficient source first, and continues - // to other validation sources until the txid is validated (true or false). - // If the txid goes through all sources and can't be validated, it will - // return null. - // - // Validation sources from most efficient to least efficient: - // - SLPDB whitelist - // - SLPDB general - // - slp-api - // - // Currently only supports a single txid at a time. + /** + * @api SLP.Utils.waterfallValidateTxid() waterfallValidateTxid() + * @apiName waterfallValidateTxid + * @apiGroup SLP Utils + * @apiDescription Use multiple validators to validate an SLP TXID. + * + * This function aggregates all the available SLP token validation sources. + * It starts with the fastest, most-efficient source first, and continues + * to other validation sources until the txid is validated (true or false). + * If the txid goes through all sources and can't be validated, it will + * return null. + * + * Validation sources from most efficient to least efficient: + * - SLPDB with whitelist filter + * - SLPDB general purpose + * - slp-api + * + * Currently only supports a single txid at a time. + * + * @apiExample Example usage: + * + * // validate single SLP txid + * (async () => { + * try { + * let validated = await bchjs.SLP.Utils.waterfallValidateTxid( + * "df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb" + * ); + * console.log(validated); + * } catch (error) { + * console.error(error); + * } + * })(); + * + * // returns + * true + */ + async waterfallValidateTxid (txid) { try { const cachedTxValidation = {} @@ -1471,7 +1496,7 @@ class Utils { const whitelistResult = await this.validateTxid3(txid) // console.log( // `whitelist-SLPDB for ${txid}: ${JSON.stringify( - // isValid, + // whitelistResult, // null, // 2 // )}` @@ -1491,11 +1516,11 @@ class Utils { // Try the general SLPDB, if the whitelist returned null. const generalResult = await this.validateTxid(txid) // console.log( - // `validateTxid() isValid: ${JSON.stringify(isValid, null, 2)}` + // `validateTxid() isValid: ${JSON.stringify(generalResult, null, 2)}` // ) // Safely retrieve the returned value. - if (generalResult[0] === null) isValid = generalResult[0].valid + if (generalResult[0] !== null) isValid = generalResult[0].valid // Exit if isValid is not null. if (isValid !== null) { @@ -1513,7 +1538,7 @@ class Utils { /* exit quietly */ } // console.log( - // `slp-validate isValid: ${JSON.stringify(isValid, null, 2)}` + // `slpValidateResult: ${JSON.stringify(slpValidateResult, null, 2)}` // ) // Exit if isValid is not null. diff --git a/test/unit/slp-utils.js b/test/unit/slp-utils.js index 1ac210c..8e78438 100644 --- a/test/unit/slp-utils.js +++ b/test/unit/slp-utils.js @@ -1855,7 +1855,8 @@ describe('#SLP Utils', () => { .throws({ error: 'TXID not found' }) } - const txid = 'd284e71227ec89f714b964d8eda595be6392bebd2fac46082bc5a9ce6fb7b33e' + const txid = + 'd284e71227ec89f714b964d8eda595be6392bebd2fac46082bc5a9ce6fb7b33e' await uut.Utils.txDetails(txid) // console.log(`result: ${JSON.stringify(result, null, 2)}`) @@ -1875,7 +1876,8 @@ describe('#SLP Utils', () => { .resolves({ data: mockData.mockTxDetails }) } - const txid = '9dbaaafc48c49a21beabada8de632009288a2cd52eecefd0c00edcffca9955d0' + const txid = + '9dbaaafc48c49a21beabada8de632009288a2cd52eecefd0c00edcffca9955d0' const result = await uut.Utils.txDetails(txid) // console.log(`result: ${JSON.stringify(result, null, 2)}`) @@ -2055,6 +2057,86 @@ describe('#SLP Utils', () => { */ }) + describe('#waterfallValidateTxid', () => { + // This test ensures that the whitelist SLPDB is called before the + // general purpose SLPDB. + it('should call validateTxid3() first', async () => { + const txid = 'fakeTxid' + + const retVal = [{ txid, valid: true }] + + // Use stubs to force the desired code path. + sandbox.stub(uut.Utils, 'validateTxid3').resolves(retVal) + sandbox + .stub(uut.Utils, 'validateTxid') + .rejects(new Error('Unexpected code path')) + sandbox + .stub(uut.Utils, 'validateTxid2') + .rejects(new Error('Unexpected code path')) + + const result = await uut.Utils.waterfallValidateTxid(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result, true) + }) + + // This test ensures that the general purpose SLPDB is called after the + // whitelist SLPDB, but before slp-api. + it('should call validateTxid() second', async () => { + const txid = 'fakeTxid' + + const retVal1 = [{ txid, valid: null }] + const retVal2 = [{ txid, valid: true }] + + // Use stubs to force the desired code path. + sandbox.stub(uut.Utils, 'validateTxid3').resolves(retVal1) + sandbox.stub(uut.Utils, 'validateTxid').resolves(retVal2) + sandbox + .stub(uut.Utils, 'validateTxid2') + .rejects(new Error('Unexpected code path')) + + const result = await uut.Utils.waterfallValidateTxid(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result, true) + }) + + // This test ensures that slp-api is called if both SLPDBs return null. + it('should call slp-api last', async () => { + const txid = 'fakeTxid' + + const retVal1 = [{ txid, valid: null }] + + // Use stubs to force the desired code path. + sandbox.stub(uut.Utils, 'validateTxid3').resolves(retVal1) + sandbox.stub(uut.Utils, 'validateTxid').resolves(retVal1) + sandbox.stub(uut.Utils, 'validateTxid2').resolves({ isValid: true }) + + const result = await uut.Utils.waterfallValidateTxid(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result, true) + }) + + it('catches and throws an error', async () => { + try { + const txid = 'fakeTxid' + + // Force an error + sandbox + .stub(uut.Utils, 'validateTxid3') + .rejects(new Error('fake error')) + + await await uut.Utils.waterfallValidateTxid(txid) + + assert.fail('Unexpected result') + } catch (err) { + // console.log(`err.message: ${err.message}`) + assert.include(err.message, 'fake error') + } + }) + }) + describe('#getWhitelist', () => { it('should return the list', async () => { sandbox