diff --git a/src/bch-js.js b/src/bch-js.js index 649aba4..9fc7be1 100644 --- a/src/bch-js.js +++ b/src/bch-js.js @@ -38,6 +38,7 @@ const Ecash = require('./ecash') // Indexers const Ninsight = require('./ninsight') const Electrumx = require('./electrumx') +const PsfSlpIndexer = require('./psf-slp-indexer') class BCHJS { constructor (config) { @@ -120,6 +121,8 @@ class BCHJS { this.DSProof = new DSProof(libConfig) this.eCash = new Ecash() + + this.PsfSlpIndexer = new PsfSlpIndexer(libConfig) } } diff --git a/src/psf-slp-indexer.js b/src/psf-slp-indexer.js new file mode 100644 index 0000000..7a614ac --- /dev/null +++ b/src/psf-slp-indexer.js @@ -0,0 +1,292 @@ +/* + This library interacts with the PSF slp indexer REST API endpoints operated + by FullStack.cash +*/ +// Public npm libraries +const axios = require('axios') + +// let _this + +class PsfSlpIndexer { + constructor (config) { + this.restURL = config.restURL + this.apiToken = config.apiToken + this.authToken = config.authToken + + if (this.authToken) { + // Add Basic Authentication token to the authorization header. + this.axiosOptions = { + headers: { + authorization: this.authToken + } + } + } else { + // Add JWT token to the authorization header. + this.axiosOptions = { + headers: { + authorization: `Token ${this.apiToken}` + } + } + } + + // _this = this + } + + /** + * @api PsfSlpIndexer.status() status() + * @apiName Status + * @apiGroup PSF SLP + * @apiDescription Return status from psf slp indexer. + * + * @apiExample Example usage: + * (async () => { + * try { + * let status = await bchjs.PsfSlpIndexer.status(); + * console.log(status); + * } catch(error) { + * console.error(error) + * } + * })() + * + * { + * "status": { + * "startBlockHeight": 543376, + * "syncedBlockHeight": 723249, + * "chainBlockHeight": 722679 + * } + * } + * + */ + async status () { + try { + const response = await axios.get( + `${this.restURL}psf/slp/status`, + this.axiosOptions + ) + return response.data + } catch (error) { + if (error.response && error.response.data) throw error.response.data + else throw error + } + } + + /** + * @api PsfSlpIndexer.balance() balance() + * @apiName SLP Balance + * @apiGroup PSF SLP + * @apiDescription Return slp balance for a single address. + * + * @apiExample Example usage: + * (async () => { + * try { + * let balance = await bchjs.PsfSlpIndexer.balance('bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'); + * console.log(balance); + * } catch(error) { + * console.error(error) + * } + * })() + * + * { + * balance: { + * utxos: [ + * { + * txid: 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9', + * vout: 1, + * type: 'token', + * qty: '1800', + * tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + * address: 'bitcoincash:qrqy3kj7r822ps6628vwqq5k8hyjl6ey3y4eea2m4s' + * } + * ], + * txs: [ + * { + * txid: '078b2c48ed1db0d5d5996f2889b8d847a49200d0a781f6aa6752f740f312688f', + * height: 717796 + * }, + * { + * txid: 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9', + * height: 717832 + * } + * ], + * balances: [ + * { + * tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + * qty: '1800' + * } + * ] + * } + * } + * + */ + async balance (address) { + try { + // Handle single address. + if (typeof address === 'string') { + const response = await axios.post( + `${this.restURL}psf/slp/address`, + { address }, + this.axiosOptions + ) + return response.data + } + throw new Error('Input address must be a string.') + } catch (error) { + if (error.response && error.response.data) throw error.response.data + else throw error + } + } + + /** + * @api PsfSlpIndexer.tokenStats() tokenStats() + * @apiName Token Stats + * @apiGroup PSF SLP + * @apiDescription Return list stats for a single slp token. + * + * @apiExample Example usage: + * (async () => { + * try { + * let tokenStats = await bchjs.PsfSlpIndexer.tokenStats('a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'); + * console.log(tokenStats); + * } catch(error) { + * console.error(error) + * } + * })() + * + * { + * tokenData: { + * type: 1, + * ticker: 'TROUT', + * name: "Trout's test token", + * tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + * documentUri: 'troutsblog.com', + * documentHash: '', + * decimals: 2, + * mintBatonIsActive: true, + * tokensInCirculationBN: '100098953386', + * tokensInCirculationStr: '100098953386', + * blockCreated: 622414, + * totalBurned: '1046614', + * totalMinted: '100100000000' + * txs: [ + * { + * txid: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + * height: 622414, + * type: 'GENESIS', + * qty: '100000000000' + * } + * ] + * } + * } + * + */ + + async tokenStats (tokenId) { + try { + // Handle single address. + if (typeof tokenId === 'string') { + const response = await axios.post( + `${this.restURL}psf/slp/token`, + { tokenId }, + this.axiosOptions + ) + return response.data + } + throw new Error('Input tokenId must be a string.') + } catch (error) { + if (error.response && error.response.data) throw error.response.data + else throw error + } + } + + /** + * @api PsfSlpIndexer.tx() tx() + * @apiName SLP Transaction Data + * @apiGroup PSF SLP + * @apiDescription Return slp transaction data. + * + * @apiExample Example usage: + * (async () => { + * try { + * let txData = await bchjs.PsfSlpIndexer.tx('a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'); + * console.log(txData); + * } catch(error) { + * console.error(error) + * } + * })() + * + * { + * txData: { + * txid: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + * hash: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + * version: 2, + * size: 339, + * locktime: 0, + * vin: [ + * { + * txid: '8370db30d94761ab9a11b71ecd22541151bf6125c8c613f0f6fab8ab794565a7', + * vout: 0, + * scriptSig: { + * asm: '304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4[ALL|FORKID] 02791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851', + * hex: '47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851' + * }, + * sequence: 4294967295, + * address: 'bitcoincash:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qwgaqm3wq', + * value: 0.00051303, + * tokenQty: 0, + * tokenQtyStr: '0', + * tokenId: null + * } + * ], + * vout: [ + * { + * value: 0, + * n: 0, + * scriptPubKey: { + * 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', + * confirmations: 97398, + * time: 1581773131, + * blocktime: 1581773131, + * blockheight: 622414, + * isSlpTx: true, + * tokenTxType: 'GENESIS', + * tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + * tokenType: 1, + * tokenTicker: 'TROUT', + * tokenName: "Trout's test token", + * tokenDecimals: 2, + * tokenUri: 'troutsblog.com', + * tokenDocHash: '', + * isValidSlp: true + * } + * } + * + */ + async tx (txid) { + try { + // Handle single address. + if (typeof txid === 'string') { + const response = await axios.post( + `${this.restURL}psf/slp/txid`, + { txid }, + this.axiosOptions + ) + return response.data + } + throw new Error('Input txid must be a string.') + } catch (error) { + if (error.response && error.response.data) throw error.response.data + else throw error + } + } +} + +module.exports = PsfSlpIndexer diff --git a/test/integration/chains/bchn/psf-slp-indexer.integration.js b/test/integration/chains/bchn/psf-slp-indexer.integration.js new file mode 100644 index 0000000..979510d --- /dev/null +++ b/test/integration/chains/bchn/psf-slp-indexer.integration.js @@ -0,0 +1,74 @@ +/* + Integration tests for the psf-slp-indexer.js library +*/ + +const assert = require('chai').assert + +const BCHJS = require('../../../../src/bch-js') +let bchjs + +describe('#psf-slp-indexer', () => { + beforeEach(async () => { + // Introduce a delay so that the BVT doesn't trip the rate limits. + if (process.env.IS_USING_FREE_TIER) await sleep(3000) + + bchjs = new BCHJS() + }) + + describe('#status', () => { + it('should return the status of the indexer.', async () => { + const result = await bchjs.PsfSlpIndexer.status() + // console.log('result: ', result) + + assert.property(result.status, 'startBlockHeight') + assert.property(result.status, 'syncedBlockHeight') + assert.property(result.status, 'chainBlockHeight') + }) + }) + + describe('#balance', () => { + it('should get balance data for an address.', async () => { + const addr = 'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n' + + const result = await bchjs.PsfSlpIndexer.balance(addr) + // console.log('result: ', result) + + assert.property(result.balance, 'utxos') + assert.property(result.balance, 'txs') + assert.property(result.balance, 'balances') + }) + }) + + describe('#tokenStats', () => { + it('should get stats on a token', async () => { + const tokenId = + '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0' + + const result = await bchjs.PsfSlpIndexer.tokenStats(tokenId) + // console.log('result: ', result) + + assert.property(result.tokenData, 'documentUri') + assert.property(result.tokenData, 'txs') + assert.property(result.tokenData, 'totalBurned') + }) + }) + + describe('#tx', () => { + it('should get hydrated tx data', async () => { + const txid = + '83361c34cac2ea7f9ca287fca57a96cc0763719f0cdf4850f9696c1e68eb635c' + + const result = await bchjs.PsfSlpIndexer.tx(txid) + // console.log('result: ', result) + + assert.property(result.txData, 'vin') + assert.property(result.txData, 'vout') + assert.property(result.txData, 'isValidSlp') + }) + }) +}) + +// Promise-based sleep function +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/test/unit/fixtures/psf-slp-indexer-mock.js b/test/unit/fixtures/psf-slp-indexer-mock.js new file mode 100644 index 0000000..c5541a0 --- /dev/null +++ b/test/unit/fixtures/psf-slp-indexer-mock.js @@ -0,0 +1,130 @@ +/* + Mocking data for unit tests for the PSF SLP INDEXER library. +*/ + +'use strict' + +const tokenStats = { + tokenData: { + type: 1, + ticker: 'TP03', + name: 'Test Plugin 03', + tokenId: '13cad617d523c8eb4ab11fff19c010e0e0a4ea4360b58e0c8c955a45a146a669', + documentUri: 'fullstack.cash', + documentHash: '', + decimals: 0, + mintBatonIsActive: false, + tokensInCirculationBN: '1', + tokensInCirculationStr: '1', + blockCreated: 722420, + totalBurned: '0', + totalMinted: '1', + txs: [ + { + txid: '13cad617d523c8eb4ab11fff19c010e0e0a4ea4360b58e0c8c955a45a146a669', + height: 722420, + type: 'GENESIS', + qty: '1' + } + ] + } +} + +const txData = { + txData: { + txid: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + hash: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + version: 2, + size: 339, + locktime: 0, + vin: [ + { + txid: '8370db30d94761ab9a11b71ecd22541151bf6125c8c613f0f6fab8ab794565a7', + vout: 0, + scriptSig: { + asm: '304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4[ALL|FORKID] 02791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851', + hex: '47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851' + }, + sequence: 4294967295, + address: 'bitcoincash:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qwgaqm3wq', + value: 0.00051303, + tokenQty: 0, + tokenQtyStr: '0', + tokenId: null + } + ], + vout: [ + { + value: 0, + n: 0, + scriptPubKey: { + 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', + confirmations: 97398, + time: 1581773131, + blocktime: 1581773131, + blockheight: 622414, + isSlpTx: true, + tokenTxType: 'GENESIS', + tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + tokenType: 1, + tokenTicker: 'TROUT', + tokenName: "Trout's test token", + tokenDecimals: 2, + tokenUri: 'troutsblog.com', + tokenDocHash: '', + isValidSlp: true + } +} +const balance = { + balance: { + utxos: [ + { + txid: 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9', + vout: 1, + type: 'token', + qty: '1800', + tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + address: 'bitcoincash:qrqy3kj7r822ps6628vwqq5k8hyjl6ey3y4eea2m4s' + } + ], + txs: [ + { + txid: '078b2c48ed1db0d5d5996f2889b8d847a49200d0a781f6aa6752f740f312688f', + height: 717796 + }, + { + txid: 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9', + height: 717832 + } + ], + balances: [ + { + tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + qty: '1800' + } + ] + } +} + +const status = { + status: { + startBlockHeight: 543376, + syncedBlockHeight: 722860, + chainBlockHeight: 722679 + } +} +module.exports = { + tokenStats, + txData, + balance, + status +} diff --git a/test/unit/psf-slp-indexer.js b/test/unit/psf-slp-indexer.js new file mode 100644 index 0000000..f695cc7 --- /dev/null +++ b/test/unit/psf-slp-indexer.js @@ -0,0 +1,258 @@ +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/psf-slp-indexer-mock') + +describe('#PsfSlpIndexer', () => { + let sandbox + beforeEach(() => (sandbox = sinon.createSandbox())) + afterEach(() => sandbox.restore()) + + describe('#status', () => { + it('should GET status', async () => { + // Stub the network call. + sandbox.stub(axios, 'get').resolves({ data: mockData.status }) + + const result = await bchjs.PsfSlpIndexer.status() + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + assert.property(result, 'status') + assert.property(result.status, 'startBlockHeight') + assert.property(result.status, 'syncedBlockHeight') + assert.property(result.status, 'chainBlockHeight') + }) + + it('should handle axios error', async () => { + try { + // Stub the network call. + sandbox.stub(axios, 'get').throws(new Error('test error')) + + await bchjs.PsfSlpIndexer.status() + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + assert.equal(true, false, 'Unexpected result!') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + it('should handle request error', async () => { + try { + // Stub the network call. + const testErr = new Error() + testErr.response = { data: { status: 422 } } + sandbox.stub(axios, 'get').throws(testErr) + + await bchjs.PsfSlpIndexer.status() + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + assert.equal(true, false, 'Unexpected result!') + } catch (err) { + assert.equal(err.status, 422) + } + }) + }) + + describe('#balance', () => { + it('should GET balance', async () => { + // Stub the network call. + sandbox.stub(axios, 'post').resolves({ data: mockData.balance }) + const addr = 'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n' + const result = await bchjs.PsfSlpIndexer.balance(addr) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + assert.property(result, 'balance') + + assert.property(result.balance, 'utxos') + assert.property(result.balance, 'txs') + assert.property(result.balance, 'balances') + assert.isArray(result.balance.utxos) + assert.isArray(result.balance.txs) + assert.isArray(result.balance.balances) + }) + it('should throw an error for improper input', async () => { + try { + const addr = 12345 + + await bchjs.PsfSlpIndexer.balance(addr) + assert.equal(true, false, 'Unexpected result!') + } catch (err) { + // console.log(`err: `, err) + assert.include(err.message, 'Input address must be a string.') + } + }) + it('should handle axios error', async () => { + try { + // Stub the network call. + sandbox.stub(axios, 'post').throws(new Error('test error')) + + const addr = 'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n' + + await bchjs.PsfSlpIndexer.balance(addr) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + assert.equal(true, false, 'Unexpected result!') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + it('should handle request error', async () => { + try { + // Stub the network call. + const testErr = new Error() + testErr.response = { data: { status: 422 } } + sandbox.stub(axios, 'post').throws(testErr) + + const addr = 'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n' + + await bchjs.PsfSlpIndexer.balance(addr) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + assert.equal(true, false, 'Unexpected result!') + } catch (err) { + assert.equal(err.status, 422) + } + }) + }) + + describe('#tokenStats', () => { + it('should GET token stats', async () => { + // Stub the network call. + sandbox.stub(axios, 'post').resolves({ data: mockData.tokenStats }) + + const tokenId = + 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2' + const result = await bchjs.PsfSlpIndexer.tokenStats(tokenId) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + assert.property(result, 'tokenData') + assert.property(result.tokenData, 'type') + assert.property(result.tokenData, 'ticker') + assert.property(result.tokenData, 'name') + assert.property(result.tokenData, 'tokenId') + assert.property(result.tokenData, 'documentUri') + assert.property(result.tokenData, 'documentHash') + assert.property(result.tokenData, 'decimals') + assert.property(result.tokenData, 'mintBatonIsActive') + assert.property(result.tokenData, 'tokensInCirculationBN') + assert.property(result.tokenData, 'tokensInCirculationStr') + assert.property(result.tokenData, 'blockCreated') + assert.property(result.tokenData, 'totalBurned') + assert.property(result.tokenData, 'totalMinted') + assert.property(result.tokenData, 'txs') + }) + it('should throw an error for improper input', async () => { + try { + const tokenId = 12345 + + await bchjs.PsfSlpIndexer.tokenStats(tokenId) + assert.equal(true, false, 'Unexpected result!') + } catch (err) { + // console.log(`err: `, err) + assert.include(err.message, 'Input tokenId must be a string.') + } + }) + it('should handle axios error', async () => { + try { + // Stub the network call. + sandbox.stub(axios, 'post').throws(new Error('test error')) + + const tokenId = + 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2' + await bchjs.PsfSlpIndexer.tokenStats(tokenId) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + assert.equal(true, false, 'Unexpected result!') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + it('should handle request error', async () => { + try { + // Stub the network call. + const testErr = new Error() + testErr.response = { data: { status: 422 } } + sandbox.stub(axios, 'post').throws(testErr) + + const tokenId = + 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2' + await bchjs.PsfSlpIndexer.tokenStats(tokenId) + assert.equal(true, false, 'Unexpected result!') + } catch (err) { + assert.equal(err.status, 422) + } + }) + }) + + describe('#tx', () => { + it('should GET transaction data', async () => { + // Stub the network call. + sandbox.stub(axios, 'post').resolves({ data: mockData.txData }) + + const txid = + 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2' + const result = await bchjs.PsfSlpIndexer.tx(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + assert.property(result, 'txData') + assert.property(result.txData, 'txid') + assert.property(result.txData, 'hash') + assert.property(result.txData, 'version') + assert.property(result.txData, 'size') + assert.property(result.txData, 'locktime') + assert.property(result.txData, 'vin') + assert.property(result.txData, 'vout') + assert.property(result.txData, 'hex') + assert.property(result.txData, 'blockhash') + assert.property(result.txData, 'confirmations') + assert.property(result.txData, 'time') + assert.property(result.txData, 'blocktime') + assert.property(result.txData, 'blockheight') + assert.property(result.txData, 'isSlpTx') + assert.property(result.txData, 'tokenTxType') + assert.property(result.txData, 'tokenId') + assert.property(result.txData, 'tokenType') + assert.property(result.txData, 'tokenTicker') + assert.property(result.txData, 'tokenName') + assert.property(result.txData, 'tokenDecimals') + assert.property(result.txData, 'tokenUri') + assert.property(result.txData, 'tokenDocHash') + assert.property(result.txData, 'isValidSlp') + }) + it('should throw an error for improper input', async () => { + try { + const txid = 12345 + + await bchjs.PsfSlpIndexer.tx(txid) + assert.equal(true, false, 'Unexpected result!') + } catch (err) { + // console.log(`err: `, err) + assert.include(err.message, 'Input txid must be a string.') + } + }) + it('should handle axios error', async () => { + try { + // Stub the network call. + sandbox.stub(axios, 'post').throws(new Error('test error')) + + const txid = + 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2' + await bchjs.PsfSlpIndexer.tx(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + assert.equal(true, false, 'Unexpected result!') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + it('should handle request error', async () => { + try { + // Stub the network call. + const testErr = new Error() + testErr.response = { data: { status: 422 } } + sandbox.stub(axios, 'post').throws(testErr) + + const txid = + 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2' + await bchjs.PsfSlpIndexer.tx(txid) + assert.equal(true, false, 'Unexpected result!') + } catch (err) { + assert.equal(err.status, 422) + } + }) + }) +})