From f09573554daf4f14b36377140f3b4acc925c9a13 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 18 Jan 2022 08:32:45 -0800 Subject: [PATCH 1/3] fix(PsfSlpIndexer.tx()): Added detection of blacklisted SLP Tokens --- src/psf-slp-indexer.js | 51 +++++++++++-- src/slp/utils.js | 2 +- .../bchn/psf-slp-indexer.integration.js | 24 +++++- test/unit/fixtures/psf-slp-indexer-mock.js | 51 +++++++++---- test/unit/psf-slp-indexer.js | 75 +++++++++++++++++++ 5 files changed, 177 insertions(+), 26 deletions(-) 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) }) }) }) From 6b838ef9cd5beda264ff816735356180ffddba64 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 18 Jan 2022 08:45:20 -0800 Subject: [PATCH 2/3] feat(Ninsight): Removing Ninsight libraries and tests --- src/bch-js.js | 4 - src/ninsight.js | 319 -------------------- test/integration/transaction-integration.js | 130 ++++---- test/unit/fixtures/ninsight-mock.js | 170 ----------- test/unit/ninsight.js | 255 ---------------- 5 files changed, 66 insertions(+), 812 deletions(-) delete mode 100644 src/ninsight.js delete mode 100644 test/unit/fixtures/ninsight-mock.js delete mode 100644 test/unit/ninsight.js diff --git a/src/bch-js.js b/src/bch-js.js index 9fc7be1..ce04a72 100644 --- a/src/bch-js.js +++ b/src/bch-js.js @@ -36,7 +36,6 @@ const DSProof = require('./dsproof') const Ecash = require('./ecash') // Indexers -const Ninsight = require('./ninsight') const Electrumx = require('./electrumx') const PsfSlpIndexer = require('./psf-slp-indexer') @@ -83,9 +82,6 @@ class BCHJS { // console.log(`apiToken: ${this.apiToken}`) - // Bitcoin.com Ninsight indexer - this.Ninsight = new Ninsight(config) - // ElectrumX indexer this.Electrumx = new Electrumx(libConfig) diff --git a/src/ninsight.js b/src/ninsight.js deleted file mode 100644 index 2839b91..0000000 --- a/src/ninsight.js +++ /dev/null @@ -1,319 +0,0 @@ -/* - This library interacts with the ninsight REST API endpoints operated by - Bitcoin.com -*/ - -const axios = require('axios') - -// let _this - -class Ninsight { - constructor (config) { - // this.restURL = config.restURL - // this.apiToken = config.apiToken - - // this.ninsightURL = `https://bch-explorer.api.bitcoin.com/v1/` - if (config) { - this.ninsightURL = config.ninsightURL - ? config.ninsightURL - : 'https://rest.bitcoin.com/v2' - } - - // Add JWT token to the authorization header. - this.axiosOptions = { - headers: { - authorization: `Token ${this.apiToken}`, - timeout: 15000 - } - } - - // _this = this - } - - /** - * @api Ninsight.utxo() utxo() - * @apiName Ninsight Utxo - * @apiGroup Ninsight - * @apiDescription Return list of uxto for address. - * - * @apiExample Example usage: - * (async () => { - * try { - * let utxo = await bchjs.Ninsight.utxo('bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'); - * console.log(utxo); - * } catch(error) { - * console.error(error) - * } - * })() - * - * // [ - * // { - * // "txid": "d31dc2cf66fe4d3d3ae18e1065def58a64920746b1702b52f060e5edeea9883b", - * // "vout": 1, - * // "amount": 0.03608203, - * // "satoshis": 3608203, - * // "height": 585570, - * // "confirmations": 10392 - * // }, - * // { - * // "txid": "41e9a118765ecf7a1ba4487c0863e23dba343cc5880381a72f0365ac2546c5fa", - * // "vout": 0, - * // "amount": 0.03608203, - * // "satoshis": 3608203, - * // "height": 577125, - * // "confirmations": 18837 - * // } - * // ] - * - */ - async utxo (address) { - try { - // Handle single address. - if (typeof address === 'string') { - const response = await axios.post( - `${this.ninsightURL}/address/utxo`, - { - addresses: [address] - }, - this.axiosOptions - ) - return response.data - - // Handle array of addresses. - } else if (Array.isArray(address)) { - const response = await axios.post( - `${this.ninsightURL}/address/utxo`, - { - addresses: address - }, - this.axiosOptions - ) - - return response.data - } - - throw new Error('Input address must be a string or array of strings.') - } catch (error) { - if (error.response && error.response.data) throw error.response.data - else throw error - } - } - - /** - * @api Ninsight.unconfirmed() unconfirmed() - * @apiName Ninsight Unconfirmed Utxo - * @apiGroup Ninsight - * @apiDescription Returns a list of unconfirmed UTXOs for an address. - * - * @apiExample Example usage: - * (async () => { - * try { - * let utxo = await bchjs.Ninsight.unconfirmed('bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'); - * console.log(utxo); - * } catch(error) { - * console.error(error) - * } - * })() - * - * // [ - * // { - * // "txid": "d31dc2cf66fe4d3d3ae18e1065def58a64920746b1702b52f060e5edeea9883b", - * // "vout": 1, - * // "amount": 0.03608203, - * // "satoshis": 3608203, - * // "confirmations": 0 - * // "ts": 1559670801 - * // }, - * // { - * // "txid": "41e9a118765ecf7a1ba4487c0863e23dba343cc5880381a72f0365ac2546c5fa", - * // "vout": 0, - * // "amount": 0.03608203, - * // "satoshis": 3608203, - * // "confirmations": 0 - * // "ts": 1559670902 - * // } - * // ] - * - */ - async unconfirmed (address) { - try { - if (typeof address === 'string') { - const response = await axios.post( - `${this.ninsightURL}/address/unconfirmed`, - { - addresses: [address] - }, - this.axiosOptions - ) - - return response.data - } else if (Array.isArray(address)) { - const response = await axios.post( - `${this.ninsightURL}/address/unconfirmed`, - { - addresses: address - }, - this.axiosOptions - ) - - return response.data - } - - throw new Error('Input address must be a string or array of strings.') - } catch (error) { - if (error.response && error.response.data) throw error.response.data - else throw error - } - } - - /** - * @api Ninsight.transactions() transactions() - * @apiName Ninsight TX History - * @apiGroup Ninsight - * @apiDescription Return transactions history for address. - * - * @apiExample Example usage: - * (async () => { - * try { - * let txs = await bchjs.Ninsight.transactions('bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'); - * console.log(txs); - * } catch(error) { - * console.error(error) - * } - * })() - * - * // [ - * // { - * // "pagesTotal": 1, - * // "txs": [ - * // { - * // "txid": "ec7bc8349386e3e1939bbdc4f8092fdbdd6a380734e68486b558cd594c451d5b", - * // "version": 2, - * // "locktime": 0, - * // "vin": [ - * // { - * // "txid": "4f1fc57c33659628938db740449bf92fb75799e1d5750a4aeef80eb52d6df1e0", - * // "vout": 0, - * // "sequence": 4294967295, - * // "n": 0, - * // ... - * // } - * // ... - * // ] - * // ... - * // } - * // ], - * // "legacyAddress": "1LpbYkEM5cryfhs58tH8c93p4SGzit7UrP", - * // "cashAddress": "bitcoincash:qrvk436u4r0ew2wj0rd9pnxhx4w90p2yfc29ta0d2n", - * // "currentPage": 0 - * // } - * // ] - * // - * - */ - async transactions (address) { - try { - if (typeof address === 'string') { - const response = await axios.post( - `${this.ninsightURL}/address/transactions`, - { - addresses: [address] - }, - this.axiosOptions - ) - - return response.data - } else if (Array.isArray(address)) { - const response = await axios.post( - `${this.ninsightURL}/address/transactions`, - { - addresses: address - }, - this.axiosOptions - ) - - return response.data - } - - throw new Error('Input address must be a string or array of strings.') - } catch (error) { - if (error.response && error.response.data) throw error.response.data - else throw error - } - } - - /** - * @api Ninsight.txDetails() txDetails() - * @apiName Ninsight TX Details - * @apiGroup Ninsight - * @apiDescription Return transactions details for address(es). - * - * @apiExample Example usage: - * (async () => { - * try { - * let result = await bchjs.Ninsight.txDetails('fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33'); - * console.log(result); - * } catch(error) { - * console.error(error) - * } - * })() - * - * // [ - * // { - * // "txid": "fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33", - * // "version": 1, - * // "locktime": 0, - * // "vin": [ - * // { - * // "sequence": 4294967295, - * // "n": 0 - * // } - * // ], - * // "vout": [ - * // ... - * // ], - * // "blockhash": "00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09", - * // "blockheight": 1000, - * // "confirmations": 659909, - * // "time": 1232346882, - * // "blocktime": 1232346882, - * // "firstSeenTime": null, - * // "valueOut": 50, - * // "size": 135 - * // } - * // ] - * - */ - async txDetails (txid) { - try { - if (typeof txid === 'string') { - const response = await axios.post( - `${this.ninsightURL}/transaction/details`, - { - txids: [txid] - }, - this.axiosOptions - ) - - return response.data - } else if (Array.isArray(txid)) { - const response = await axios.post( - `${this.ninsightURL}/transaction/details`, - { - txids: txid - }, - this.axiosOptions - ) - - return response.data - } - - throw new Error('Transaction ID must be a string or array of strings.') - } catch (error) { - if (error.response && error.response.data) throw error.response.data - else throw error - } - } -} - -module.exports = Ninsight diff --git a/test/integration/transaction-integration.js b/test/integration/transaction-integration.js index 57a4136..b95a296 100644 --- a/test/integration/transaction-integration.js +++ b/test/integration/transaction-integration.js @@ -11,72 +11,74 @@ describe('#Transaction', () => { if (process.env.IS_USING_FREE_TIER) await bchjs.Util.sleep(1000) }) - describe('#getOld', () => { - it('should get details about a non-SLP transaction', async () => { - const txid = - '2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7' + if (process.env.TESTSLP) { + describe('#getOld', () => { + it('should get details about a non-SLP transaction', async () => { + const txid = + '2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7' - const result = await bchjs.Transaction.getOld(txid) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) + const result = await bchjs.Transaction.getOld(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) - // Assert that there are stanardized properties. - assert.property(result, 'txid') - assert.property(result, 'vin') - assert.property(result, 'vout') - assert.property(result.vout[0], 'value') - assert.property(result.vout[0].scriptPubKey, 'addresses') + // Assert that there are stanardized properties. + assert.property(result, 'txid') + assert.property(result, 'vin') + assert.property(result, 'vout') + assert.property(result.vout[0], 'value') + assert.property(result.vout[0].scriptPubKey, 'addresses') - // Assert that added properties exist. - assert.property(result.vin[0], 'address') - assert.property(result.vin[0], 'value') - assert.property(result, 'isValidSLPTx') - assert.equal(result.isValidSLPTx, false) + // Assert that added properties exist. + assert.property(result.vin[0], 'address') + assert.property(result.vin[0], 'value') + assert.property(result, 'isValidSLPTx') + assert.equal(result.isValidSLPTx, false) + }) + + it('should get details about a SLP transaction', async () => { + const txid = + '266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1' + + const result = await bchjs.Transaction.getOld(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // Assert that there are stanardized properties. + assert.property(result, 'txid') + assert.property(result, 'vin') + assert.property(result, 'vout') + assert.property(result.vout[0], 'value') + assert.property(result.vout[1].scriptPubKey, 'addresses') + + // Assert that added properties exist. + assert.property(result.vout[0], 'tokenQty') + assert.equal(result.vout[0].tokenQty, null) + assert.property(result.vin[0], 'address') + assert.property(result.vin[0], 'value') + assert.property(result.vin[0], 'tokenQty') + assert.property(result, 'isValidSLPTx') + assert.equal(result.isValidSLPTx, true) + }) + + // it('should get problematic transaction', async () => { + // const txid = 'a55515de32577e296c512840bcaabed5823bb773fb4f8fd8e5197cc96cbc54d1' + // + // const result = await bchjs.Transaction.get(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + // }) + + // TX a19f2f395a8b0e15b6202944c56834367d128f1e3630486a4756de53424a46fe has + // an input TXID (bd84bc1dd5ecd976165892306992401272f6bedeb37d7b2cdbf74fc4a55967a6) + // that is also a valid SLP tx, but is unrelated. Both TXs pass DAG validation, + // but for separate tokens. + it('should get problematic transaction', async () => { + const txid = + 'a19f2f395a8b0e15b6202944c56834367d128f1e3630486a4756de53424a46fe' + + const result = await bchjs.Transaction.getOld(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // The token ID should equal the txid for this Vin. + assert.equal(result.vin[2].txid, result.vin[2].tokenId) + }) }) - - it('should get details about a SLP transaction', async () => { - const txid = - '266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1' - - const result = await bchjs.Transaction.getOld(txid) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - // Assert that there are stanardized properties. - assert.property(result, 'txid') - assert.property(result, 'vin') - assert.property(result, 'vout') - assert.property(result.vout[0], 'value') - assert.property(result.vout[1].scriptPubKey, 'addresses') - - // Assert that added properties exist. - assert.property(result.vout[0], 'tokenQty') - assert.equal(result.vout[0].tokenQty, null) - assert.property(result.vin[0], 'address') - assert.property(result.vin[0], 'value') - assert.property(result.vin[0], 'tokenQty') - assert.property(result, 'isValidSLPTx') - assert.equal(result.isValidSLPTx, true) - }) - - // it('should get problematic transaction', async () => { - // const txid = 'a55515de32577e296c512840bcaabed5823bb773fb4f8fd8e5197cc96cbc54d1' - // - // const result = await bchjs.Transaction.get(txid) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - // }) - - // TX a19f2f395a8b0e15b6202944c56834367d128f1e3630486a4756de53424a46fe has - // an input TXID (bd84bc1dd5ecd976165892306992401272f6bedeb37d7b2cdbf74fc4a55967a6) - // that is also a valid SLP tx, but is unrelated. Both TXs pass DAG validation, - // but for separate tokens. - it('should get problematic transaction', async () => { - const txid = - 'a19f2f395a8b0e15b6202944c56834367d128f1e3630486a4756de53424a46fe' - - const result = await bchjs.Transaction.getOld(txid) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - // The token ID should equal the txid for this Vin. - assert.equal(result.vin[2].txid, result.vin[2].tokenId) - }) - }) + } }) diff --git a/test/unit/fixtures/ninsight-mock.js b/test/unit/fixtures/ninsight-mock.js deleted file mode 100644 index f455459..0000000 --- a/test/unit/fixtures/ninsight-mock.js +++ /dev/null @@ -1,170 +0,0 @@ -/* - Mocking data for unit tests for the Ninsight library. -*/ - -const utxo = { - utxos: [ - { - txid: '2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7', - vout: 0, - amount: 0.00001, - satoshis: 1000, - height: 602405, - confirmations: 36459 - } - ], - legacyAddress: '15NCRBJsHaJy8As5bX1oh2YauRejnZ1MKF', - cashAddress: 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9', - slpAddress: 'simpleledger:qqh793x9au6ehvh7r2zflzguanlme760wuwzunhjfm', - scriptPubKey: '76a9142fe2c4c5ef359bb2fe1a849f891cecffbcfb4f7788ac', - asm: - 'OP_DUP OP_HASH160 2fe2c4c5ef359bb2fe1a849f891cecffbcfb4f77 OP_EQUALVERIFY OP_CHECKSIG' -} - -const unconfirmed = { - utxos: [ - { - txid: '3904ffe6f8fba4ceda5e887130f60fcb18bdc7dcee10392a57f89475c5c108f1', - vout: 0, - amount: 0.03608203, - satoshis: 3608203, - confirmations: 0, - ts: 1559670801 - } - ], - legacyAddress: '1AyWs8U4HUnTLmxxFiGoJbsXauRsvBrcKW', - cashAddress: 'bitcoincash:qpkkjkhe29mqhqmu3evtq3dsnruuzl3rku6usknlh5', - slpAddress: 'simpleledger:qpkkjkhe29mqhqmu3evtq3dsnruuzl3rkuk8mdxlf2', - scriptPubKey: '76a9146d695af951760b837c8e58b045b098f9c17e23b788ac' -} - -const utxoPost = [utxo, utxo] -const unconfirmedPost = [unconfirmed, unconfirmed] - -const transactions = { - pagesTotal: 1, - txs: [ - { - txid: 'ec7bc8349386e3e1939bbdc4f8092fdbdd6a380734e68486b558cd594c451d5b', - version: 2, - locktime: 0, - vin: [ - { - txid: - '4f1fc57c33659628938db740449bf92fb75799e1d5750a4aeef80eb52d6df1e0', - vout: 0, - sequence: 4294967295, - n: 0, - scriptSig: { - hex: - '483045022100a3662a19ae384a1ceddea57765e425e61b04823e976d574da3911ac6b55d7f9b02200e571d985bce987675a2d58587a346fa40c39f4df13dc88548a92c52d5b24422412103f953f7630acc15bd3f5078c698f3af777286ae955b57e4857c158f75d87adb5f', - asm: - '3045022100a3662a19ae384a1ceddea57765e425e61b04823e976d574da3911ac6b55d7f9b02200e571d985bce987675a2d58587a346fa40c39f4df13dc88548a92c52d5b24422[ALL|FORKID] 03f953f7630acc15bd3f5078c698f3af777286ae955b57e4857c158f75d87adb5f' - }, - addr: '17HPz8RQ4XM6mjre6aspvqyj1j648CZidM', - valueSat: 1111, - value: 0.00001111, - doubleSpentTxID: null - }, - { - txid: - '126d62c299e7e14c66fe0b485d13082c23641f003690462046bc24ad2d1180c1', - vout: 0, - sequence: 4294967295, - n: 1, - scriptSig: { - hex: - '47304402203e3f923207111ff9bbd2fb5ab1a49a9145ad809ee0cad0e0ddaed64bfe38dc16022012ee288fb413bd500c63f8bb95e46b6b57d34762decd46b7188478a1c398eeda412103f953f7630acc15bd3f5078c698f3af777286ae955b57e4857c158f75d87adb5f', - asm: - '304402203e3f923207111ff9bbd2fb5ab1a49a9145ad809ee0cad0e0ddaed64bfe38dc16022012ee288fb413bd500c63f8bb95e46b6b57d34762decd46b7188478a1c398eeda[ALL|FORKID] 03f953f7630acc15bd3f5078c698f3af777286ae955b57e4857c158f75d87adb5f' - }, - addr: '17HPz8RQ4XM6mjre6aspvqyj1j648CZidM', - valueSat: 1000, - value: 0.00001, - doubleSpentTxID: null - } - ], - vout: [ - { - value: '0.00001736', - n: 0, - scriptPubKey: { - hex: '76a914d96ac75ca8df9729d278da50ccd7355c5785444e88ac', - asm: - 'OP_DUP OP_HASH160 d96ac75ca8df9729d278da50ccd7355c5785444e OP_EQUALVERIFY OP_CHECKSIG', - addresses: ['1LpbYkEM5cryfhs58tH8c93p4SGzit7UrP'], - type: 'pubkeyhash' - }, - spentTxId: null, - spentIndex: null, - spentHeight: null - } - ], - blockheight: -1, - confirmations: 0, - time: 1559673337, - valueOut: 0.00001736, - size: 339, - valueIn: 0.00002111, - fees: 0.00000375 - } - ], - legacyAddress: '1LpbYkEM5cryfhs58tH8c93p4SGzit7UrP', - cashAddress: 'bitcoincash:qrvk436u4r0ew2wj0rd9pnxhx4w90p2yfc29ta0d2n', - currentPage: 0 -} - -const transactionsPost = [transactions, transactions] - -const details = { - txid: 'fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33', - version: 1, - locktime: 0, - vin: [ - { - coinbase: '04ffff001d02fd04', - sequence: 4294967295, - n: 0 - } - ], - vout: [ - { - value: '50.00000000', - n: 0, - scriptPubKey: { - hex: - '4104f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446aac', - asm: - '04f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446a OP_CHECKSIG', - addresses: ['1BW18n7MfpU35q4MTBSk8pse3XzQF8XvzT'], - type: 'pubkeyhash', - cashAddrs: ['bitcoincash:qpej6mkrwca4tvy2snq4crhrf88v4ljspysx0ueetk'] - }, - spentTxId: null, - spentIndex: null, - spentHeight: null - } - ], - blockhash: - '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09', - blockheight: 1000, - confirmations: 585610, - time: 1232346882, - blocktime: 1232346882, - isCoinBase: true, - valueOut: 50, - size: 135 -} - -const detailsPost = [details, details] - -module.exports = { - utxo, - utxoPost, - unconfirmed, - unconfirmedPost, - transactions, - transactionsPost, - details, - detailsPost -} diff --git a/test/unit/ninsight.js b/test/unit/ninsight.js deleted file mode 100644 index 45a898e..0000000 --- a/test/unit/ninsight.js +++ /dev/null @@ -1,255 +0,0 @@ -const chai = require('chai') -const assert = chai.assert -const axios = require('axios') -const sinon = require('sinon') - -const BCHJS = require('../../src/bch-js') -const bchjs = new BCHJS() - -const mockData = require('./fixtures/ninsight-mock') - -describe('#Ninsight', () => { - let sandbox - beforeEach(() => (sandbox = sinon.createSandbox())) - afterEach(() => sandbox.restore()) - - describe('#utxo', () => { - it('should throw an error for improper input', async () => { - try { - const addr = 12345 - - await bchjs.Ninsight.utxo(addr) - assert.equal(true, false, 'Unexpected result!') - } catch (err) { - // console.log(`err: `, err) - assert.include( - err.message, - 'Input address must be a string or array of strings.' - ) - } - }) - - it('should GET utxos for a single address', async () => { - // Stub the network call. - sandbox.stub(axios, 'post').resolves({ data: mockData.utxo }) - - const addr = 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9' - - const result = await bchjs.Ninsight.utxo(addr) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.property(result, 'utxos') - assert.property(result, 'legacyAddress') - assert.property(result, 'cashAddress') - assert.property(result, 'slpAddress') - assert.property(result, 'scriptPubKey') - assert.property(result, 'asm') - - assert.isArray(result.utxos) - - assert.property(result.utxos[0], 'txid') - assert.property(result.utxos[0], 'vout') - assert.property(result.utxos[0], 'amount') - assert.property(result.utxos[0], 'satoshis') - assert.property(result.utxos[0], 'height') - assert.property(result.utxos[0], 'confirmations') - }) - - it('should POST utxo details for an array of addresses', async () => { - // Mock the network call. - sandbox.stub(axios, 'post').resolves({ data: mockData.utxoPost }) - - const addr = [ - 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7', - 'bitcoincash:qz0us0z6ucpqt07jgpad0shgh7xmwxyr3ynlcsq0wr' - ] - - const result = await bchjs.Ninsight.utxo(addr) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.isArray(result[0].utxos) - - assert.property(result[0], 'utxos') - assert.property(result[0], 'legacyAddress') - assert.property(result[0], 'cashAddress') - assert.property(result[0], 'slpAddress') - assert.property(result[0], 'scriptPubKey') - assert.property(result[0], 'asm') - }) - }) - - describe('#unconfirmed', () => { - it('should throw an error for improper input', async () => { - try { - const addr = 12345 - - await bchjs.Ninsight.unconfirmed(addr) - assert.equal(true, false, 'Unexpected result!') - } catch (err) { - assert.include( - err.message, - 'Input address must be a string or array of strings.' - ) - } - }) - - it('should POST utxos for a single address', async () => { - // Stub the network call. - sandbox.stub(axios, 'post').resolves({ data: mockData.unconfirmed }) - - const addr = 'bitcoincash:qpkkjkhe29mqhqmu3evtq3dsnruuzl3rku6usknlh5' - - const result = await bchjs.Ninsight.unconfirmed(addr) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.property(result, 'utxos') - assert.property(result, 'legacyAddress') - assert.property(result, 'cashAddress') - assert.property(result, 'slpAddress') - assert.property(result, 'scriptPubKey') - - assert.isArray(result.utxos) - - assert.property(result.utxos[0], 'txid') - assert.property(result.utxos[0], 'vout') - assert.property(result.utxos[0], 'amount') - assert.property(result.utxos[0], 'satoshis') - assert.property(result.utxos[0], 'confirmations') - assert.property(result.utxos[0], 'ts') - }) - - it('should POST utxo details for an array of addresses', async () => { - // Mock the network call. - sandbox.stub(axios, 'post').resolves({ data: mockData.unconfirmedPost }) - - const addr = [ - 'bitcoincash:qpkkjkhe29mqhqmu3evtq3dsnruuzl3rku6usknlh5', - 'bitcoincash:qz0us0z6ucpqt07jgpad0shgh7xmwxyr3ynlcsq0wr' - ] - - const result = await bchjs.Ninsight.unconfirmed(addr) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.isArray(result[0].utxos) - - assert.property(result[0], 'utxos') - assert.property(result[0], 'legacyAddress') - assert.property(result[0], 'cashAddress') - assert.property(result[0], 'slpAddress') - assert.property(result[0], 'scriptPubKey') - }) - }) - - describe('#transactions', () => { - it('should throw an error for improper input', async () => { - try { - const addr = 12345 - - await bchjs.Ninsight.transactions(addr) - assert.equal(true, false, 'Unexpected result!') - } catch (err) { - // console.log(`err: `, err) - assert.include( - err.message, - 'Input address must be a string or array of strings.' - ) - } - }) - it('should POST transaction history for a single address', async () => { - // Stub the network call. - sandbox.stub(axios, 'post').resolves({ data: mockData.transactionsPost }) - - const addr = 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9' - - const result = await bchjs.Ninsight.transactions(addr) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.property(result[0], 'cashAddress') - assert.property(result[0], 'legacyAddress') - assert.property(result[0], 'txs') - assert.isArray(result[0].txs) - assert.property(result[0].txs[0], 'txid') - assert.property(result[0].txs[0], 'vin') - assert.property(result[0].txs[0], 'vout') - }) - it('should POST transaction history for an array of addresses', async () => { - // Mock the network call. - sandbox.stub(axios, 'post').resolves({ data: mockData.transactionsPost }) - - const addr = [ - 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7', - 'bitcoincash:qz0us0z6ucpqt07jgpad0shgh7xmwxyr3ynlcsq0wr' - ] - - const result = await bchjs.Ninsight.transactions(addr) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.property(result[0], 'cashAddress') - assert.property(result[0], 'legacyAddress') - assert.property(result[0], 'txs') - assert.isArray(result[0].txs) - assert.property(result[0].txs[0], 'txid') - }) - }) - describe('#txDetails', () => { - it('should throw an error for improper input', async () => { - try { - const txid = 12345 - - await bchjs.Ninsight.txDetails(txid) - assert.equal(true, false, 'Unexpected result!') - } catch (err) { - // console.log(`err: `, err) - assert.include( - err.message, - 'Transaction ID must be a string or array of strings.' - ) - } - }) - it('should POST transaction details for a single TxID', async () => { - // Stub the network call. - sandbox.stub(axios, 'post').resolves({ data: mockData.detailsPost }) - - const txid = 'fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33' - - const result = await bchjs.Ninsight.txDetails(txid) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.property(result[0], 'txid') - assert.property(result[0], 'version') - assert.property(result[0], 'locktime') - assert.property(result[0], 'vin') - assert.property(result[0], 'vout') - assert.property(result[0], 'blockhash') - assert.property(result[0], 'blockheight') - assert.property(result[0], 'confirmations') - assert.property(result[0], 'time') - assert.property(result[0], 'blocktime') - assert.property(result[0], 'isCoinBase') - assert.property(result[0], 'valueOut') - assert.property(result[0], 'size') - }) - it('should POST transaction details for an array of TxIDs', async () => { - // Stub the network call. - sandbox.stub(axios, 'post').resolves({ data: mockData.detailsPost }) - - const txid = [ - 'fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33', - 'fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33' - ] - - const result = await bchjs.Ninsight.txDetails(txid) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.property(result[0], 'txid') - assert.property(result[0], 'vin') - assert.property(result[0], 'vout') - }) - }) -}) From 1056c3724cfc71f4f64c1c94ce7d01ea566dd826 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 18 Jan 2022 11:41:52 -0800 Subject: [PATCH 3/3] feat(Utxo.get()): Moving old function to getOld. New get() uses psf-slp-indexer --- src/psf-slp-indexer.js | 1 + src/utxo.js | 156 +++++++++++++++++- .../chains/bchn/utxo-integration.js | 36 +++- test/unit/fixtures/utxo-mocks.js | 92 ++++++++++- test/unit/utxo-unit.js | 58 ++++++- 5 files changed, 328 insertions(+), 15 deletions(-) diff --git a/src/psf-slp-indexer.js b/src/psf-slp-indexer.js index 194b622..ac89dbc 100644 --- a/src/psf-slp-indexer.js +++ b/src/psf-slp-indexer.js @@ -132,6 +132,7 @@ class PsfSlpIndexer { */ async balance (address) { try { + console.log('balance() address: ', address) // Handle single address. if (typeof address === 'string') { const response = await axios.post( diff --git a/src/utxo.js b/src/utxo.js index 6982ec6..ad21d57 100644 --- a/src/utxo.js +++ b/src/utxo.js @@ -8,19 +8,21 @@ // Local libraries const Electrumx = require('./electrumx') const Slp = require('./slp/slp') +const PsfSlpIndexer = require('./psf-slp-indexer') class UTXO { - constructor (config) { + constructor (config = {}) { // Encapsulate dependencies for easier mocking. this.electrumx = new Electrumx(config) this.slp = new Slp(config) + this.psfSlpIndexer = new PsfSlpIndexer(config) } /** - * @api Utxo.get() get() - * @apiName get + * @api Utxo.getOld() getOld() + * @apiName getOld * @apiGroup UTXO - * @apiDescription Get UTXOs for an address + * @apiDescription Get UTXOs for an address (from SLPDB) * * Given an address, this function will return an object with thre following * properties: @@ -45,7 +47,7 @@ class UTXO { * @apiExample Example usage: * (async () => { * try { - * let utxos = await bchjs.Utxo.get('simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj'); + * let utxos = await bchjs.Utxo.getOld('simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj'); * console.log(utxos); * } catch(error) { * console.error(error) @@ -181,7 +183,7 @@ class UTXO { * * */ - async get (address, useWhitelist = false) { + async getOld (address, useWhitelist = false) { try { if (!address) { throw new Error('Address must be an array or a string') @@ -286,6 +288,148 @@ class UTXO { } } + /** + * @api Utxo.get() get() + * @apiName get + * @apiGroup UTXO + * @apiDescription Get UTXOs for an address (from psf-slp-indexer) + * + * Given an address, this function will return an object with thre following + * properties: + * - address: "" - the address these UTXOs are associated with + * - bchUtxos: [] - UTXOs confirmed to be spendable as normal BCH + * - nullUtxo: [] - UTXOs that did not pass SLP validation. Should be ignored and + * not spent, to be safe. + * - slpUtxos: {} - UTXOs confirmed to be colored as valid SLP tokens + * - type1: {} + * - tokens: [] - SLP token Type 1 tokens. + * - mintBatons: [] - SLP token Type 1 mint batons. + * - nft: {} + * - tokens: [] - NFT tokens + * - groupTokens: [] - NFT Group tokens, used to create NFT tokens. + * - groupMintBatons: [] - Minting baton to create more NFT Group tokens. + * + * Note: You can pass in an optional second Boolean argument. The default + * `false` will use the normal waterfall validation method. Set to `true`, + * SLP UTXOs will be validated with the whitelist filtered SLPDB. This will + * result is many more UTXOs in the `nullUtxos` array. + * + * @apiExample Example usage: + * (async () => { + * try { + * let utxos = await bchjs.Utxo.get('simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj'); + * console.log(utxos); + * } catch(error) { + * console.error(error) + * } + * })() + * + * // returns + * [ + * { + * "address": "bitcoincash:qrm0c67wwqh0w7wjxua2gdt2xggnm90xws00a3lezv", + * "bchUtxos": [ + * { + * "height": 674513, + * "tx_hash": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04", + * "tx_pos": 3, + * "value": 38134, + * "txid": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04", + * "vout": 3, + * "isValid": false + * } + * ], + */ + // This version of get() uses the psf-slp-indexer. It will replace the older + // get() function that uses SLPDB. + // TODO: NFT UTXOs are identified as non-token UTXOs, which will cause a wallet + // to burn them. The psf-slp-indexer needs to be updated to mark these UTXOs. + async get (address) { + try { + // Convert address to an array if it is a string. + if (typeof address !== 'string') { + throw new Error('address input must be a string') + } + + // Ensure the address is a BCH address. + const addr = this.slp.Address.toCashAddress(address) + + // Get the UTXOs associated with the address. + const utxoData = await this.electrumx.utxo(addr) + // console.log(`utxoData: ${JSON.stringify(utxoData, null, 2)}`) + const utxos = utxoData.utxos + + // Get SLP UTXOs from the psf-slp-indexer + const slpUtxoData = await this.psfSlpIndexer.balance(addr) + // console.log(`slpUtxoData: ${JSON.stringify(slpUtxoData, null, 2)}`) + const slpUtxos = slpUtxoData.balance.utxos + + // Loop through the Fulcrum UTXOs. + for (let i = 0; i < utxos.length; i++) { + const thisUtxo = utxos[i] + + // Loop through the UTXOs from psf-slp-indexer. + for (let j = 0; j < slpUtxos.length; j++) { + const thisSlpUtxo = slpUtxos[j] + + // If the non-hydrated UTXO matches the SLP UTXO, then combine the data + // and mark the UTXO as an SLP token. + if ( + thisUtxo.tx_hash === thisSlpUtxo.txid && + thisUtxo.tx_pos === thisSlpUtxo.vout + ) { + thisUtxo.txid = thisUtxo.tx_hash + thisUtxo.vout = thisUtxo.tx_pos + thisUtxo.isSlp = true + thisUtxo.type = thisSlpUtxo.type + thisUtxo.qty = thisSlpUtxo.qty + thisUtxo.tokenId = thisSlpUtxo.tokenId + thisUtxo.address = thisSlpUtxo.address + + break + } + } + + // If there was no match, then this is a normal BCH UTXO. Mark it as such. + if (!thisUtxo.isSlp) { + thisUtxo.txid = thisUtxo.tx_hash + thisUtxo.vout = thisUtxo.tx_pos + thisUtxo.isSlp = false + thisUtxo.address = addr + } + } + + const bchUtxos = utxos.filter(x => x.isSlp === false) + const type1TokenUtxos = utxos.filter( + x => x.isSlp === true && x.type === 'token' + ) + const type1BatonUtxos = utxos.filter( + x => x.isSlp === true && x.type === 'baton' + ) + const nullUtxos = utxos.filter(x => x.isSlp === null) + + const outObj = { + address: addr, + bchUtxos, + slpUtxos: { + type1: { + tokens: type1TokenUtxos, + mintBatons: type1BatonUtxos + }, + nft: {} // Allocated for future support of NFT spec. + }, + nullUtxos + } + + return outObj + } catch (err) { + // console.error('Error in bchjs.utxo.get2(): ', err) + + if (err.error) throw new Error(err.error) + throw err + } + } + /** * @api Utxo.findBiggestUtxo() findBiggestUtxo() * @apiName findBiggestUtxo diff --git a/test/integration/chains/bchn/utxo-integration.js b/test/integration/chains/bchn/utxo-integration.js index bcb5a14..1a2e3a0 100644 --- a/test/integration/chains/bchn/utxo-integration.js +++ b/test/integration/chains/bchn/utxo-integration.js @@ -13,7 +13,7 @@ describe('#UTXO', () => { if (process.env.IS_USING_FREE_TIER) await sleep(3000) }) - + /* describe('#get', () => { it('should get hydrated and filtered UTXOs for an address', async () => { // const addr = 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9' @@ -90,7 +90,40 @@ describe('#UTXO', () => { assert.isAbove(result[0].nullUtxos.length, 1) }) }) +*/ + describe('#get', () => { + it('should hydrate address with BCH and SLP UTXOs', async () => { + const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' + const result = await bchjs.Utxo.get(addr) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // Assert expected properties exist. + assert.property(result, 'address') + assert.property(result, 'bchUtxos') + assert.property(result, 'slpUtxos') + assert.property(result.slpUtxos, 'type1') + assert.property(result.slpUtxos, 'nft') + assert.property(result, 'nullUtxos') + + assert.isAbove(result.bchUtxos.length, 0) + assert.isAbove(result.slpUtxos.type1.tokens.length, 0) + assert.equal(result.slpUtxos.type1.mintBatons.length, 0) + }) + + // TODO: NFTs are currently not identified as different than normal BCH UTXOs. + // The psf-slp-indexer needs to be updated to fix this issue. + it('should handle NFTs and minting batons', async () => { + const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj' + + const result = await bchjs.Utxo.get(addr) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // Assert that minting batons are correctly identified. + assert.isAbove(result.slpUtxos.type1.mintBatons.length, 0) + }) + }) + /* describe('#findBiggestUtxo', () => { it('should sort UTXOs from Electrumx', async () => { const addr = 'bitcoincash:qq54fgjn3hz0357n8a6guy4demw9xfkjk5jcj0xr0z' @@ -118,6 +151,7 @@ describe('#UTXO', () => { assert.equal(result.satoshis, 800) }) }) +*/ }) function sleep (ms) { diff --git a/test/unit/fixtures/utxo-mocks.js b/test/unit/fixtures/utxo-mocks.js index 72094c5..df49eae 100644 --- a/test/unit/fixtures/utxo-mocks.js +++ b/test/unit/fixtures/utxo-mocks.js @@ -280,10 +280,100 @@ const electrumxUtxos = { ] } +const fulcrumUtxos01 = { + success: true, + utxos: [ + { + height: 674512, + tx_hash: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + tx_pos: 1, + value: 546 + }, + { + height: 674512, + tx_hash: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + tx_pos: 2, + value: 546 + }, + { + height: 674512, + tx_hash: + 'eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b', + tx_pos: 1, + value: 546 + }, + { + height: 674513, + tx_hash: + '705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04', + tx_pos: 1, + value: 546 + }, + { + height: 674513, + tx_hash: + '705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04', + tx_pos: 2, + value: 546 + }, + { + height: 674513, + tx_hash: + '705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04', + tx_pos: 3, + value: 38134 + } + ] +} + +const psfSlpIndexerUtxos01 = { + balance: { + utxos: [ + { + txid: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + vout: 1, + type: 'token', + qty: '10000000000', + tokenId: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + address: 'bitcoincash:qrm0c67wwqh0w7wjxua2gdt2xggnm90xws00a3lezv' + }, + { + txid: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + vout: 2, + type: 'baton', + tokenId: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + address: 'bitcoincash:qrm0c67wwqh0w7wjxua2gdt2xggnm90xws00a3lezv' + } + ], + txs: [ + { + txid: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + height: 674512 + } + ], + balances: [ + { + tokenId: + 'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230', + qty: '10000000000' + } + ] + } +} + module.exports = { mockUtxoData, mockHydratedUtxos, mockTwoHydratedAddrs, mockEveryUtxoType, - electrumxUtxos + electrumxUtxos, + fulcrumUtxos01, + psfSlpIndexerUtxos01 } diff --git a/test/unit/utxo-unit.js b/test/unit/utxo-unit.js index 3992f65..e4d3b63 100644 --- a/test/unit/utxo-unit.js +++ b/test/unit/utxo-unit.js @@ -15,7 +15,7 @@ describe('#utxo', () => { beforeEach(() => (sandbox = sinon.createSandbox())) afterEach(() => sandbox.restore()) - describe('#get', () => { + describe('#getOld', () => { it('should get hydrated and filtered UTXOs for an address', async () => { // Mock dependencies. sandbox.stub(bchjs.Utxo.electrumx, 'utxo').resolves(mockData.mockUtxoData) @@ -25,7 +25,7 @@ describe('#utxo', () => { const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' - const result = await bchjs.Utxo.get(addr) + const result = await bchjs.Utxo.getOld(addr) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) @@ -46,7 +46,7 @@ describe('#utxo', () => { const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' - await bchjs.Utxo.get(addr) + await bchjs.Utxo.getOld(addr) assert.fail('Unexpected code path') } catch (err) { @@ -66,7 +66,7 @@ describe('#utxo', () => { 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9' ] - const result = await bchjs.Utxo.get(addr) + const result = await bchjs.Utxo.getOld(addr) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) @@ -88,7 +88,7 @@ describe('#utxo', () => { addrs.push(addr) } - await bchjs.Utxo.get(addrs) + await bchjs.Utxo.getOld(addrs) assert.fail('Unexpected code path') } catch (err) { @@ -105,7 +105,7 @@ describe('#utxo', () => { const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj' - const result = await bchjs.Utxo.get(addr) + const result = await bchjs.Utxo.getOld(addr) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) @@ -133,7 +133,7 @@ describe('#utxo', () => { const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' const useWhitelist = true - const result = await bchjs.Utxo.get(addr, useWhitelist) + const result = await bchjs.Utxo.getOld(addr, useWhitelist) // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.isArray(result) @@ -188,4 +188,48 @@ describe('#utxo', () => { assert.equal(result.satoshis, 800) }) }) + + describe('#get', () => { + it('should throw an error if input is not a string', async () => { + try { + const addr = 123 + + await bchjs.Utxo.get(addr) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'address input must be a string') + } + }) + + it('should return UTXO information', async () => { + // mock dependencies + sandbox + .stub(bchjs.Utxo.electrumx, 'utxo') + .resolves(mockData.fulcrumUtxos01) + sandbox + .stub(bchjs.Utxo.psfSlpIndexer, 'balance') + .resolves(mockData.psfSlpIndexerUtxos01) + + const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj' + + const result = await bchjs.Utxo.get(addr) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // Assert expected properties exist + assert.property(result, 'address') + assert.property(result, 'bchUtxos') + assert.property(result, 'slpUtxos') + assert.property(result.slpUtxos, 'type1') + assert.property(result.slpUtxos.type1, 'tokens') + assert.property(result.slpUtxos.type1, 'mintBatons') + assert.property(result.slpUtxos, 'nft') + assert.property(result, 'nullUtxos') + + assert.equal(result.bchUtxos.length, 4) + assert.equal(result.slpUtxos.type1.tokens.length, 1) + assert.equal(result.slpUtxos.type1.mintBatons.length, 1) + assert.equal(result.nullUtxos.length, 0) + }) + }) })