diff --git a/src/psf-slp-indexer.js b/src/psf-slp-indexer.js index b9a5b6a..194b622 100644 --- a/src/psf-slp-indexer.js +++ b/src/psf-slp-indexer.js @@ -11,11 +11,12 @@ const axios = require('axios') // Local libraries const RawTransaction = require('./raw-transactions') +const SlpUtils = require('./slp/utils') // let _this class PsfSlpIndexer { - constructor (config) { + constructor (config = {}) { this.restURL = config.restURL this.apiToken = config.apiToken this.authToken = config.authToken @@ -38,6 +39,7 @@ class PsfSlpIndexer { // Encapsulate dependencies this.rawTransaction = new RawTransaction(config) + this.slpUtils = new SlpUtils(config) // _this = this } @@ -308,16 +310,18 @@ class PsfSlpIndexer { // 'TX not found in psf-slp-indexer. Retrieving from full node.' // ) + // Check if this txid belongs to a blacklisted token. + const isInBlacklist = await this.checkBlacklist(txid) + // Get the TX Details from the full node. const txDetails = await this.rawTransaction.getTxData(txid) // console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`) - // TODO: Run the TX through decodeOpReturn() to see if it has a tokenID - // that is in the blacklist. If it is, then set isValidSlp = null to - // signal that it's state can not be determined. - - // Assumption: transaction is not a valid SLP UTXO. - txDetails.isValidSlp = false + if (isInBlacklist) { + txDetails.isValidSlp = null + } else { + txDetails.isValidSlp = false + } const outObj = { txData: txDetails @@ -327,6 +331,39 @@ class PsfSlpIndexer { } else throw error } } + + // Check if the txid has an OP_RETURN containing a tokenID that is in the + // blacklist. In that case, the isValidSlp property should be marked as + // null, and not false. + async checkBlacklist (txid) { + try { + // TODO: Add endpoint to psf-slp-indexer to retrieve current blacklist. + // This should be done once at startup, and not each time this function + // is called. + const blacklist = [ + 'dd21be4532d93661e8ffe16db6535af0fb8ee1344d1fef81a193e2b4cfa9fbc9' + ] + + const outTokenData = await this.slpUtils.decodeOpReturn(txid) + // console.log('outTokenData: ', outTokenData) + + // Loop through each token in the blacklist. + for (let i = 0; i < blacklist.length; i++) { + // If a match is found, return true. + if (outTokenData.tokenId === blacklist[i]) { + return true + } + } + + // By default, return false. + return false + } catch (err) { + // console.log(err) + + // Exit quietly. + return false + } + } } module.exports = PsfSlpIndexer diff --git a/src/slp/utils.js b/src/slp/utils.js index e1a4003..67283e9 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -11,7 +11,7 @@ const Util = require('../util') let _this class Utils { - constructor (config) { + constructor (config = {}) { this.restURL = config.restURL this.apiToken = config.apiToken this.slpParser = slpParser diff --git a/test/integration/chains/bchn/psf-slp-indexer.integration.js b/test/integration/chains/bchn/psf-slp-indexer.integration.js index 2bf2119..c703a05 100644 --- a/test/integration/chains/bchn/psf-slp-indexer.integration.js +++ b/test/integration/chains/bchn/psf-slp-indexer.integration.js @@ -56,7 +56,7 @@ describe('#psf-slp-indexer', () => { describe('#tx', () => { it('should get hydrated tx data for an SLP transaction', async () => { const txid = - '83361c34cac2ea7f9ca287fca57a96cc0763719f0cdf4850f9696c1e68eb635c' + '947ccb2a0d62ca287bc4b0993874ab0f9f6afd454193e631e2bf84dca66731fc' const result = await bchjs.PsfSlpIndexer.tx(txid) // console.log('result: ', result) @@ -67,7 +67,7 @@ describe('#psf-slp-indexer', () => { assert.equal(result.txData.isValidSlp, true) }) - it('should handle non-SLP transaction', async () => { + it('should mark non-SLP transaction as false', async () => { const txid = '03d6e6b63647ce7b02ecc73dc6d41b485be14a3e20eed4474b8a840358ddf14e' @@ -84,7 +84,7 @@ describe('#psf-slp-indexer', () => { // Currently FlexUSD UTXOs are reported as invalid SLP UTXOs, which means // the wallet will burn them. There is a TODO in the code. This test will // need to be changed when it is done. - it('should handle a blacklisted token', async () => { + it('should mark blacklisted token as null', async () => { const txid = '302113c11b90edc5f36c073d2f8a75e1e0eaf59b56235491a843d3819cd6a85f' @@ -94,7 +94,23 @@ describe('#psf-slp-indexer', () => { assert.property(result.txData, 'vin') assert.property(result.txData, 'vout') assert.property(result.txData, 'isValidSlp') - assert.equal(result.txData.isValidSlp, false) + assert.equal(result.txData.isValidSlp, null) + }) + + it('should throw error for non-existent txid', async () => { + try { + const txid = + '302113c11b90edc5f36c073d2f8a75e1e0eaf59b56235491a843d3819cd6a85e' + + await bchjs.PsfSlpIndexer.tx(txid) + // console.log('result: ', result) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + + assert.include(err.message, 'No such mempool or blockchain transaction') + } }) }) }) diff --git a/test/unit/fixtures/psf-slp-indexer-mock.js b/test/unit/fixtures/psf-slp-indexer-mock.js index c5541a0..255e5ec 100644 --- a/test/unit/fixtures/psf-slp-indexer-mock.js +++ b/test/unit/fixtures/psf-slp-indexer-mock.js @@ -21,7 +21,8 @@ const tokenStats = { totalMinted: '1', txs: [ { - txid: '13cad617d523c8eb4ab11fff19c010e0e0a4ea4360b58e0c8c955a45a146a669', + txid: + '13cad617d523c8eb4ab11fff19c010e0e0a4ea4360b58e0c8c955a45a146a669', height: 722420, type: 'GENESIS', qty: '1' @@ -39,11 +40,14 @@ const txData = { locktime: 0, vin: [ { - txid: '8370db30d94761ab9a11b71ecd22541151bf6125c8c613f0f6fab8ab794565a7', + txid: + '8370db30d94761ab9a11b71ecd22541151bf6125c8c613f0f6fab8ab794565a7', vout: 0, scriptSig: { - asm: '304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4[ALL|FORKID] 02791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851', - hex: '47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851' + asm: + '304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4[ALL|FORKID] 02791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851', + hex: + '47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851' }, sequence: 4294967295, address: 'bitcoincash:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qwgaqm3wq', @@ -58,16 +62,20 @@ const txData = { value: 0, n: 0, scriptPubKey: { - asm: 'OP_RETURN 5262419 1 47454e45534953 54524f5554 54726f75742773207465737420746f6b656e 74726f757473626c6f672e636f6d 0 2 2 000000174876e800', - hex: '6a04534c500001010747454e455349530554524f55541254726f75742773207465737420746f6b656e0e74726f757473626c6f672e636f6d4c000102010208000000174876e800', + asm: + 'OP_RETURN 5262419 1 47454e45534953 54524f5554 54726f75742773207465737420746f6b656e 74726f757473626c6f672e636f6d 0 2 2 000000174876e800', + hex: + '6a04534c500001010747454e455349530554524f55541254726f75742773207465737420746f6b656e0e74726f757473626c6f672e636f6d4c000102010208000000174876e800', type: 'nulldata' }, tokenQtyStr: '0', tokenQty: 0 } ], - hex: '0200000001a7654579abb8faf6f013c6c82561bf51115422cd1eb7119aab6147d930db7083000000006a47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851ffffffff040000000000000000476a04534c500001010747454e455349530554524f55541254726f75742773207465737420746f6b656e0e74726f757473626c6f672e636f6d4c000102010208000000174876e80022020000000000001976a914db4d39ceb7794ffe5d06855f249e1d3a7f1b024088ac22020000000000001976a914db4d39ceb7794ffe5d06855f249e1d3a7f1b024088accec20000000000001976a9145904159f2f69bfa63eefa712633a0d96dc2e7e8888ac00000000', - blockhash: '0000000000000000009f65225a3e12e23a7ea057c869047e0f36563a1f410267', + hex: + '0200000001a7654579abb8faf6f013c6c82561bf51115422cd1eb7119aab6147d930db7083000000006a47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851ffffffff040000000000000000476a04534c500001010747454e455349530554524f55541254726f75742773207465737420746f6b656e0e74726f757473626c6f672e636f6d4c000102010208000000174876e80022020000000000001976a914db4d39ceb7794ffe5d06855f249e1d3a7f1b024088ac22020000000000001976a914db4d39ceb7794ffe5d06855f249e1d3a7f1b024088accec20000000000001976a9145904159f2f69bfa63eefa712633a0d96dc2e7e8888ac00000000', + blockhash: + '0000000000000000009f65225a3e12e23a7ea057c869047e0f36563a1f410267', confirmations: 97398, time: 1581773131, blocktime: 1581773131, @@ -88,27 +96,32 @@ const balance = { balance: { utxos: [ { - txid: 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9', + txid: + 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9', vout: 1, type: 'token', qty: '1800', - tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + tokenId: + 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', address: 'bitcoincash:qrqy3kj7r822ps6628vwqq5k8hyjl6ey3y4eea2m4s' } ], txs: [ { - txid: '078b2c48ed1db0d5d5996f2889b8d847a49200d0a781f6aa6752f740f312688f', + txid: + '078b2c48ed1db0d5d5996f2889b8d847a49200d0a781f6aa6752f740f312688f', height: 717796 }, { - txid: 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9', + txid: + 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9', height: 717832 } ], balances: [ { - tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + tokenId: + 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', qty: '1800' } ] @@ -122,9 +135,19 @@ const status = { chainBlockHeight: 722679 } } + +const tokenData01 = { + tokenType: 1, + txType: 'MINT', + tokenId: 'dd21be4532d93661e8ffe16db6535af0fb8ee1344d1fef81a193e2b4cfa9fbc9', + mintBatonVout: 2, + qty: {} +} + module.exports = { tokenStats, txData, balance, - status + status, + tokenData01 } diff --git a/test/unit/psf-slp-indexer.js b/test/unit/psf-slp-indexer.js index 3e26a26..cd14ea0 100644 --- a/test/unit/psf-slp-indexer.js +++ b/test/unit/psf-slp-indexer.js @@ -71,6 +71,7 @@ describe('#PsfSlpIndexer', () => { assert.isArray(result.balance.txs) assert.isArray(result.balance.balances) }) + it('should throw an error for improper input', async () => { try { const addr = 12345 @@ -82,6 +83,7 @@ describe('#PsfSlpIndexer', () => { assert.include(err.message, 'Input address must be a string.') } }) + it('should handle axios error', async () => { try { // Stub the network call. @@ -96,6 +98,7 @@ describe('#PsfSlpIndexer', () => { assert.include(err.message, 'test error') } }) + it('should handle request error', async () => { try { // Stub the network call. @@ -139,6 +142,7 @@ describe('#PsfSlpIndexer', () => { assert.property(result.tokenData, 'totalMinted') assert.property(result.tokenData, 'txs') }) + it('should throw an error for improper input', async () => { try { const tokenId = 12345 @@ -150,6 +154,7 @@ describe('#PsfSlpIndexer', () => { assert.include(err.message, 'Input tokenId must be a string.') } }) + it('should handle axios error', async () => { try { // Stub the network call. @@ -164,6 +169,7 @@ describe('#PsfSlpIndexer', () => { assert.include(err.message, 'test error') } }) + it('should handle request error', async () => { try { // Stub the network call. @@ -215,6 +221,7 @@ describe('#PsfSlpIndexer', () => { assert.property(result.txData, 'tokenDocHash') assert.property(result.txData, 'isValidSlp') }) + it('should throw an error for improper input', async () => { try { const txid = 12345 @@ -226,6 +233,7 @@ describe('#PsfSlpIndexer', () => { assert.include(err.message, 'Input txid must be a string.') } }) + it('should handle axios error', async () => { try { // Stub the network call. @@ -249,6 +257,7 @@ describe('#PsfSlpIndexer', () => { sandbox.stub(axios, 'post').rejects(testErr) // Stub the call to the full node + sandbox.stub(bchjs.PsfSlpIndexer, 'checkBlacklist').resolves(false) sandbox .stub(bchjs.PsfSlpIndexer.rawTransaction, 'getTxData') .resolves({ txid: 'fakeTxid' }) @@ -259,6 +268,72 @@ describe('#PsfSlpIndexer', () => { // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.equal(result.txData.txid, 'fakeTxid') + assert.equal(result.txData.isValidSlp, false) + }) + + it('should return isValidSlp=null for blacklisted token', async () => { + // Stub the call to the SLP indexer. + const testErr = { + response: { data: { error: 'Key not found in database' } } + } + sandbox.stub(axios, 'post').rejects(testErr) + + // Stub the call to the full node + sandbox.stub(bchjs.PsfSlpIndexer, 'checkBlacklist').resolves(true) + sandbox + .stub(bchjs.PsfSlpIndexer.rawTransaction, 'getTxData') + .resolves({ txid: 'fakeTxid' }) + + const txid = + 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2' + const result = await bchjs.PsfSlpIndexer.tx(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result.txData.txid, 'fakeTxid') + assert.equal(result.txData.isValidSlp, null) + }) + }) + + describe('#checkBlacklist', () => { + it('should return true if txid contains token in blacklist', async () => { + // Mock dependencies + sandbox + .stub(bchjs.PsfSlpIndexer.slpUtils, 'decodeOpReturn') + .resolves(mockData.tokenData01) + + const txid = + '302113c11b90edc5f36c073d2f8a75e1e0eaf59b56235491a843d3819cd6a85f' + + const result = await bchjs.PsfSlpIndexer.checkBlacklist(txid) + // console.log('result: ', result) + + assert.equal(result, true) + }) + + it('should return false if there is an error', async () => { + // Force an error + sandbox + .stub(bchjs.PsfSlpIndexer.slpUtils, 'decodeOpReturn') + .rejects(new Error('test error')) + + const result = await bchjs.PsfSlpIndexer.checkBlacklist() + + assert.equal(result, false) + }) + + it('should return false if there is no tokenId match', async () => { + // Mock dependencies + mockData.tokenData01.tokenId = 'abc123' + sandbox + .stub(bchjs.PsfSlpIndexer.slpUtils, 'decodeOpReturn') + .resolves(mockData.tokenData01) + + const txid = + '302113c11b90edc5f36c073d2f8a75e1e0eaf59b56235491a843d3819cd6a85f' + + const result = await bchjs.PsfSlpIndexer.checkBlacklist(txid) + + assert.equal(result, false) }) }) })