From 79ea4b77b982ddf77e42759b70c135c97dc4d447 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Mon, 8 Feb 2021 17:25:08 -0800 Subject: [PATCH 1/4] fix(tokenUtxoDetails): Replacing validation with waterfallValidateTxid() --- src/slp/utils.js | 104 +---------------------------------------------- 1 file changed, 2 insertions(+), 102 deletions(-) diff --git a/src/slp/utils.js b/src/slp/utils.js index f62575e..3ca596b 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -1308,108 +1308,8 @@ class Utils { if (!isValid) isValid = null // let isValid = null - // 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. - - // If not in the cache, try the general SLPDB. - if (isValid === null) { - // console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`) - isValid = await this.validateTxid(utxo.txid) - // console.log( - // `validateTxid() isValid: ${JSON.stringify(isValid, null, 2)}` - // ) - - // Handle corner case where SLPDB returns an array with a null element. - if (isValid[0] === null) { - isValid = [{ txid: utxo.txid, valid: null }] - } - - isValid = isValid[0].valid - - // Save the result to the local cache. - // cachedTxValidation[utxo.txid] = isValid - } - // console.log(`isValid: ${JSON.stringify(isValid, null, 2)}`) - - // console.log( - // `pre-validateTxid3() isValid: ${JSON.stringify(isValid, null, 2)}` - // ) - - // If still null, check the whitelist SLPDB - if (isValid === null) { - // console.log( - // `checking against whitelist SLPDB. outAry[${i}]: ${JSON.stringify( - // outAry[i], - // null, - // 2 - // )}` - // ) - - // Figure out if the token UTXO is in the whitelist. - let utxoInWhitelist = false - for (let j = 0; j < this.whitelist.length; j++) { - if (outAry[i].tokenId === this.whitelist[j].tokenId) { - utxoInWhitelist = true - break - } - } - - // If the utxo.tokenId is in the whitelist, check the validity with - // the whitelist SLPDB. This should still be functioning properly - // if the general SLPDB is not. - if (utxoInWhitelist) { - isValid = await this.validateTxid3(utxo.txid) - - // console.log( - // `whitelist-SLPDB for ${utxo.txid}: ${JSON.stringify( - // isValid, - // null, - // 2 - // )}` - // ) - - if (isValid[0] !== null) isValid = isValid[0].valid - - // Save the result to the local cache. - // cachedTxValidation[utxo.txid] = isValid - } - } - - // console.log( - // `pre-validateTxid2() isValid: ${JSON.stringify(isValid, null, 2)}` - // ) - - // If still null, as a last resort, check it against slp-validate - if (isValid === null) { - try { - isValid = await this.validateTxid2(utxo.txid) - } catch (err) { - // Mark as invalid if validateTxid2() throws an error. - isValid = null - } - // console.log( - // `slp-validate isValid: ${JSON.stringify(isValid, null, 2)}` - // ) - - if (isValid !== null) isValid = isValid.isValid - - // Save the result to the local cache. - // cachedTxValidation[utxo.txid] = isValid - } + // Validate using a 'waterfall' of validators. + isValid = await this.waterfallValidateTxid(utxo.txid) // console.log(`isValid: ${JSON.stringify(isValid, null, 2)}`) outAry[i].isValid = isValid From 28f563b58ca628c8077e397db419298705585d8c Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Mon, 8 Feb 2021 19:17:50 -0800 Subject: [PATCH 2/4] Updating unit tests to reflect use of waterfallValidateTxid() --- test/unit/slp-utils.js | 276 +++-------------------------------------- 1 file changed, 20 insertions(+), 256 deletions(-) diff --git a/test/unit/slp-utils.js b/test/unit/slp-utils.js index 8e78438..5bf0d48 100644 --- a/test/unit/slp-utils.js +++ b/test/unit/slp-utils.js @@ -569,14 +569,7 @@ describe('#SLP Utils', () => { // // change UTXO will contain the same SLP txid, but it is not an SLP UTXO. it('should return details on minting baton from genesis transaction', async () => { // Mock the call to REST API - // Stub the call to validateTxid - sandbox.stub(uut.Utils, 'validateTxid').resolves([ - { - txid: - 'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90', - valid: true - } - ]) + sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) // Stub the calls to decodeOpReturn. sandbox.stub(uut.Utils, 'decodeOpReturn').resolves({ @@ -675,13 +668,7 @@ describe('#SLP Utils', () => { }) // Stub the call to validateTxid - sandbox.stub(uut.Utils, 'validateTxid').resolves([ - { - txid: - 'cf4b922d1e1aa56b52d752d4206e1448ea76c3ebe69b3b97d8f8f65413bd5c76', - valid: true - } - ]) + sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) const utxos = [ { @@ -746,14 +733,7 @@ describe('#SLP Utils', () => { qty: '2100000000000000' }) - // Stub the call to validateTxid - sandbox.stub(uut.Utils, 'validateTxid').resolves([ - { - txid: - 'fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb', - valid: true - } - ]) + sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) const utxos = [ { @@ -790,9 +770,10 @@ describe('#SLP Utils', () => { it('should handle BCH and SLP utxos in the same TX', async () => { // Mock external dependencies. - sandbox - .stub(uut.Utils, 'validateTxid') - .resolves(mockData.mockDualValidation) + // sandbox + // .stub(uut.Utils, 'validateTxid') + // .resolves(mockData.mockDualValidation) + sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) sandbox .stub(uut.Utils, 'decodeOpReturn') @@ -898,14 +879,7 @@ describe('#SLP Utils', () => { qty: '2000000000000000000' }) - // Stub the call to validateTxid - sandbox.stub(uut.Utils, 'validateTxid').resolves([ - { - txid: - '67fd3c7c3a6eb0fea9ab311b91039545086220f7eeeefa367fa28e6e43009f19', - valid: true - } - ]) + sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) const utxos = [ { @@ -1017,14 +991,7 @@ describe('#SLP Utils', () => { // Stub the calls to decodeOpReturn. sandbox.stub(uut.Utils, 'decodeOpReturn').resolves(slpData) - // Stub the call to validateTxid - sandbox.stub(uut.Utils, 'validateTxid').resolves([ - { - txid: - 'd2ec6abff5d1c8ed9ab5db6d140dcaebb813463e42933a4a4db171e7222a0954', - valid: true - } - ]) + sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) const utxos = [ { @@ -1095,14 +1062,6 @@ describe('#SLP Utils', () => { qty: '10000000000' } - const stubValid = [ - { - txid: - '880587f01e3112e779c0fdf1b9b859c242a28e56ead85483eeedcaa52f051a04', - valid: true - } - ] - // Mock external dependencies. // Stub the calls to decodeOpReturn. sandbox @@ -1118,11 +1077,12 @@ describe('#SLP Utils', () => { .resolves(slpData) // Stub the call to validateTxid - sandbox - .stub(uut.Utils, 'validateTxid') - .resolves(stubValid) - .onCall(1) - .resolves(stubValid) + // sandbox + // .stub(uut.Utils, 'validateTxid') + // .resolves(stubValid) + // .onCall(1) + // .resolves(stubValid) + sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) const utxos = [ { @@ -1184,14 +1144,6 @@ describe('#SLP Utils', () => { qty: '1' } - const stubValid = [ - { - txid: - '4ef6eb92950a13a69e97c2c02c7967d806aa874c0e2a6b5546a8880f2cd14bc4', - valid: true - } - ] - // Mock external dependencies. // Stub the calls to decodeOpReturn. sandbox @@ -1206,14 +1158,7 @@ describe('#SLP Utils', () => { .onCall(4) .resolves(slpData) - // Stub the call to validateTxid - sandbox - .stub(uut.Utils, 'validateTxid') - .resolves(stubValid) - .onCall(1) - .resolves(stubValid) - .onCall(2) - .resolves(stubValid) + sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) const utxos = [ { @@ -1286,14 +1231,6 @@ describe('#SLP Utils', () => { qty: '1' } - const stubValid = [ - { - txid: - '35846676e7514658bbd2fd60b1f0d4d86195908f6b2de5328d54c8e4a2d05919', - valid: true - } - ] - // Mock external dependencies. // Stub the calls to decodeOpReturn. sandbox @@ -1308,14 +1245,7 @@ describe('#SLP Utils', () => { .onCall(4) .resolves(genesisData) - // Stub the call to validateTxid - sandbox - .stub(uut.Utils, 'validateTxid') - .resolves(stubValid) - .onCall(1) - .resolves(stubValid) - .onCall(2) - .resolves(stubValid) + sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) const utxos = [ { @@ -1379,14 +1309,6 @@ describe('#SLP Utils', () => { qty: '1' } - const stubValid = [ - { - txid: - '9b6db26b64aedcedc0bd9a3037b29b3598573ec5cea99eec03faa838616cd683', - valid: true - } - ] - // Mock external dependencies. // Stub the calls to decodeOpReturn. sandbox @@ -1395,8 +1317,7 @@ describe('#SLP Utils', () => { .onCall(1) .resolves(slpData) - // Stub the call to validateTxid - sandbox.stub(uut.Utils, 'validateTxid').resolves(stubValid) + sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) const utxos = [ { @@ -1453,14 +1374,6 @@ describe('#SLP Utils', () => { qty: '1' } - const stubValid = [ - { - txid: - '6d68a7ffbb63ef851c43025f801a1d365cddda50b00741bca022c743d74cd61a', - valid: true - } - ] - // Mock external dependencies. // Stub the calls to decodeOpReturn. sandbox @@ -1471,8 +1384,7 @@ describe('#SLP Utils', () => { .onCall(2) .resolves(slpData) - // Stub the call to validateTxid - sandbox.stub(uut.Utils, 'validateTxid').resolves(stubValid) + sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) const utxos = [ { @@ -1530,14 +1442,6 @@ describe('#SLP Utils', () => { qty: '1' } - const stubValid = [ - { - txid: - '57cc47c265ce878679e95e2cec510d8a1a9840f5c62feb4743cc5947d57d9766', - valid: true - } - ] - // Mock external dependencies. // Stub the calls to decodeOpReturn. sandbox @@ -1552,12 +1456,7 @@ describe('#SLP Utils', () => { .onCall(4) .resolves(slpData) - // Stub the call to validateTxid - sandbox - .stub(uut.Utils, 'validateTxid') - .resolves(stubValid) - .onCall(1) - .resolves(stubValid) + sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) const utxos = [ { @@ -1695,141 +1594,6 @@ describe('#SLP Utils', () => { assert.equal(data[0].isValid, false) }) - - it('should use backup validators when regular SLPDB returns null', async () => { - // Mock the call to REST API - // Stub the calls to decodeOpReturn. - sandbox - .stub(uut.Utils, 'decodeOpReturn') - .onCall(0) - .resolves({ - tokenType: 1, - txType: 'SEND', - tokenId: - '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7', - amounts: ['200000000', '99887500000000'] - }) - .onCall(1) - .resolves({ - tokenType: 1, - txType: 'GENESIS', - ticker: 'TOK-CH', - name: 'TokyoCash', - tokenId: - '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7', - documentUri: '', - documentHash: '', - decimals: 8, - mintBatonVout: 0, - qty: '2100000000000000' - }) - - // Force default SLPDB to return 'null', which should trigger usage of the - // backup whitelist-SLPDB. - sandbox.stub(uut.Utils, 'validateTxid').resolves([ - { - txid: - 'fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb', - valid: null - } - ]) - - // Force the token UTXO to be in the whitelist. - uut.Utils.whitelist = mockData.whitelist - - // Mock the response from the whitelist-SLPDB. - sandbox.stub(uut.Utils, 'validateTxid3').resolves([ - { - txid: - 'fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb', - valid: null - } - ]) - - // Mock the response from slp-api - sandbox.stub(uut.Utils, 'validateTxid2').resolves({ - txid: - 'fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb', - isValid: true, - msg: '' - }) - - const utxos = [ - { - txid: - 'fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb', - vout: 1, - amount: 0.00000546, - satoshis: 546, - height: 596089, - confirmations: 748 - } - ] - - const data = await uut.Utils.tokenUtxoDetails(utxos) - // console.log(`data: ${JSON.stringify(data, null, 2)}`) - - // The UTXO should have been validated by the third validation source. - assert.equal(data[0].isValid, true) - }) - - it('should handle SLPDB returning null corner case', async () => { - // Mock the call to REST API - // Stub the calls to decodeOpReturn. - sandbox - .stub(uut.Utils, 'decodeOpReturn') - .onCall(0) - .resolves({ - tokenType: 1, - txType: 'SEND', - tokenId: - '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7', - amounts: ['200000000', '99887500000000'] - }) - .onCall(1) - .resolves({ - tokenType: 1, - txType: 'GENESIS', - ticker: 'TOK-CH', - name: 'TokyoCash', - tokenId: - '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7', - documentUri: '', - documentHash: '', - decimals: 8, - mintBatonVout: 0, - qty: '2100000000000000' - }) - - // Force default SLPDB to return 'null' corner case. - sandbox.stub(uut.Utils, 'validateTxid').resolves([null]) - - // Mock the response from slp-api - sandbox.stub(uut.Utils, 'validateTxid2').resolves({ - txid: - 'fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb', - isValid: true, - msg: '' - }) - - const utxos = [ - { - txid: - 'fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb', - vout: 1, - amount: 0.00000546, - satoshis: 546, - height: 596089, - confirmations: 748 - } - ] - - const data = await uut.Utils.tokenUtxoDetails(utxos) - // console.log(`data: ${JSON.stringify(data, null, 2)}`) - - // The UTXO should have been validated by the third validation source. - assert.equal(data[0].isValid, true) - }) }) describe('#txDetails', () => { From 95c88b5f29ee6ae6cdb9eff33c21761ec384e158 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Mon, 8 Feb 2021 20:33:27 -0800 Subject: [PATCH 3/4] fix(_hydrateUtxo): Moving utxo hydration into a private function --- package.json | 2 +- src/slp/utils.js | 86 ++++++++++++++++++++++-------------------------- 2 files changed, 41 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index ff7b565..53f36bc 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 '#waterfallValidateTxid' test/integration/", + "test:temp": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#tokenUtxoDetails' test/integration/", "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/", diff --git a/src/slp/utils.js b/src/slp/utils.js index 3ca596b..ad47d57 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -1058,10 +1058,6 @@ class Utils { // CT 5/31/20: Refactored to use slp-parse library. async tokenUtxoDetails (utxos) { try { - // utxo list may have duplicate tx_hash, varying tx_pos - // only need to call decodeOpReturn once for those - const decodeOpReturnCache = {} - const cachedTxValidation = {} // Throw error if input is not an array. if (!Array.isArray(utxos)) throw new Error('Input must be an array.') @@ -1070,19 +1066,7 @@ class Utils { for (let i = 0; i < utxos.length; i++) { const utxo = utxos[i] - // CT 1/9/21: This code can be removed after 2/1/21 - // if (!utxo.satoshis) { - // // If Electrumx, convert the value to satoshis. - // if (utxo.value) { - // utxo.satoshis = utxo.value - // } else { - // // If there is neither a satoshis or value property, throw an error. - // throw new Error( - // `utxo ${i} does not have a satoshis or value property.` - // ) - // } - // } - + // Ensure the UTXO has a txid or tx_hash property. if (!utxo.txid) { // If Electrumx, convert the tx_hash property to txid. if (utxo.tx_hash) { @@ -1107,6 +1091,42 @@ class Utils { } } + // Hydrate each UTXO with data from SLP OP_REUTRNs. + const outAry = await this._hydrateUtxo(utxos) + // console.log(`outAry: ${JSON.stringify(outAry, null, 2)}`) + + // *After* each UTXO has been hydrated with SLP data, + // validate the TXID with SLPDB. + for (let i = 0; i < outAry.length; i++) { + const utxo = outAry[i] + + // *After* the UTXO has been hydrated with SLP data, + // validate the TXID with SLPDB. + if (utxo.tokenType) { + // Only execute this block if the current UTXO has a 'tokenType' + // property. i.e. it has been successfully hydrated with SLP + // information. + + // Validate using a 'waterfall' of validators. + utxo.isValid = await this.waterfallValidateTxid(utxo.txid) + // console.log(`isValid: ${JSON.stringify(isValid, null, 2)}`) + } + } + + return outAry + } catch (error) { + if (error.response && error.response.data) throw error.response.data + throw error + } + } + + // This is a private function that is called by tokenUtxoDetails(). + // It loops through an array of UTXOs and tries to hydrate them with SLP + // token information from the OP_RETURN data. + async _hydrateUtxo (utxos) { + try { + const decodeOpReturnCache = {} + // Output Array const outAry = [] @@ -1166,7 +1186,6 @@ class Utils { utxo.vout !== 1 // UTXO is not the reciever of the genesis or mint tokens. ) { // Can safely be marked as false. - // outAry[i] = false utxo.isValid = false outAry[i] = utxo } else { @@ -1191,7 +1210,6 @@ class Utils { utxo.decimals = slpData.decimals utxo.tokenType = slpData.tokenType - // something outAry[i] = utxo } } @@ -1203,8 +1221,8 @@ class Utils { utxo.vout !== 1 // UTXO is not the reciever of the genesis or mint tokens. ) { // Can safely be marked as false. - // outAry[i] = false utxo.isValid = false + outAry[i] = utxo } else { // If UTXO passes validation, then return formatted token data. @@ -1255,8 +1273,8 @@ class Utils { // console.log('tokenQty: ', tokenQty) if (!tokenQty) { - // outAry[i] = false utxo.isValid = false + outAry[i] = utxo } else { // If UTXO passes validation, then return formatted token data. @@ -1293,30 +1311,6 @@ class Utils { outAry[i] = utxo } } - - // *After* the UTXO has been hydrated with SLP data, - // validate the TXID with SLPDB. - if (outAry[i].tokenType) { - // Only execute this block if the current UTXO has a 'tokenType' - // property. i.e. it has been successfully hydrated with SLP - // information. - - // CT 12/13/2020: Disabling the cache until I get processing of tokens - // to be more stable. - // If the value has been cached, use the cached version first. - let isValid = cachedTxValidation[utxo.txid] - if (!isValid) isValid = null - // let isValid = null - - // Validate using a 'waterfall' of validators. - isValid = await this.waterfallValidateTxid(utxo.txid) - - // console.log(`isValid: ${JSON.stringify(isValid, null, 2)}`) - outAry[i].isValid = isValid - - // Save the txid to the local cache to reduce API calls. - cachedTxValidation[utxo.txid] = isValid - } } return outAry @@ -1369,7 +1363,7 @@ class Utils { // If the value has been cached, use the cached version first. let isValid = cachedTxValidation[txid] - if (!isValid) { + if (!isValid && isValid !== false) { isValid = null } else { return isValid From 36432af7d5099ee5d10e6f7e889a68760b5038ba Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 9 Feb 2021 07:22:01 -0800 Subject: [PATCH 4/4] feat(tokenUtxoDetailsWL): tokenUtxoDetails that use whitelist SLPDB --- package.json | 4 +- src/slp/utils.js | 89 ++++++++++++++++++++++++++++++ test/integration/slp.js | 116 +++++++++++++++++++++++++++++++++++++++- test/unit/slp-utils.js | 104 +++++++++++++++++++++++------------ 4 files changed, 274 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index 53f36bc..0c52ac2 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "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 '#tokenUtxoDetails' test/integration/", - "test:temp2": "mocha --timeout=30000 -g '#waterfallValidateTxid' test/unit/", + "test:temp": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#tokenUtxoDetailsWL' 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/", "docs": "./node_modules/.bin/apidoc -i src/ -o docs", diff --git a/src/slp/utils.js b/src/slp/utils.js index ad47d57..40325e4 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -1120,6 +1120,95 @@ class Utils { } } + /** + * @api SLP.Utils.tokenUtxoDetailsWL() tokenUtxoDetailsWL() + * @apiName tokenUtxoDetailsWL + * @apiGroup SLP Utils + * @apiDescription + * + * Same as tokenUtxoDetails(), but it only uses the whitelist SLPDB to + * validate UTXOs. This will result in a lot of `isValid: null` values, + * but much more performant handling of SLP tokens. Some wallet apps prefer + * the scaling performance over the breadth of supported tokens. + * + */ + async tokenUtxoDetailsWL (utxos) { + try { + // Throw error if input is not an array. + if (!Array.isArray(utxos)) throw new Error('Input must be an array.') + + // Loop through each element in the array and validate the input before + // further processing. + for (let i = 0; i < utxos.length; i++) { + const utxo = utxos[i] + + // Ensure the UTXO has a txid or tx_hash property. + if (!utxo.txid) { + // If Electrumx, convert the tx_hash property to txid. + if (utxo.tx_hash) { + utxo.txid = utxo.tx_hash + } else { + // If there is neither a txid or tx_hash property, throw an error. + throw new Error( + `utxo ${i} does not have a txid or tx_hash property.` + ) + } + } + + // Ensure the UTXO has a vout or tx_pos property. + if (!Number.isInteger(utxo.vout)) { + if (Number.isInteger(utxo.tx_pos)) { + utxo.vout = utxo.tx_pos + } else { + throw new Error( + `utxo ${i} does not have a vout or tx_pos property.` + ) + } + } + } + + // Hydrate each UTXO with data from SLP OP_REUTRNs. + const outAry = await this._hydrateUtxo(utxos) + // console.log(`outAry: ${JSON.stringify(outAry, null, 2)}`) + + // *After* each UTXO has been hydrated with SLP data, + // validate the TXID with SLPDB. + for (let i = 0; i < outAry.length; i++) { + const utxo = outAry[i] + + // *After* the UTXO has been hydrated with SLP data, + // validate the TXID with SLPDB. + if (utxo.tokenType) { + // Only execute this block if the current UTXO has a 'tokenType' + // property. i.e. it has been successfully hydrated with SLP + // information. + + // Validate against the whitelist SLPDB. + const whitelistResult = await this.validateTxid3(utxo.txid) + // console.log( + // `whitelist-SLPDB for ${txid}: ${JSON.stringify( + // whitelistResult, + // null, + // 2 + // )}` + // ) + + let isValid = null + + // Safely retrieve the returned value. + if (whitelistResult[0] !== null) isValid = whitelistResult[0].valid + + utxo.isValid = isValid + } + } + + return outAry + } catch (error) { + if (error.response && error.response.data) throw error.response.data + throw error + } + } + // This is a private function that is called by tokenUtxoDetails(). // It loops through an array of UTXOs and tries to hydrate them with SLP // token information from the OP_RETURN data. diff --git a/test/integration/slp.js b/test/integration/slp.js index 3b03253..d77adc2 100644 --- a/test/integration/slp.js +++ b/test/integration/slp.js @@ -32,7 +32,8 @@ describe('#SLP', () => { describe('#util', () => { describe('#list', () => { it('should get information on the Spice token', async () => { - const tokenId = '4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf' + const tokenId = + '4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf' const result = await bchjs.SLP.Utils.list(tokenId) // console.log(`result: ${JSON.stringify(result, null, 2)}`) @@ -490,6 +491,116 @@ describe('#SLP', () => { }) }) + describe('#tokenUtxoDetailsWL', () => { + it('should return details for a simple SEND SLP token utxo', async () => { + const utxos = [ + { + height: 660554, + tx_hash: + '89b3f0c84efe8b01b24e2d7ac08636de5781f31dbb84478e3de868ca0a7ed93a', + tx_pos: 1, + value: 546 + } + ] + + const data = await bchjs.SLP.Utils.tokenUtxoDetailsWL(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert.property(data[0], 'txid') + assert.property(data[0], 'vout') + assert.property(data[0], 'height') + assert.property(data[0], 'utxoType') + assert.property(data[0], 'tokenId') + assert.property(data[0], 'tokenTicker') + assert.property(data[0], 'tokenName') + assert.property(data[0], 'tokenDocumentUrl') + assert.property(data[0], 'tokenDocumentHash') + assert.property(data[0], 'decimals') + assert.property(data[0], 'tokenQty') + assert.property(data[0], 'isValid') + assert.equal(data[0].isValid, true) + }) + + it('should return false for BCH-only UTXOs', async () => { + const utxos = [ + { + txid: + 'a937f792c7c9eb23b4f344ce5c233d1ac0909217d0a504d71e6b1e4efb864a3b', + vout: 0, + amount: 0.00001, + satoshis: 1000, + confirmations: 0, + ts: 1578424704 + }, + { + txid: + '53fd141c2e999e080a5860887441a2c45e9cbe262027e2bd2ac998fc76e43c44', + vout: 0, + amount: 0.00001, + satoshis: 1000, + confirmations: 0, + ts: 1578424634 + } + ] + + const data = await bchjs.SLP.Utils.tokenUtxoDetailsWL(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert.isArray(data) + assert.equal(data[0].isValid, false) + assert.equal(data[1].isValid, false) + }) + + it('should handle a dust attack', async () => { + // it("#dustattack", async () => { + const utxos = [ + { + height: 655965, + tx_hash: + 'a675af87dcd8d39be782737aa52e0076b52eb2f5ce355ffcb5567a64dd96b77e', + tx_pos: 151, + value: 547, + satoshis: 547, + txid: + 'a675af87dcd8d39be782737aa52e0076b52eb2f5ce355ffcb5567a64dd96b77e', + vout: 151, + address: 'bitcoincash:qq4dw3sm8qvglspy6w2qg0u2ugsy9zcfcqrpeflwww', + hdIndex: 11 + } + ] + + const data = await bchjs.SLP.Utils.tokenUtxoDetailsWL(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert.equal(data[0].isValid, false) + }) + + it('should handle null SLPDB validations', async () => { + const utxos = [ + { + height: 665577, + tx_hash: + '4b89405c54d1c0bde8aa476a47561a42a6e7a5e927daa2ec69d428810eae3419', + tx_pos: 1, + value: 546 + }, + { + height: 665577, + tx_hash: + '3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488', + tx_pos: 1, + value: 546 + } + ] + + const data = await bchjs.SLP.Utils.tokenUtxoDetailsWL(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert.isArray(data) + // assert.equal(data[0].isValid, null) + }) + }) + describe('#balancesForAddress', () => { it('should fetch all balances for address: simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9', async () => { const balances = await bchjs.SLP.Utils.balancesForAddress( @@ -738,7 +849,8 @@ describe('#SLP', () => { describe('#waterfallValidateTxid', () => { it('should validate known good txid not in whitelist', async () => { - const txid = '3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488' + const txid = + '3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488' const result = await bchjs.SLP.Utils.waterfallValidateTxid(txid) // console.log(`result: ${JSON.stringify(result, null, 2)}`) diff --git a/test/unit/slp-utils.js b/test/unit/slp-utils.js index 5bf0d48..4eb7b31 100644 --- a/test/unit/slp-utils.js +++ b/test/unit/slp-utils.js @@ -497,41 +497,6 @@ describe('#SLP Utils', () => { } }) - // CT 1/9/21: This test can be removed after 2/1/21 - // it('should throw error if utxo does not have satoshis or value property.', async () => { - // try { - // const utxos = [ - // { - // txid: - // 'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90', - // vout: 3, - // amount: 0.00002015, - // satoshis: 2015, - // height: 594892, - // confirmations: 5 - // }, - // { - // txid: - // 'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90', - // vout: 2, - // amount: 0.00000546, - // height: 594892, - // confirmations: 5 - // } - // ] - // - // await uut.Utils.tokenUtxoDetails(utxos) - // - // assert.equal(true, false, 'Unexpected result.') - // } catch (err) { - // assert.include( - // err.message, - // 'utxo 1 does not have a satoshis or value property', - // 'Expected error message.' - // ) - // } - // }) - it('should throw error if utxo does not have txid or tx_hash property.', async () => { try { const utxos = [ @@ -1596,6 +1561,75 @@ describe('#SLP Utils', () => { }) }) + describe('#tokenUtxoDetailsWL', () => { + it('should return details for a simple SEND SLP token utxo', async () => { + // Mock the call to REST API + // Stub the calls to decodeOpReturn. + sandbox + .stub(uut.Utils, 'decodeOpReturn') + .onCall(0) + .resolves({ + tokenType: 1, + txType: 'SEND', + tokenId: + '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7', + amounts: ['200000000', '99887500000000'] + }) + .onCall(1) + .resolves({ + tokenType: 1, + txType: 'GENESIS', + ticker: 'TOK-CH', + name: 'TokyoCash', + tokenId: + '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7', + documentUri: '', + documentHash: '', + decimals: 8, + mintBatonVout: 0, + qty: '2100000000000000' + }) + + // Stub the call to validateTxid + sandbox.stub(uut.Utils, 'validateTxid3').resolves([ + { + txid: + 'fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb', + valid: true + } + ]) + + const utxos = [ + { + txid: + 'fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb', + vout: 1, + amount: 0.00000546, + satoshis: 546, + height: 596089, + confirmations: 748 + } + ] + + const data = await uut.Utils.tokenUtxoDetailsWL(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert.property(data[0], 'txid') + assert.property(data[0], 'vout') + assert.property(data[0], 'height') + assert.property(data[0], 'utxoType') + assert.property(data[0], 'tokenId') + assert.property(data[0], 'tokenTicker') + assert.property(data[0], 'tokenName') + assert.property(data[0], 'tokenDocumentUrl') + assert.property(data[0], 'tokenDocumentHash') + assert.property(data[0], 'decimals') + assert.property(data[0], 'tokenQty') + assert.property(data[0], 'isValid') + assert.equal(data[0].isValid, true) + }) + }) + describe('#txDetails', () => { it('should throw an error if txid is not included', async () => { try {