diff --git a/package.json b/package.json index e325ced..817f1f8 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "test:integration:local:testnet": "RESTURL=http://localhost:4000/v4/ mocha --timeout 30000 test/integration/chains/testnet", "test:integration:decatur:bchn": "export RESTURL=http://192.168.0.36:3000/v4/ && mocha --timeout 30000 test/integration/", "test:temp": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#hydrateUtxos' test/integration/", - "test:temp2": "mocha --timeout=30000 -g '#decodeOpReturn' test/unit/", + "test:temp2": "mocha --timeout=30000 -g '#_hydrateUtxo' test/unit/", "test:temp3": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#hydrateUtxos' test/integration/", "test:temp4": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#decodeOpReturn' test/integration/", "coverage": "nyc report --reporter=text-lcov | coveralls", diff --git a/src/slp/utils.js b/src/slp/utils.js index bd70131..86e959f 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -1,3 +1,5 @@ +/* eslint-disable no-useless-catch */ + // Public npm libraries const axios = require('axios') const slpParser = require('slp-parser') @@ -1014,21 +1016,16 @@ class Utils { return tokenData } catch (error) { - console.log('decodeOpReturn error: ', error) - console.log(`decodeOpReturn error.message: ${error.message}`) - - if (error.response && error.response.data) { - // throw error.response.data - - // error.bchApiMessage = error.message - // error.message = error.response.data - // error.data = error.response.data - console.log( - `decodeOpReturn error.response.data: ${JSON.stringify( - error.response.data - )}` - ) - } + // Used for debugging + // console.log('decodeOpReturn error: ', error) + // console.log(`decodeOpReturn error.message: ${error.message}`) + // if (error.response && error.response.data) { + // console.log( + // `decodeOpReturn error.response.data: ${JSON.stringify( + // error.response.data + // )}` + // ) + // } throw error } } @@ -1256,6 +1253,12 @@ class Utils { // token data. If that call throws an error due to hitting rate limits, this // function will not throw an error. Instead, it will mark the `isValid` // property as `null` + // + // Exception to the above: It *will* throw an error if decodeOpReturn() throws + // an error while trying to get the Genesis transaction for a Send or Mint + // transaction. However, that is a rare occurence since the cache of + // decodeOpReturn() will minimize API calls for this case. This behavior + // could be changed, but right now it's a corner case of a corner case. async _hydrateUtxo (utxos, usrObj = null) { try { const decodeOpReturnCache = {} @@ -1315,6 +1318,7 @@ class Utils { // Halt the execution of the loop and increase to the next index. continue } + // console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`) const txType = slpData.txType.toLowerCase() @@ -1352,6 +1356,10 @@ class Utils { utxo.decimals = slpData.decimals utxo.tokenType = slpData.tokenType + // Initial value is null until UTXO can be validated and confirmed + // to be valid (true) or not (false). + utxo.isValid = null + outAry[i] = utxo } } @@ -1401,6 +1409,10 @@ class Utils { utxo.mintBatonVout = slpData.mintBatonVout + // Initial value is null until UTXO can be validated and confirmed + // to be valid (true) or not (false). + utxo.isValid = null + outAry[i] = utxo } } @@ -1442,6 +1454,10 @@ class Utils { utxo.decimals = genesisData.decimals utxo.tokenType = slpData.tokenType + // Initial value is null until UTXO can be validated and confirmed + // to be valid (true) or not (false). + utxo.isValid = null + // Calculate the real token quantity. const tokenQtyBig = new BigNumber(tokenQty).div( @@ -1460,7 +1476,7 @@ class Utils { return outAry } catch (error) { console.log('_hydrateUtxo error: ', error) - if (error.response && error.response.data) throw error.response.data + // if (error.response && error.response.data) throw error.response.data throw error } } diff --git a/test/unit/fixtures/slp/mock-utils.js b/test/unit/fixtures/slp/mock-utils.js index c37fd18..a5d672e 100644 --- a/test/unit/fixtures/slp/mock-utils.js +++ b/test/unit/fixtures/slp/mock-utils.js @@ -1366,6 +1366,23 @@ const txDetailsSLPNftChild = { } const mock429Error = new Error('Request failed with status code 429') +mock429Error.response = { + status: 429, + statusText: 'Too Many Requests', + data: { + error: + 'Too many requests. Your limits are currently 100 requests per minute. Increase rate limits at https://fullstack.cash' + } +} + +const mock503Error = new Error('Request failed with status code 503') +mock503Error.response = { + status: 503, + statusText: 'Service Unavailable', + data: { + error: 'Some error message' + } +} module.exports = { mockList, @@ -1399,5 +1416,6 @@ module.exports = { whitelist, slpdbStatus, mockValidateTxidArray, - mock429Error + mock429Error, + mock503Error } diff --git a/test/unit/slp-utils.js b/test/unit/slp-utils.js index c93cb55..2ea2aae 100644 --- a/test/unit/slp-utils.js +++ b/test/unit/slp-utils.js @@ -495,8 +495,284 @@ describe('#SLP Utils', () => { assert.fail('Unexpect result') } catch (err) { - console.log('err: ', err) - // assert.include(err.message, 'amount string size not 8 bytes') + // console.log('err: ', err) + + assert.property(err, 'message') + assert.property(err, 'response') + assert.equal(err.response.status, 429) + assert.equal(err.response.statusText, 'Too Many Requests') + assert.include(err.response.data.error, 'Too many requests') + } + }) + + // 503 means the service is down. + it('should throw error when axios returns 503 status', async () => { + try { + // Mock the call to the REST API + sandbox.stub(uut.Utils.axios, 'post').rejects(mockData.mock503Error) + + const txid = + 'a60a522cc11ad7011b74e57fbabbd99296e4b9346bcb175dcf84efb737030415' + + await uut.Utils.decodeOpReturn(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.fail('Unexpect result') + } catch (err) { + // console.log('err: ', err) + + assert.property(err, 'message') + assert.property(err, 'response') + assert.equal(err.response.status, 503) + assert.equal(err.response.statusText, 'Service Unavailable') + assert.property(err.response, 'data') + } + }) + }) + + describe('#_hydrateUtxo', () => { + // // This captures an important corner-case. When an SLP token is created, the + // // 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 + // sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) + + // Stub the calls to decodeOpReturn. + sandbox.stub(uut.Utils, 'decodeOpReturn').resolves({ + tokenType: 1, + txType: 'GENESIS', + ticker: 'SLPSDK', + name: 'SLP SDK example using BITBOX', + tokenId: + 'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90', + documentUri: 'developer.bitcoin.com', + documentHash: '', + decimals: 8, + mintBatonVout: 2, + qty: '50700000000' + }) + + const utxos = [ + { + txid: + 'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90', + vout: 3, + amount: 0.00002015, + satoshis: 2015, + height: 594892, + confirmations: 5 + }, + { + txid: + 'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90', + vout: 2, + amount: 0.00000546, + satoshis: 546, + height: 594892, + confirmations: 5 + } + ] + + const data = await uut.Utils._hydrateUtxo(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + // assert.equal(data[0], false, "Change UTXO marked as false.") + assert.property(data[0], 'txid') + assert.property(data[0], 'vout') + assert.property(data[0], 'amount') + assert.property(data[0], 'satoshis') + assert.property(data[0], 'height') + assert.property(data[0], 'confirmations') + assert.property(data[0], 'isValid') + assert.equal(data[0].isValid, false) + + assert.property(data[1], 'txid') + assert.property(data[1], 'vout') + assert.property(data[1], 'amount') + assert.property(data[1], 'satoshis') + assert.property(data[1], 'height') + assert.property(data[1], 'confirmations') + assert.property(data[1], 'utxoType') + assert.property(data[1], 'tokenId') + assert.property(data[1], 'tokenTicker') + assert.property(data[1], 'tokenName') + assert.property(data[1], 'tokenDocumentUrl') + assert.property(data[1], 'tokenDocumentHash') + assert.property(data[1], 'decimals') + }) + + // 429 means the user is exceeding the rate limits. + it('should return isValid=null for 429 rate limit error', async () => { + // Force decodeOpReturn() to throw a 429 error. + sandbox.stub(uut.Utils, 'decodeOpReturn').rejects(mockData.mock429Error) + + const utxos = [ + { + txid: + 'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90', + vout: 3, + amount: 0.00002015, + satoshis: 2015, + height: 594892, + confirmations: 5 + }, + { + txid: + 'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90', + vout: 2, + amount: 0.00000546, + satoshis: 546, + height: 594892, + confirmations: 5 + } + ] + + const result = await uut.Utils._hydrateUtxo(utxos) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result[0].isValid, null) + assert.equal(result[1].isValid, null) + }) + + // 503 means the server is down or is not responding. + it('should return isValid=null for 503 rate limit error', async () => { + // Force decodeOpReturn() to throw a 429 error. + sandbox.stub(uut.Utils, 'decodeOpReturn').rejects(mockData.mock503Error) + + const utxos = [ + { + txid: + 'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90', + vout: 3, + amount: 0.00002015, + satoshis: 2015, + height: 594892, + confirmations: 5 + }, + { + txid: + 'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90', + vout: 2, + amount: 0.00000546, + satoshis: 546, + height: 594892, + confirmations: 5 + } + ] + + const result = await uut.Utils._hydrateUtxo(utxos) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result[0].isValid, null) + assert.equal(result[1].isValid, null) + }) + + 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' + }) + + // sandbox.stub(uut.Utils, 'waterfallValidateTxid').resolves(true) + + const utxos = [ + { + txid: + 'fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb', + vout: 1, + amount: 0.00000546, + satoshis: 546, + height: 596089, + confirmations: 748 + } + ] + + const data = await uut.Utils._hydrateUtxo(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert.property(data[0], 'txid') + assert.property(data[0], 'vout') + assert.property(data[0], 'amount') + assert.property(data[0], 'satoshis') + assert.property(data[0], 'height') + assert.property(data[0], 'confirmations') + 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, null) + }) + + // I don't know if this is the ideal behavior, but this is the behavior + // that is in production, so I wanted to capture it in a test case. This + // behavior can always be changed in the future. + it('should throw error if 429 when querying Genesis transaction', async () => { + try { + // 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) + .rejects(mockData.mock429Error) + + const utxos = [ + { + txid: + 'fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb', + vout: 1, + amount: 0.00000546, + satoshis: 546, + height: 596089, + confirmations: 748 + } + ] + + await uut.Utils._hydrateUtxo(utxos) + + assert.fail('Unexpected result') + } catch (err) { + // console.log('error: ', err) + + assert.property(err, 'message') + assert.property(err, 'response') + assert.equal(err.response.status, 429) + assert.equal(err.response.statusText, 'Too Many Requests') + assert.include(err.response.data.error, 'Too many requests') } }) })