diff --git a/src/controllers/json-rpc/bch/index.js b/src/controllers/json-rpc/bch/index.js index 66c4cd7..16d7dd4 100644 --- a/src/controllers/json-rpc/bch/index.js +++ b/src/controllers/json-rpc/bch/index.js @@ -429,7 +429,8 @@ class BCHRPC { * @apiPermission public * @apiName Transaction * @apiGroup JSON BCH - * @apiDescription Get data about a specific transaction. + * @apiDescription Get expanded transaction data for an array of transaction + * IDs. Each call is limited to 20 TXIDs or less. * Given a transaction the endpoint will return an object with the * following properties * @@ -455,13 +456,13 @@ class BCHRPC { * - status: - HTTP Status Code * * @apiExample Example usage: - * {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "transaction", "txid": "01517ff1587fa5ffe6f5eb91c99cf3f2d22330cd7ee847e928ce90ca95bf781b"}} + * {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "transaction", "txids": ["01517ff1587fa5ffe6f5eb91c99cf3f2d22330cd7ee847e928ce90ca95bf781b"]}} * * @apiSuccessExample {json} Success-Response: * { * "jsonrpc":"2.0", * "id":"555", - * "result":{ + * "txData": [{ * "method":"bch", * "reciever":"QmU86vLVbUY1UhziKB6rak7GPKRA2QHWvzNm2AjEvXNsT6", * "value":{ @@ -516,20 +517,11 @@ class BCHRPC { * "status":200 * } * } - * } + * }] */ async transaction (rpcData) { try { - // console.log('transaction rpcData: ', rpcData) - - const txid = rpcData.payload.params.txid - - const data = await this.bchjs.Transaction.get(txid.toString()) - console.log(`data: ${JSON.stringify(data, null, 2)}`) - - const retObj = data - retObj.status = 200 - + const retObj = await this.useCases.bch.getTxData(rpcData) return retObj } catch (err) { console.error('Error in JSON RPC BCH transaction()') diff --git a/src/use-cases/bch/index.js b/src/use-cases/bch/index.js index f9da57d..2c14bc2 100644 --- a/src/use-cases/bch/index.js +++ b/src/use-cases/bch/index.js @@ -19,9 +19,9 @@ class BCHUseCases { // Get transaction history for an address, sorted by block height. async getTransactions (rpcData) { try { - console.log( - `getTransactions rpcData: ${JSON.stringify(rpcData, null, 2)}` - ) + // console.log( + // `getTransactions rpcData: ${JSON.stringify(rpcData, null, 2)}` + // ) // Get the list of addresses. const addr = rpcData.payload.params.address @@ -34,11 +34,11 @@ class BCHUseCases { // Default to page 1 let page = rpcData.payload.params.page if (!page) page = 0 - console.log('page: ', page) + // console.log('page: ', page) // Get the transaction history for the list of addresses. const data = await this.bchjs.Electrumx.transactions([addr]) - console.log(`data: ${JSON.stringify(data, null, 2)}`) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) if (!data.success) { throw new Error('Could not query Fulcrum indexer.') @@ -50,13 +50,13 @@ class BCHUseCases { data.transactions[0].transactions, sortOrder ) - console.log(`txsArr: ${JSON.stringify(txsArr, null, 2)}`) + // console.log(`txsArr: ${JSON.stringify(txsArr, null, 2)}`) // Paginate the results const pagedResults = this.bchjs.Util.chunk100(txsArr) - console.log( - `pagedResults[page]: ${JSON.stringify(pagedResults[page], null, 2)}` - ) + // console.log( + // `pagedResults[page]: ${JSON.stringify(pagedResults[page], null, 2)}` + // ) const retObj = { address: addr, @@ -80,6 +80,62 @@ class BCHUseCases { } } } + + // Get transaction data for an array of TXIDs. + async getTxData (rpcData) { + try { + // console.log('transaction rpcData: ', rpcData) + + const txids = rpcData.payload.params.txids + // console.log(`txids: ${JSON.stringify(txids, null, 2)}`) + + if (!Array.isArray(txids)) { + return { + success: false, + status: 422, + message: 'Input txids must be an array of transaction IDs.', + endpoint: 'transaction' + } + } + + if (txids.length > 20) { + return { + success: false, + status: 422, + message: 'Array input must be 20 elements or less.', + endpoint: 'transaction' + } + } + + const txData = [] + for (let i = 0; i < txids.length; i++) { + const thisTxid = txids[i] + + const data = await this.bchjs.Transaction.get(thisTxid.toString()) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + txData.push(data) + } + + const retObj = { + status: 200, + txData + } + + return retObj + } catch (err) { + console.error('Error in JSON RPC BCH transaction()') + // throw err + + // Return an error response + return { + success: false, + status: 422, + message: err.message, + endpoint: 'transaction' + } + } + } } module.exports = BCHUseCases diff --git a/test/integration/use-cases/bch/index-bch.use-cases.integration.js b/test/integration/use-cases/bch/index-bch.use-cases.integration.js index 4970cbd..5b62f64 100644 --- a/test/integration/use-cases/bch/index-bch.use-cases.integration.js +++ b/test/integration/use-cases/bch/index-bch.use-cases.integration.js @@ -82,4 +82,49 @@ describe('#BCH', () => { assert.isAbove(result.txs[1].height, result.txs[0].height) }) }) + + describe('#getTxData', () => { + it('should get data for a single non-SLP tx', async () => { + const txids = [ + '11384d7e5a8af93806591debe5bbe2d7826aeea987b874dfbe372dfdcc0ee54f' + ] + + const rpcData = { + payload: { + params: { + txids + } + } + } + + const result = await uut.getTxData(rpcData) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result.status, 200) + assert.isArray(result.txData) + assert.equal(result.txData.length, 1) + }) + + it('should get data on multiple non-SLP txs', async () => { + const txids = [ + '11384d7e5a8af93806591debe5bbe2d7826aeea987b874dfbe372dfdcc0ee54f', + 'f9b54fd8d27b0237923437ed4df8d45557f52b3ad4d8d03d4104e925c84ab4ca' + ] + + const rpcData = { + payload: { + params: { + txids + } + } + } + + const result = await uut.getTxData(rpcData) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.equal(result.status, 200) + assert.isArray(result.txData) + assert.equal(result.txData.length, 2) + }) + }) }) diff --git a/test/unit/controllers/json-rpc/bch.json-rpc.controller.unit.js b/test/unit/controllers/json-rpc/bch.json-rpc.controller.unit.js index 3a1b96f..ae0136d 100644 --- a/test/unit/controllers/json-rpc/bch.json-rpc.controller.unit.js +++ b/test/unit/controllers/json-rpc/bch.json-rpc.controller.unit.js @@ -155,6 +155,7 @@ describe('#BCHRPC', () => { assert.equal(result, true) }) + it('should route to the pubkey method', async () => { // Mock dependencies sandbox.stub(uut, 'pubKey').resolves(true) @@ -387,10 +388,7 @@ describe('#BCHRPC', () => { }) describe('#transaction', () => { - it('should return data from bchjs', async () => { - // Mock dependencies - sandbox.stub(uut.bchjs.Transaction, 'get').resolves({ success: true }) - + it('should route data to the use-case library', async () => { // Generate the parsed data that the main router would pass to this // endpoint. const id = uid() @@ -411,7 +409,7 @@ describe('#BCHRPC', () => { it('should return an error for invalid input', async () => { // Force an error sandbox - .stub(uut.bchjs.Transaction, 'get') + .stub(uut.useCases.bch, 'getTxData') .rejects(new Error('Invalid data')) // Generate the parsed data that the main router would pass to this @@ -465,6 +463,7 @@ describe('#BCHRPC', () => { assert.property(pubKey, 'publicKey') assert.equal(pubKey.publicKey, mock.publicKey) }) + it('should throw an error if public key is not found', async () => { // Force an error sandbox diff --git a/test/unit/mocks/use-cases/bch/bch.use-cases.mocks.js b/test/unit/mocks/use-cases/bch/bch.use-cases.mocks.js index debae70..f3cc71d 100644 --- a/test/unit/mocks/use-cases/bch/bch.use-cases.mocks.js +++ b/test/unit/mocks/use-cases/bch/bch.use-cases.mocks.js @@ -23,6 +23,72 @@ const fulcrumOut01 = { ] } -module.exports = { - fulcrumOut01 +const txData01 = { + txData: { + txid: '11384d7e5a8af93806591debe5bbe2d7826aeea987b874dfbe372dfdcc0ee54f', + hash: '11384d7e5a8af93806591debe5bbe2d7826aeea987b874dfbe372dfdcc0ee54f', + version: 1, + size: 371, + locktime: 0, + vin: [ + { + txid: 'c4f3e3d6f146f1d54bd5c22cad6b189637b44a655b057cd0303d85558c033594', + vout: 310, + scriptSig: { + asm: '304402200d40ad599c7e33c7f5245080c0ea35c837c0d142b82eb6824fceaf049c2a355902200590e35951111f6c603c0fb4e513cf0e47734d7bc8b45f105585bea0f2b923de[ALL|FORKID] 0352ede8ab8f2ae49b3921c385e59f700ae1e6f73cbb37f61cd1244e70c99bf496', + hex: '47304402200d40ad599c7e33c7f5245080c0ea35c837c0d142b82eb6824fceaf049c2a355902200590e35951111f6c603c0fb4e513cf0e47734d7bc8b45f105585bea0f2b923de41210352ede8ab8f2ae49b3921c385e59f700ae1e6f73cbb37f61cd1244e70c99bf496' + }, + sequence: 4294967295, + address: 'bitcoincash:qzgkqac0tlrfsajg2f7e4e8nyh3glmn4s523tysd6r', + value: 0.08809511 + }, + { + txid: 'c32265f89b2d5d1d5ffd5212b3245105d7d0ba726306fbeb93cd866ce0fd2d61', + vout: 1, + scriptSig: { + asm: '3045022100be9cc5e982110b0f563460baa19a42f34ab775708ad3461a6cf065db9dffb1b7022031ffb2721828be4aa1f4967a367e8af2848cdfce41c68565d0a2a2a70117ea5d[ALL|FORKID] 02d2eabc002e25fc08622a71d9ef96c17b21176f438d67bc537ac49a2e0e594372', + hex: '483045022100be9cc5e982110b0f563460baa19a42f34ab775708ad3461a6cf065db9dffb1b7022031ffb2721828be4aa1f4967a367e8af2848cdfce41c68565d0a2a2a70117ea5d412102d2eabc002e25fc08622a71d9ef96c17b21176f438d67bc537ac49a2e0e594372' + }, + sequence: 4294967295, + address: 'bitcoincash:qr92r3ms2696ghpmk9467px90jrzzdesy5fmqc05p3', + value: 0.00011805 + } + ], + vout: [ + { + value: 0.0814, + n: 0, + scriptPubKey: { + asm: 'OP_HASH160 84459c796307fe2c0316ce7a8bffc511c27bcd55 OP_EQUAL', + hex: 'a91484459c796307fe2c0316ce7a8bffc511c27bcd5587', + reqSigs: 1, + type: 'scripthash', + addresses: ['bitcoincash:pzzyt8revvrlutqrzm884zllc5guy77d2545uyqe8h'] + } + }, + { + value: 0.00680944, + n: 1, + scriptPubKey: { + asm: 'OP_DUP OP_HASH160 aab2f091ea97e02c58c4e27fb77afb99538ea39e OP_EQUALVERIFY OP_CHECKSIG', + hex: '76a914aab2f091ea97e02c58c4e27fb77afb99538ea39e88ac', + reqSigs: 1, + type: 'pubkeyhash', + addresses: ['bitcoincash:qz4t9uy3a2t7qtzccn38ldm6lwv48r4rnc9mxdtvt9'] + } + } + ], + hex: '01000000029435038c55853d30d07c055b654ab43796186bad2cc2d54bd5f146f1d6e3f3c4360100006a47304402200d40ad599c7e33c7f5245080c0ea35c837c0d142b82eb6824fceaf049c2a355902200590e35951111f6c603c0fb4e513cf0e47734d7bc8b45f105585bea0f2b923de41210352ede8ab8f2ae49b3921c385e59f700ae1e6f73cbb37f61cd1244e70c99bf496ffffffff612dfde06c86cd93ebfb066372bad0d7055124b31252fd5f1d5d2d9bf86522c3010000006b483045022100be9cc5e982110b0f563460baa19a42f34ab775708ad3461a6cf065db9dffb1b7022031ffb2721828be4aa1f4967a367e8af2848cdfce41c68565d0a2a2a70117ea5d412102d2eabc002e25fc08622a71d9ef96c17b21176f438d67bc537ac49a2e0e594372ffffffff02e0347c000000000017a91484459c796307fe2c0316ce7a8bffc511c27bcd5587f0630a00000000001976a914aab2f091ea97e02c58c4e27fb77afb99538ea39e88ac00000000', + blockhash: + '000000000000000001e8e99be82172d5fe913e21f5a78e4e3bdce2c7dc6315ce', + confirmations: 94517, + time: 1586584650, + blocktime: 1586584650, + isValidSlp: false + } +} + +module.exports = { + fulcrumOut01, + txData01 } diff --git a/test/unit/mocks/use-cases/index.js b/test/unit/mocks/use-cases/index.js index 9bec557..b23e72f 100644 --- a/test/unit/mocks/use-cases/index.js +++ b/test/unit/mocks/use-cases/index.js @@ -38,6 +38,13 @@ class BCH { status: 200 } } + + async getTxData(rpcData) { + return { + success: true, + status: 200 + } + } } class UseCasesMock { diff --git a/test/unit/use-cases/bch/index-bch.use-case.unit.js b/test/unit/use-cases/bch/index-bch.use-case.unit.js index 64cea40..ed288db 100644 --- a/test/unit/use-cases/bch/index-bch.use-case.unit.js +++ b/test/unit/use-cases/bch/index-bch.use-case.unit.js @@ -122,4 +122,94 @@ describe('#bch-use-case', () => { assert.equal(result.endpoint, 'transactions') }) }) + + describe('#getTxData', () => { + it('should get tx data', async () => { + // Mock dependencies + sandbox.stub(uut.bchjs.Transaction, 'get').resolves(mockData.txData01) + + const rpcData = { + payload: { + params: { + txids: [ + '11384d7e5a8af93806591debe5bbe2d7826aeea987b874dfbe372dfdcc0ee54f' + ] + } + } + } + + const result = await uut.getTxData(rpcData) + // console.log('result: ', result) + + assert.equal(result.status, 200) + assert.isArray(result.txData) + }) + + it('should return error if txids is not an array', async () => { + const rpcData = { + payload: { + params: { + txids: 1234 + } + } + } + + const result = await uut.getTxData(rpcData) + // console.log('result: ', result) + + assert.equal(result.success, false) + assert.equal(result.status, 422) + assert.equal(result.endpoint, 'transaction') + assert.equal( + result.message, + 'Input txids must be an array of transaction IDs.' + ) + }) + + it('should return error if txids array has more than 20 elements', async () => { + const rpcData = { + payload: { + params: { + txids: [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25 + ] + } + } + } + + const result = await uut.getTxData(rpcData) + // console.log('result: ', result) + + assert.equal(result.success, false) + assert.equal(result.status, 422) + assert.equal(result.endpoint, 'transaction') + assert.equal(result.message, 'Array input must be 20 elements or less.') + }) + + it('should handle errors', async () => { + // Force an error + sandbox + .stub(uut.bchjs.Transaction, 'get') + .rejects(new Error('test error')) + + const rpcData = { + payload: { + params: { + txids: [ + '11384d7e5a8af93806591debe5bbe2d7826aeea987b874dfbe372dfdcc0ee54f' + ] + } + } + } + + const result = await uut.getTxData(rpcData) + // console.log('result: ', result) + + assert.equal(result.success, false) + assert.equal(result.status, 422) + assert.equal(result.endpoint, 'transaction') + assert.equal(result.message, 'test error') + }) + }) })