From 0a897f19bb26c3ff4000f868a817fd8f53660cb2 Mon Sep 17 00:00:00 2001 From: Stoyan Zhekov Date: Fri, 11 Sep 2020 21:08:13 +0900 Subject: [PATCH] code, unit and integration tests and api-doc added --- src/electrumx.js | 98 +++++++++++++++++++++++++++ test/integration/electrumx.js | 64 +++++++++++++++++ test/integration/testnet/electrumx.js | 64 +++++++++++++++++ test/unit/electrumx.js | 72 ++++++++++++++++++++ test/unit/fixtures/electrumx-mock.js | 91 ++++++++++++++++++++++++- 5 files changed, 388 insertions(+), 1 deletion(-) diff --git a/src/electrumx.js b/src/electrumx.js index 70cc4ae..0bccf05 100644 --- a/src/electrumx.js +++ b/src/electrumx.js @@ -445,6 +445,104 @@ class ElectrumX { else throw error } } + + /** + * @api Electrumx.txData() txData() + * @apiName ElectrumX txData + * @apiGroup ElectrumX + * @apiDescription Returns an object with transaction details of the TXID + * + * @apiExample Example usage: + * (async () => { + * try { + * let result = await bchjs.Electrumx.txData('4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251') + * console.log(result); + * } catch(error) { + * console.error(error) + * } + * })() + * + * result = { + * "success": true, + * "details": { + * "blockhash": "0000000000000000002aaf94953da3b487317508ebd1003a1d75d6d6ec2e75cc", + * "blocktime": 1578327094, + * "confirmations": 31861, + * "hash": "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251", + * ... + * "vin": [ + * { + * "scriptSig": { + * ... + * "vout": [ + * { + * "n": 0, + * "scriptPubKey": { + * "addresses": [ + * "bitcoincash: pqvfecpwxvj53ayqfwkxtjaxsgpvnklcyg8xewk9hl" + * ], + * } + * ... + * } + * + * (async () => { + * try { + * let result = await bchjs.Electrumx.txData(['4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251', '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251']) + * console.log(result); + * } catch(error) { + * console.error(error) + * } + * })() + * + * result = { + * "transactions": [ + * { + * "txid": "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251", + * "details": { + * "blockhash": "0000000000000000002aaf94953da3b487317508ebd1003a1d75d6d6ec2e75cc", + * "blocktime": 1578327094, + * "confirmations": 31861, + * "hash": "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251", + * ... + * } + * }, + * { + * "txid": "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251", + * "details": { + * "blockhash": "0000000000000000002aaf94953da3b487317508ebd1003a1d75d6d6ec2e75cc", + * "blocktime": 1578327094, + * ... + * } + * ] + * } + */ + async txData(txid) { + try { + // Handle single transaction. + if (typeof txid === "string") { + const response = await axios.get( + `${this.restURL}electrumx/tx/data/${txid}`, + _this.axiosOptions + ) + return response.data + } else if (Array.isArray(txid)) { + const response = await axios.post( + `${this.restURL}electrumx/tx/data`, + { + txids: txid + }, + _this.axiosOptions + ) + + return response.data + } + + throw new Error(`Input txId 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 = ElectrumX diff --git a/test/integration/electrumx.js b/test/integration/electrumx.js index 06ba186..02f07eb 100644 --- a/test/integration/electrumx.js +++ b/test/integration/electrumx.js @@ -255,6 +255,7 @@ describe(`#ElectrumX`, () => { assert.isArray(result.headers) assert.equal(result.headers.length, 2) }) + it(`should GET block headers for given height with default count = 1`, async () => { const height = 42 @@ -267,4 +268,67 @@ describe(`#ElectrumX`, () => { assert.equal(result.headers.length, 1) }) }) + + describe(`#txData`, () => { + it(`should GET details for a single transaction`, async () => { + const txid = "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251" + + const result = await bchjs.Electrumx.txData(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, "success") + assert.equal(result.success, true) + + assert.property(result, "details") + assert.isObject(result.details) + + assert.property(result.details, "blockhash") + assert.property(result.details, "hash") + assert.property(result.details, "hex") + assert.property(result.details, "vin") + assert.property(result.details, "vout") + assert.equal(result.details.hash, txid) + }) + + it(`should POST details for an array of transactions`, async () => { + const txids = [ + "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251", + "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251" + ] + + const result = await bchjs.Electrumx.txData(txids) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, "success") + assert.equal(result.success, true) + + assert.property(result, "transactions") + assert.isArray(result.transactions) + + assert.property(result.transactions[0], "txid") + assert.property(result.transactions[0], "details") + + assert.property(result.transactions[0].details, "blockhash") + assert.property(result.transactions[0].details, "hash") + assert.property(result.transactions[0].details, "hex") + assert.property(result.transactions[0].details, "vin") + assert.property(result.transactions[0].details, "vout") + }) + + it(`should throw error on array size rate limit`, async () => { + try { + const txids = [] + for (let i = 0; i < 25; i++) + txids.push("4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251") + + await bchjs.Electrumx.txData(txids) + + console.log(`result: ${util.inspect(result)}`) + assert.equal(true, false, "Unexpected result!") + } catch (err) { + assert.hasAnyKeys(err, ["error"]) + assert.include(err.error, "Array too large") + } + }) + }) }) diff --git a/test/integration/testnet/electrumx.js b/test/integration/testnet/electrumx.js index 5faf52d..491ff38 100644 --- a/test/integration/testnet/electrumx.js +++ b/test/integration/testnet/electrumx.js @@ -259,6 +259,7 @@ describe(`#ElectrumX`, () => { assert.isArray(result.headers) assert.equal(result.headers.length, 2) }) + it(`should GET block headers for given height with default count = 1`, async () => { const height = 42 @@ -271,4 +272,67 @@ describe(`#ElectrumX`, () => { assert.equal(result.headers.length, 1) }) }) + + describe(`#txData`, () => { + it(`should GET details for a single transaction`, async () => { + const txid = "76d2ee0ebd8b978742f6478ff9a12f478c34e4cee0a2b919059101d961bd01ee" + + const result = await bchjs.Electrumx.txData(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, "success") + assert.equal(result.success, true) + + assert.property(result, "details") + assert.isObject(result.details) + + assert.property(result.details, "blockhash") + assert.property(result.details, "hash") + assert.property(result.details, "hex") + assert.property(result.details, "vin") + assert.property(result.details, "vout") + assert.equal(result.details.hash, txid) + }) + + it(`should POST details for an array of transactions`, async () => { + const txids = [ + "76d2ee0ebd8b978742f6478ff9a12f478c34e4cee0a2b919059101d961bd01ee", + "76d2ee0ebd8b978742f6478ff9a12f478c34e4cee0a2b919059101d961bd01ee" + ] + + const result = await bchjs.Electrumx.txData(txids) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, "success") + assert.equal(result.success, true) + + assert.property(result, "transactions") + assert.isArray(result.transactions) + + assert.property(result.transactions[0], "txid") + assert.property(result.transactions[0], "details") + + assert.property(result.transactions[0].details, "blockhash") + assert.property(result.transactions[0].details, "hash") + assert.property(result.transactions[0].details, "hex") + assert.property(result.transactions[0].details, "vin") + assert.property(result.transactions[0].details, "vout") + }) + + it(`should throw error on array size rate limit`, async () => { + try { + const txids = [] + for (let i = 0; i < 25; i++) + txids.push("76d2ee0ebd8b978742f6478ff9a12f478c34e4cee0a2b919059101d961bd01ee") + + await bchjs.Electrumx.txData(txids) + + console.log(`result: ${util.inspect(result)}`) + assert.equal(true, false, "Unexpected result!") + } catch (err) { + assert.hasAnyKeys(err, ["error"]) + assert.include(err.error, "Array too large") + } + }) + }) }) diff --git a/test/unit/electrumx.js b/test/unit/electrumx.js index 49fa177..67d1aad 100644 --- a/test/unit/electrumx.js +++ b/test/unit/electrumx.js @@ -272,6 +272,7 @@ describe(`#ElectrumX`, () => { assert.include(err.error, `height must be a positive number`) } }) + it(`should throw an error for improper count input`, async () => { try { const height = 42 @@ -284,6 +285,7 @@ describe(`#ElectrumX`, () => { assert.include(err.error, `count must be a positive number`) } }) + it(`should GET block headers for a given height`, async () => { // Stub the network call. sandbox.stub(axios, "get").resolves({ data: mockData.blockHeaders }) @@ -296,4 +298,74 @@ describe(`#ElectrumX`, () => { assert.equal(result.length, 2) }) }) + + describe(`#txData`, () => { + it(`should throw an error for improper input`, async () => { + try { + const txid = 12345 + + await bchjs.Electrumx.txData(txid) + assert.equal(true, false, "Unexpected result!") + } catch (err) { + // console.log(`err: `, err) + assert.include( + err.message, + `Input txId must be a string or array of strings` + ) + } + }) + + it(`should GET details data for a single transaction`, async () => { + // Stub the network call. + sandbox.stub(axios, "get").resolves({ data: mockData.details }) + + const txid = "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251" + + const result = await bchjs.Electrumx.txData(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, "success") + assert.equal(result.success, true) + + assert.property(result, "details") + assert.isObject(result.details) + + assert.property(result.details, "blockhash") + assert.property(result.details, "hash") + assert.property(result.details, "hex") + assert.property(result.details, "vin") + assert.property(result.details, "vout") + assert.equal(result.details.hash, txid) + }) + + it(`should POST details for an array of transactions`, async () => { + // Stub the network call. + sandbox.stub(axios, "post").resolves({ data: mockData.detailsArray }) + + const txids = [ + "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251", + "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251" + ] + + const result = await bchjs.Electrumx.txData(txids) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.property(result, "success") + assert.equal(result.success, true) + + assert.property(result, "transactions") + assert.isArray(result.transactions) + + assert.property(result.transactions[0], "txid") + assert.property(result.transactions[0], "details") + + assert.property(result.transactions[0].details, "blockhash") + assert.property(result.transactions[0].details, "hash") + assert.property(result.transactions[0].details, "hex") + assert.property(result.transactions[0].details, "vin") + assert.property(result.transactions[0].details, "vout") + + assert.equal(result.transactions.length, 2, "2 outputs for 2 inputs") + }) + }) }) diff --git a/test/unit/fixtures/electrumx-mock.js b/test/unit/fixtures/electrumx-mock.js index 16cbc00..71774fc 100644 --- a/test/unit/fixtures/electrumx-mock.js +++ b/test/unit/fixtures/electrumx-mock.js @@ -163,6 +163,93 @@ const blockHeaders = [ '01000000f528fac1bcb685d0cd6c792320af0300a5ce15d687c7149548904e31000000004e8985a786d864f21e9cbb7cbdf4bc9265fe681b7a0893ac55a8e919ce035c2f85de6849ffff001d385ccb7c' ] +const txDetails = { + blockhash: '0000000000000000002aaf94953da3b487317508ebd1003a1d75d6d6ec2e75cc', + blocktime: 1578327094, + confirmations: 31861, + hash: '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251', + hex: '020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000', + locktime: 0, + size: 392, + time: 1578327094, + txid: '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251', + version: 2, + vin: [ + { + scriptSig: { + asm: 'dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e[ALL|FORKID] 020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309', + hex: '41dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309' + }, + sequence: 4294967295, + txid: '6796672c8f0342770e3d4ac2a3b0e4494daeb7af7997f3518a0c8402f43ed165', + vout: 1 + }, + { + scriptSig: { + asm: '347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f77[ALL|FORKID] 028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954', + hex: '41347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954' + }, + sequence: 4294967295, + txid: '6796672c8f0342770e3d4ac2a3b0e4494daeb7af7997f3518a0c8402f43ed165', + vout: 0 + } + ], + vout: [ + { + n: 0, + scriptPubKey: { + addresses: ['bitcoincash: pqvfecpwxvj53ayqfwkxtjaxsgpvnklcyg8xewk9hl'], + asm: 'OP_HASH160 189ce02e332548f4804bac65cba68202c9dbf822 OP_EQUAL', + hex: 'a914189ce02e332548f4804bac65cba68202c9dbf82287', + reqSigs: 1, + type: 'scripthash' + }, + value: 0.0562057 + }, + { + n: 1, + scriptPubKey: { + addresses: ['bitcoincash: qq59hv6s3qdjrtyfwfxxldkuj9xsjmx48vrz882knz'], + asm: 'OP_DUP OP_HASH160 285bb350881b21ac89724c6fb6dc914d096cd53b OP_EQUALVERIFY OP_CHECKSIG', + hex: '76a914285bb350881b21ac89724c6fb6dc914d096cd53b88ac', + reqSigs: 1, + type: 'pubkeyhash' + }, + value: 0.00589197 + }, + { + n: 2, + scriptPubKey: { + addresses: ['bitcoincash: qpzlruwy4xu5rxjs3z37nsj29y7h59gwvsu4ddp0u4'], + asm: 'OP_DUP OP_HASH160 45f1f1c4a9b9419a5088a3e9c24a293d7a150e64 OP_EQUALVERIFY OP_CHECKSIG', + hex: '76a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac', + reqSigs: 1, + type: 'pubkeyhash' + }, + value: 0.03272697 + } + ] +} + +const details = { + success: true, + details: txDetails +} + +const detailsArray = { + success: true, + transactions: [ + { + txid: txDetails.txid, + details: txDetails + }, + { + txid: txDetails.txid, + details: txDetails + } + ] +} + module.exports = { utxo, utxos, @@ -172,5 +259,7 @@ module.exports = { transactions, unconfirmed, unconfirmedArray, - blockHeaders + blockHeaders, + details, + detailsArray }