From d2e847d3fb619cf49a8244c0808e02442e665391 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 23 Mar 2021 17:11:37 -0700 Subject: [PATCH 1/5] feat(transactions): Created transaction library --- package.json | 2 +- src/bch-js.js | 2 + src/raw-transactions.js | 83 --------- src/transaction.js | 172 ++++++++++++++++++ test/integration/chains/bchn/slp.js | 2 +- .../chains/bchn/utxo-integration.js | 2 +- test/integration/rawtransaction.js | 49 +++-- test/integration/transaction-integration.js | 36 ++++ test/unit/raw-tranactions.js | 3 +- test/unit/transaction-unit.js | 3 + 10 files changed, 241 insertions(+), 113 deletions(-) create mode 100644 src/transaction.js create mode 100644 test/integration/transaction-integration.js create mode 100644 test/unit/transaction-unit.js diff --git a/package.json b/package.json index be72138..71d93b5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test:integration:temp:bchn": "export RESTURL=http://157.90.174.219:3000/v4/ && mocha --timeout 30000 test/integration/", "test:temp": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#Encryption' test/integration/", "test:temp2": "mocha --timeout=30000 -g '#_hydrateUtxo' test/unit/", - "test:temp3": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#hydrateUtxos' test/integration/", + "test:temp3": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#transaction' test/integration/", "test:temp4": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#decodeOpReturn' test/integration/", "coverage": "nyc report --reporter=text-lcov | coveralls", "coverage:report": "nyc --reporter=html mocha --timeout 25000 test/unit/", diff --git a/src/bch-js.js b/src/bch-js.js index 9d5faa1..6d3914d 100644 --- a/src/bch-js.js +++ b/src/bch-js.js @@ -31,6 +31,7 @@ const SLP = require('./slp/slp') const IPFS = require('./ipfs') const Encryption = require('./encryption') const Utxo = require('./utxo') +const Transaction = require('./transaction') // Indexers const Ninsight = require('./ninsight') @@ -113,6 +114,7 @@ class BCHJS { this.IPFS = new IPFS() this.Utxo = new Utxo(libConfig) + this.Transaction = new Transaction(libConfig) } } diff --git a/src/raw-transactions.js b/src/raw-transactions.js index 8df8b3e..5815395 100644 --- a/src/raw-transactions.js +++ b/src/raw-transactions.js @@ -1,7 +1,5 @@ const axios = require('axios') -const SlpUtils = require('./slp/utils') - let _this class RawTransactions { @@ -29,9 +27,6 @@ class RawTransactions { // Encapsulate dependencies this.axios = axios - // Dependencies - this.slpUtils = new SlpUtils(config) - _this = this } @@ -387,84 +382,6 @@ class RawTransactions { } } - // Wraps getTxData(), but also appends SLP token information to each input - // and output of the transaction. This is a very API-heavy call. - // Returns false if the txid is not an SLP transaction. - // - // Warning! This is a prototype function and the output can change at any time - // without reflecting a change in the semantic version. DO NOT USE IN PRODUCTION. - async getTxDataSlp (txid) { - try { - if (typeof txid !== 'string') { - throw new Error('Input must be a string or array of strings.') - } - - const txDetails = await this.getTxData(txid) - - // First get the token information for the output. If that fails, then - // this is not an SLP transaction, and this method can return false. - let outTokenData - try { - outTokenData = await this.slpUtils.decodeOpReturn(txid) - // console.log(`outTokenData: ${JSON.stringify(outTokenData, null, 2)}`) - - // Add token information to the tx details object. - txDetails.tokenTxType = outTokenData.txType - txDetails.tokenId = outTokenData.tokenId - - // Add the token quantity to each output. - for (let i = 0; i < outTokenData.amounts.length; i++) { - txDetails.vout[i + 1].tokenQty = outTokenData.amounts[i] - } - - // Loop through each input and retrieve the token data. - for (let i = 0; i < txDetails.vin.length; i++) { - const thisVin = txDetails.vin[i] - - try { - // If decodeOpReturn() throws an error, then this input is not - // from an SLP transaction and can be ignored. - const inTokenData = await this.slpUtils.decodeOpReturn(thisVin.txid) - // console.log( - // `vin[${i}] tokenData: ${JSON.stringify(inTokenData, null, 2)}` - // ) - - // Get the appropriate vout token amount. This may throw an error, - // which means this Vin is not actually a token UTXO, it was just - // associated with a previous token TX. - const tokenQty = inTokenData.amounts[thisVin.vout - 1] - // console.log(`tokenQty: ${JSON.stringify(tokenQty, null, 2)}`) - - if (tokenQty) { - thisVin.tokenQty = tokenQty - // txDetails.vin[i].tokenQty = tokenQty - } else { - thisVin.tokenQty = null - } - } catch (err) { - // console.log('catch 2: ', err) - // If decodeOpReturn() throws an error, then this input is not - // from an SLP transaction and can be ignored. - // thisVin.tokenQty = null - thisVin.tokenQty = null - continue - } - // console.log( - // `2: txDetails.vin[i]: ${JSON.stringify(txDetails.vin[i], null, 2)}` - // ) - } - } catch (err) { - // console.log('catch 1: ', err) - return false - } - - return txDetails - } catch (error) { - if (error.response && error.response.data) throw error.response.data - else throw error - } - } - /** * @api RawTransactions.sendRawTransaction() sendRawTransaction() * @apiName sendRawTransaction diff --git a/src/transaction.js b/src/transaction.js new file mode 100644 index 0000000..2bac153 --- /dev/null +++ b/src/transaction.js @@ -0,0 +1,172 @@ +/* + High-level functions for working with Transactions +*/ + +const RawTransaction = require('./raw-transactions') +const SlpUtils = require('./slp/utils') + +class Transaction { + constructor (config) { + // Encapsulate dependencies + this.slpUtils = new SlpUtils(config) + this.rawTransaction = new RawTransaction(config) + } + + // Get hydrated details about a transaction, including SLP token details if + // it's an SLP transaction. + async get (txid) { + try { + if (typeof txid !== 'string') { + throw new Error('Input must be a string or array of strings.') + } + + const txDetails = await this.rawTransaction.getTxData(txid) + + // Setup default SLP properties. + txDetails.isValidSLPTx = false + + // First get the token information for the output. If that fails, then + // this is not an SLP transaction, and this method can return false. + let outTokenData + try { + outTokenData = await this.slpUtils.decodeOpReturn(txid) + // console.log(`outTokenData: ${JSON.stringify(outTokenData, null, 2)}`) + + // Add token information to the tx details object. + txDetails.tokenTxType = outTokenData.txType + txDetails.tokenId = outTokenData.tokenId + + // Add the token quantity to each output. + for (let i = 0; i < outTokenData.amounts.length; i++) { + txDetails.vout[i + 1].tokenQty = outTokenData.amounts[i] + } + + // Loop through each input and retrieve the token data. + for (let i = 0; i < txDetails.vin.length; i++) { + const thisVin = txDetails.vin[i] + + try { + // If decodeOpReturn() throws an error, then this input is not + // from an SLP transaction and can be ignored. + const inTokenData = await this.slpUtils.decodeOpReturn(thisVin.txid) + // console.log( + // `vin[${i}] tokenData: ${JSON.stringify(inTokenData, null, 2)}` + // ) + + // Get the appropriate vout token amount. This may throw an error, + // which means this Vin is not actually a token UTXO, it was just + // associated with a previous token TX. + const tokenQty = inTokenData.amounts[thisVin.vout - 1] + // console.log(`tokenQty: ${JSON.stringify(tokenQty, null, 2)}`) + + if (tokenQty) { + thisVin.tokenQty = tokenQty + // txDetails.vin[i].tokenQty = tokenQty + } else { + thisVin.tokenQty = null + } + } catch (err) { + // console.log('catch 2: ', err) + // If decodeOpReturn() throws an error, then this input is not + // from an SLP transaction and can be ignored. + // thisVin.tokenQty = null + thisVin.tokenQty = null + continue + } + // console.log( + // `2: txDetails.vin[i]: ${JSON.stringify(txDetails.vin[i], null, 2)}` + // ) + } + } catch (err) { + // If decoding the op_return fails, then it's not an SLP transaction, + // and the non-hyrated TX details can be returned. + return txDetails + } + + return txDetails + } catch (err) { + console.error('Error in transactions.js/get()') + throw err + } + } + + // Wraps getTxData(), but also appends SLP token information to each input + // and output of the transaction. This is a very API-heavy call. + // Returns false if the txid is not an SLP transaction. + // + // Warning! This is a prototype function and the output can change at any time + // without reflecting a change in the semantic version. DO NOT USE IN PRODUCTION. + // async getTxDataSlp (txid) { + // try { + // if (typeof txid !== 'string') { + // throw new Error('Input must be a string or array of strings.') + // } + // + // const txDetails = await this.getTxData(txid) + // + // // First get the token information for the output. If that fails, then + // // this is not an SLP transaction, and this method can return false. + // let outTokenData + // try { + // outTokenData = await this.slpUtils.decodeOpReturn(txid) + // // console.log(`outTokenData: ${JSON.stringify(outTokenData, null, 2)}`) + // + // // Add token information to the tx details object. + // txDetails.tokenTxType = outTokenData.txType + // txDetails.tokenId = outTokenData.tokenId + // + // // Add the token quantity to each output. + // for (let i = 0; i < outTokenData.amounts.length; i++) { + // txDetails.vout[i + 1].tokenQty = outTokenData.amounts[i] + // } + // + // // Loop through each input and retrieve the token data. + // for (let i = 0; i < txDetails.vin.length; i++) { + // const thisVin = txDetails.vin[i] + // + // try { + // // If decodeOpReturn() throws an error, then this input is not + // // from an SLP transaction and can be ignored. + // const inTokenData = await this.slpUtils.decodeOpReturn(thisVin.txid) + // // console.log( + // // `vin[${i}] tokenData: ${JSON.stringify(inTokenData, null, 2)}` + // // ) + // + // // Get the appropriate vout token amount. This may throw an error, + // // which means this Vin is not actually a token UTXO, it was just + // // associated with a previous token TX. + // const tokenQty = inTokenData.amounts[thisVin.vout - 1] + // // console.log(`tokenQty: ${JSON.stringify(tokenQty, null, 2)}`) + // + // if (tokenQty) { + // thisVin.tokenQty = tokenQty + // // txDetails.vin[i].tokenQty = tokenQty + // } else { + // thisVin.tokenQty = null + // } + // } catch (err) { + // // console.log('catch 2: ', err) + // // If decodeOpReturn() throws an error, then this input is not + // // from an SLP transaction and can be ignored. + // // thisVin.tokenQty = null + // thisVin.tokenQty = null + // continue + // } + // // console.log( + // // `2: txDetails.vin[i]: ${JSON.stringify(txDetails.vin[i], null, 2)}` + // // ) + // } + // } catch (err) { + // // console.log('catch 1: ', err) + // return false + // } + // + // return txDetails + // } catch (error) { + // if (error.response && error.response.data) throw error.response.data + // else throw error + // } + // } +} + +module.exports = Transaction diff --git a/test/integration/chains/bchn/slp.js b/test/integration/chains/bchn/slp.js index 6c9760e..c220bc8 100644 --- a/test/integration/chains/bchn/slp.js +++ b/test/integration/chains/bchn/slp.js @@ -25,7 +25,7 @@ describe('#SLP', () => { beforeEach(async () => { // Introduce a delay so that the BVT doesn't trip the rate limits. - if (process.env.IS_USING_FREE_TIER) await sleep(1000) + if (process.env.IS_USING_FREE_TIER) await sleep(2000) bchjs = new BCHJS() }) diff --git a/test/integration/chains/bchn/utxo-integration.js b/test/integration/chains/bchn/utxo-integration.js index 43e5b8a..da931c5 100644 --- a/test/integration/chains/bchn/utxo-integration.js +++ b/test/integration/chains/bchn/utxo-integration.js @@ -11,7 +11,7 @@ describe('#UTXO', () => { beforeEach(async () => { // sandbox = sinon.createSandbox() - if (process.env.IS_USING_FREE_TIER) await sleep(1000) + if (process.env.IS_USING_FREE_TIER) await sleep(2000) }) describe('#get', () => { diff --git a/test/integration/rawtransaction.js b/test/integration/rawtransaction.js index 86c18f5..9bda860 100644 --- a/test/integration/rawtransaction.js +++ b/test/integration/rawtransaction.js @@ -1,6 +1,5 @@ /* - Integration tests for the bchjs. Only covers calls made to - rest.bitcoin.com. + Integration tests for the bchjs. TODO */ @@ -277,29 +276,29 @@ describe('#rawtransaction', () => { }) }) - describe('#getTxDataSlp', () => { - it('should return tx data with SLP information', async () => { - const txid = 'b438855cfcab64516b44097d7212df9cdb99226c8d7c7ab504d35fcfd834cb5b' - - const result = await bchjs.RawTransactions.getTxDataSlp(txid) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.property(result.vin[0], 'address') - assert.property(result.vin[0], 'tokenQty') - - assert.equal(result.vin[0].tokenQty, null) - assert.equal(result.vin[1].tokenQty, '100000000000') - }) - - it('should handle non-slp tx', async () => { - const txid = '04a6aca328af2445327015895b8da9766093b1989b52e477559759eb8072fc0a' - - const result = await bchjs.RawTransactions.getTxDataSlp(txid) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.equal(result, false) - }) - }) + // describe('#getTxDataSlp', () => { + // it('should return tx data with SLP information', async () => { + // const txid = 'b438855cfcab64516b44097d7212df9cdb99226c8d7c7ab504d35fcfd834cb5b' + // + // const result = await bchjs.RawTransactions.getTxDataSlp(txid) + // // console.log(`result: ${JSON.stringify(result, null, 2)}`) + // + // assert.property(result.vin[0], 'address') + // assert.property(result.vin[0], 'tokenQty') + // + // assert.equal(result.vin[0].tokenQty, null) + // assert.equal(result.vin[1].tokenQty, '100000000000') + // }) + // + // it('should handle non-slp tx', async () => { + // const txid = '04a6aca328af2445327015895b8da9766093b1989b52e477559759eb8072fc0a' + // + // const result = await bchjs.RawTransactions.getTxDataSlp(txid) + // // console.log(`result: ${JSON.stringify(result, null, 2)}`) + // + // assert.equal(result, false) + // }) + // }) }) function sleep (ms) { diff --git a/test/integration/transaction-integration.js b/test/integration/transaction-integration.js new file mode 100644 index 0000000..ead3e42 --- /dev/null +++ b/test/integration/transaction-integration.js @@ -0,0 +1,36 @@ +/* + Integration tests for the transaction.js library. +*/ + +const assert = require('chai').assert +const BCHJS = require('../../src/bch-js') +const bchjs = new BCHJS() + +describe('#transaction', () => { + beforeEach(async () => { + if (process.env.IS_USING_FREE_TIER) await bchjs.Util.sleep(1000) + }) + + describe('#get', () => { + it('should get details about a non-SLP transaction', async () => { + const txid = + '2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7' + + const result = await bchjs.Transaction.get(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 added properties + assert.property(result.vin[0], 'address') + // TODO: Add value to input + assert.property(result, 'isValidSLPTx') + assert.equal(result.isValidSLPTx, false) + }) + }) +}) diff --git a/test/unit/raw-tranactions.js b/test/unit/raw-tranactions.js index a75c846..f84dc68 100644 --- a/test/unit/raw-tranactions.js +++ b/test/unit/raw-tranactions.js @@ -1,6 +1,5 @@ /* - TODO: - -Create a mocking library of data to compare unit and integration tests. + Unit tests for raw-transactions.js library. */ // Public npm libraries diff --git a/test/unit/transaction-unit.js b/test/unit/transaction-unit.js new file mode 100644 index 0000000..4fc123c --- /dev/null +++ b/test/unit/transaction-unit.js @@ -0,0 +1,3 @@ +/* + Unit tests for the transaction.js library. +*/ From ce951afddbe1358a06486fc0c6de1e463d77649e Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 23 Mar 2021 17:37:00 -0700 Subject: [PATCH 2/5] fix(RawTransactions.getTxData): Adding BCH value to tx inputs --- package.json | 2 +- src/raw-transactions.js | 4 +++- test/integration/rawtransaction.js | 2 ++ test/integration/transaction-integration.js | 10 +++++++++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 71d93b5..d1a258f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test:integration:temp:bchn": "export RESTURL=http://157.90.174.219:3000/v4/ && mocha --timeout 30000 test/integration/", "test:temp": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#Encryption' test/integration/", "test:temp2": "mocha --timeout=30000 -g '#_hydrateUtxo' test/unit/", - "test:temp3": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#transaction' test/integration/", + "test:temp3": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#getTxData' test/integration/", "test:temp4": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#decodeOpReturn' test/integration/", "coverage": "nyc report --reporter=text-lcov | coveralls", "coverage:report": "nyc --reporter=html mocha --timeout 25000 test/unit/", diff --git a/src/raw-transactions.js b/src/raw-transactions.js index 5815395..16bc593 100644 --- a/src/raw-transactions.js +++ b/src/raw-transactions.js @@ -319,7 +319,8 @@ class RawTransactions { retArray.push({ vin: i, - address: voutSender.scriptPubKey.addresses[0] + address: voutSender.scriptPubKey.addresses[0], + value: voutSender.value }) } @@ -373,6 +374,7 @@ class RawTransactions { // Add the input address to the transaction data. for (let i = 0; i < inAddrs.length; i++) { txDetails.vin[i].address = inAddrs[i].address + txDetails.vin[i].value = inAddrs[i].value } return txDetails diff --git a/test/integration/rawtransaction.js b/test/integration/rawtransaction.js index 9bda860..bf88216 100644 --- a/test/integration/rawtransaction.js +++ b/test/integration/rawtransaction.js @@ -262,6 +262,7 @@ describe('#rawtransaction', () => { assert.equal(result.length, 1) assert.property(result[0], 'vin') assert.property(result[0], 'address') + assert.property(result[0], 'value') }) }) @@ -273,6 +274,7 @@ describe('#rawtransaction', () => { // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.property(result.vin[0], 'address') + assert.property(result.vin[0], 'value') }) }) diff --git a/test/integration/transaction-integration.js b/test/integration/transaction-integration.js index ead3e42..1d71738 100644 --- a/test/integration/transaction-integration.js +++ b/test/integration/transaction-integration.js @@ -17,7 +17,7 @@ describe('#transaction', () => { '2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7' const result = await bchjs.Transaction.get(txid) - console.log(`result: ${JSON.stringify(result, null, 2)}`) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) // Assert that there are stanardized properties. assert.property(result, 'txid') @@ -32,5 +32,13 @@ describe('#transaction', () => { 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.get(txid) + console.log(`result: ${JSON.stringify(result, null, 2)}`) + }) }) }) From 40fa6440f402d928836e3d54ce0257536cceeff7 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 23 Mar 2021 18:49:27 -0700 Subject: [PATCH 3/5] feat(Transactions.get()): Added tx hydration method --- package.json | 2 +- src/slp/utils.js | 5 --- src/transaction.js | 42 ++++++++++++++++++--- test/integration/transaction-integration.js | 22 +++++++++-- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index d1a258f..71d93b5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test:integration:temp:bchn": "export RESTURL=http://157.90.174.219:3000/v4/ && mocha --timeout 30000 test/integration/", "test:temp": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#Encryption' test/integration/", "test:temp2": "mocha --timeout=30000 -g '#_hydrateUtxo' test/unit/", - "test:temp3": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#getTxData' test/integration/", + "test:temp3": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#transaction' test/integration/", "test:temp4": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#decodeOpReturn' test/integration/", "coverage": "nyc report --reporter=text-lcov | coveralls", "coverage:report": "nyc --reporter=html mocha --timeout 25000 test/unit/", diff --git a/src/slp/utils.js b/src/slp/utils.js index 109e484..0a9bdbb 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -1097,11 +1097,6 @@ class Utils { * "tokenType": 1 * } */ - - // CT 1/11/20: Refactored to comply with this GitHub Issue: - // https://github.com/Bitcoin-com/slp-sdk/issues/84 - - // CT 5/31/20: Refactored to use slp-parse library. async tokenUtxoDetails (utxos, usrObj = null) { try { // Throw error if input is not an array. diff --git a/src/transaction.js b/src/transaction.js index 2bac153..023b042 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -32,13 +32,38 @@ class Transaction { outTokenData = await this.slpUtils.decodeOpReturn(txid) // console.log(`outTokenData: ${JSON.stringify(outTokenData, null, 2)}`) + // Get Genesis data for this token. + const genesisData = await this.slpUtils.decodeOpReturn( + outTokenData.tokenId + // decodeOpReturnCache + // usrObj // pass user data when making an internal call. + ) + // console.log(`genesisData: ${JSON.stringify(genesisData, null, 2)}`) + // Add token information to the tx details object. txDetails.tokenTxType = outTokenData.txType txDetails.tokenId = outTokenData.tokenId + txDetails.tokenTicker = genesisData.ticker + txDetails.tokenName = genesisData.name + txDetails.tokenDecimals = genesisData.decimals + txDetails.tokenUri = genesisData.documentUri + txDetails.tokenDocHash = genesisData.documentHash // Add the token quantity to each output. for (let i = 0; i < outTokenData.amounts.length; i++) { - txDetails.vout[i + 1].tokenQty = outTokenData.amounts[i] + const rawQty = outTokenData.amounts[i] + const realQty = Number(rawQty) / Math.pow(10, txDetails.tokenDecimals) + + txDetails.vout[i + 1].tokenQty = realQty + } + + // Add tokenQty = null to any outputs that don't have a value. + for (let i = 0; i < txDetails.vout.length; i++) { + const thisVout = txDetails.vout[i] + + if (!thisVout.tokenQty && thisVout.tokenQty !== 0) { + thisVout.tokenQty = null + } } // Loop through each input and retrieve the token data. @@ -60,24 +85,29 @@ class Transaction { // console.log(`tokenQty: ${JSON.stringify(tokenQty, null, 2)}`) if (tokenQty) { - thisVin.tokenQty = tokenQty + const realQty = + Number(tokenQty) / Math.pow(10, txDetails.tokenDecimals) + + thisVin.tokenQty = realQty // txDetails.vin[i].tokenQty = tokenQty } else { thisVin.tokenQty = null } } catch (err) { - // console.log('catch 2: ', err) // If decodeOpReturn() throws an error, then this input is not // from an SLP transaction and can be ignored. // thisVin.tokenQty = null thisVin.tokenQty = null continue } - // console.log( - // `2: txDetails.vin[i]: ${JSON.stringify(txDetails.vin[i], null, 2)}` - // ) } + + // Finally, validate the SLP TX. + // this.slpUtils.waterfallValidateTxid(txid, usrObj) + txDetails.isValidSLPTx = await this.slpUtils.waterfallValidateTxid(txid) } catch (err) { + // console.log('Error: ', err) + // If decoding the op_return fails, then it's not an SLP transaction, // and the non-hyrated TX details can be returned. return txDetails diff --git a/test/integration/transaction-integration.js b/test/integration/transaction-integration.js index 1d71738..d47289e 100644 --- a/test/integration/transaction-integration.js +++ b/test/integration/transaction-integration.js @@ -26,9 +26,9 @@ describe('#transaction', () => { assert.property(result.vout[0], 'value') assert.property(result.vout[0].scriptPubKey, 'addresses') - // Assert that added properties + // Assert that added properties exist. assert.property(result.vin[0], 'address') - // TODO: Add value to input + assert.property(result.vin[0], 'value') assert.property(result, 'isValidSLPTx') assert.equal(result.isValidSLPTx, false) }) @@ -38,7 +38,23 @@ describe('#transaction', () => { '266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1' const result = await bchjs.Transaction.get(txid) - console.log(`result: ${JSON.stringify(result, null, 2)}`) + // 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) }) }) }) From 5d3ea2b5599971ad9ffd1b4f796233275cda5ffb Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 24 Mar 2021 07:24:08 -0700 Subject: [PATCH 4/5] fix(Transaction.get()): Added unit tests --- package.json | 4 +- src/transaction.js | 79 +-------- test/integration/transaction-integration.js | 2 +- test/unit/fixtures/transaction-mock.js | 184 ++++++++++++++++++++ test/unit/transaction-unit.js | 116 ++++++++++++ 5 files changed, 304 insertions(+), 81 deletions(-) create mode 100644 test/unit/fixtures/transaction-mock.js diff --git a/package.json b/package.json index 71d93b5..f8d2e07 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "test:integration:decatur:bchn": "export RESTURL=http://192.168.0.36:3000/v4/ && mocha --timeout 30000 test/integration/", "test:integration:temp:bchn": "export RESTURL=http://157.90.174.219:3000/v4/ && mocha --timeout 30000 test/integration/", "test:temp": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#Encryption' test/integration/", - "test:temp2": "mocha --timeout=30000 -g '#_hydrateUtxo' test/unit/", - "test:temp3": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#transaction' test/integration/", + "test:temp2": "mocha --timeout=30000 -g '#TransactionLib' test/unit/", + "test:temp3": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#Transaction' test/integration/", "test:temp4": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#decodeOpReturn' test/integration/", "coverage": "nyc report --reporter=text-lcov | coveralls", "coverage:report": "nyc --reporter=html mocha --timeout 25000 test/unit/", diff --git a/src/transaction.js b/src/transaction.js index 023b042..fb22cd3 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -21,6 +21,7 @@ class Transaction { } const txDetails = await this.rawTransaction.getTxData(txid) + // console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`) // Setup default SLP properties. txDetails.isValidSLPTx = false @@ -119,84 +120,6 @@ class Transaction { throw err } } - - // Wraps getTxData(), but also appends SLP token information to each input - // and output of the transaction. This is a very API-heavy call. - // Returns false if the txid is not an SLP transaction. - // - // Warning! This is a prototype function and the output can change at any time - // without reflecting a change in the semantic version. DO NOT USE IN PRODUCTION. - // async getTxDataSlp (txid) { - // try { - // if (typeof txid !== 'string') { - // throw new Error('Input must be a string or array of strings.') - // } - // - // const txDetails = await this.getTxData(txid) - // - // // First get the token information for the output. If that fails, then - // // this is not an SLP transaction, and this method can return false. - // let outTokenData - // try { - // outTokenData = await this.slpUtils.decodeOpReturn(txid) - // // console.log(`outTokenData: ${JSON.stringify(outTokenData, null, 2)}`) - // - // // Add token information to the tx details object. - // txDetails.tokenTxType = outTokenData.txType - // txDetails.tokenId = outTokenData.tokenId - // - // // Add the token quantity to each output. - // for (let i = 0; i < outTokenData.amounts.length; i++) { - // txDetails.vout[i + 1].tokenQty = outTokenData.amounts[i] - // } - // - // // Loop through each input and retrieve the token data. - // for (let i = 0; i < txDetails.vin.length; i++) { - // const thisVin = txDetails.vin[i] - // - // try { - // // If decodeOpReturn() throws an error, then this input is not - // // from an SLP transaction and can be ignored. - // const inTokenData = await this.slpUtils.decodeOpReturn(thisVin.txid) - // // console.log( - // // `vin[${i}] tokenData: ${JSON.stringify(inTokenData, null, 2)}` - // // ) - // - // // Get the appropriate vout token amount. This may throw an error, - // // which means this Vin is not actually a token UTXO, it was just - // // associated with a previous token TX. - // const tokenQty = inTokenData.amounts[thisVin.vout - 1] - // // console.log(`tokenQty: ${JSON.stringify(tokenQty, null, 2)}`) - // - // if (tokenQty) { - // thisVin.tokenQty = tokenQty - // // txDetails.vin[i].tokenQty = tokenQty - // } else { - // thisVin.tokenQty = null - // } - // } catch (err) { - // // console.log('catch 2: ', err) - // // If decodeOpReturn() throws an error, then this input is not - // // from an SLP transaction and can be ignored. - // // thisVin.tokenQty = null - // thisVin.tokenQty = null - // continue - // } - // // console.log( - // // `2: txDetails.vin[i]: ${JSON.stringify(txDetails.vin[i], null, 2)}` - // // ) - // } - // } catch (err) { - // // console.log('catch 1: ', err) - // return false - // } - // - // return txDetails - // } catch (error) { - // if (error.response && error.response.data) throw error.response.data - // else throw error - // } - // } } module.exports = Transaction diff --git a/test/integration/transaction-integration.js b/test/integration/transaction-integration.js index d47289e..3a924b8 100644 --- a/test/integration/transaction-integration.js +++ b/test/integration/transaction-integration.js @@ -6,7 +6,7 @@ const assert = require('chai').assert const BCHJS = require('../../src/bch-js') const bchjs = new BCHJS() -describe('#transaction', () => { +describe('#Transaction', () => { beforeEach(async () => { if (process.env.IS_USING_FREE_TIER) await bchjs.Util.sleep(1000) }) diff --git a/test/unit/fixtures/transaction-mock.js b/test/unit/fixtures/transaction-mock.js new file mode 100644 index 0000000..abf9ace --- /dev/null +++ b/test/unit/fixtures/transaction-mock.js @@ -0,0 +1,184 @@ +/* + Mocking data used in the transaction-unit.js tests. +*/ + +const nonSlpTxDetails = { + txid: '2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7', + hash: '2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7', + version: 2, + size: 225, + locktime: 0, + vin: [ + { + txid: '5f09d317e24c5d376f737a2711f3bd1d381abdb41743fff3819b4f76382e1eac', + vout: 1, + scriptSig: { + asm: + '3044022000dd11c41a472f2e54348db996e60864d489429f12d1e044d49ff600b880c9590220715a926404bb0e2731a3795afb341ec1dad3f84ead7d27cd31fcc59abb14738c[ALL|FORKID] 038476128287ac37c7a3cf7e8625fd5f024db1bc3d8e37395abe7bf42fda78d0d9', + hex: + '473044022000dd11c41a472f2e54348db996e60864d489429f12d1e044d49ff600b880c9590220715a926404bb0e2731a3795afb341ec1dad3f84ead7d27cd31fcc59abb14738c4121038476128287ac37c7a3cf7e8625fd5f024db1bc3d8e37395abe7bf42fda78d0d9' + }, + sequence: 4294967295, + address: 'bitcoincash:qqxy8hycqe89j7wa79gnggq6z3gaqu2uvqy26xehfe', + value: 0.00047504 + } + ], + vout: [ + { + value: 0.00001, + n: 0, + scriptPubKey: { + asm: + 'OP_DUP OP_HASH160 2fe2c4c5ef359bb2fe1a849f891cecffbcfb4f77 OP_EQUALVERIFY OP_CHECKSIG', + hex: '76a9142fe2c4c5ef359bb2fe1a849f891cecffbcfb4f7788ac', + reqSigs: 1, + type: 'pubkeyhash', + addresses: ['bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'] + } + }, + { + value: 0.00046256, + n: 1, + scriptPubKey: { + asm: + 'OP_DUP OP_HASH160 2dbf5e1804c39a497b908c876097d63210c84902 OP_EQUALVERIFY OP_CHECKSIG', + hex: '76a9142dbf5e1804c39a497b908c876097d63210c8490288ac', + reqSigs: 1, + type: 'pubkeyhash', + addresses: ['bitcoincash:qqkm7hscqnpe5jtmjzxgwcyh6ceppjzfqg3jdn422e'] + } + } + ], + hex: + '0200000001ac1e2e38764f9b81f3ff4317b4bd1a381dbdf311277a736f375d4ce217d3095f010000006a473044022000dd11c41a472f2e54348db996e60864d489429f12d1e044d49ff600b880c9590220715a926404bb0e2731a3795afb341ec1dad3f84ead7d27cd31fcc59abb14738c4121038476128287ac37c7a3cf7e8625fd5f024db1bc3d8e37395abe7bf42fda78d0d9ffffffff02e8030000000000001976a9142fe2c4c5ef359bb2fe1a849f891cecffbcfb4f7788acb0b40000000000001976a9142dbf5e1804c39a497b908c876097d63210c8490288ac00000000', + blockhash: '0000000000000000010903a1fc4274499037c9339be9ec7338ee980331c20ce5', + confirmations: 77741, + time: 1569792892, + blocktime: 1569792892 +} + +const slpTxDetails = { + txid: '266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1', + hash: '266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1', + version: 2, + size: 479, + locktime: 0, + vin: [ + { + txid: 'abc685f1f2a95f51e5e05a350f3fb9c74676e9f78c835b2a019c888ac0a2a736', + vout: 2, + scriptSig: { + asm: + '3045022100e5f0f6f1212fcbb10eedb7fdc38fca6e86629b4e7e8356a3ad7371a109fc37a602204bfff37d1a34d2e2908b81c23677706fb59ff4ab639fa3299da6c303de74e1f7[ALL|FORKID] 0245b9b3586fab3cfd46db6d116c4588004fe7fe9798216ccb8e55a89bcebc07ac', + hex: + '483045022100e5f0f6f1212fcbb10eedb7fdc38fca6e86629b4e7e8356a3ad7371a109fc37a602204bfff37d1a34d2e2908b81c23677706fb59ff4ab639fa3299da6c303de74e1f741210245b9b3586fab3cfd46db6d116c4588004fe7fe9798216ccb8e55a89bcebc07ac' + }, + sequence: 4294967295, + address: 'bitcoincash:qzv7t2pzn2d0pklnetdjt65crh6fe8vnhuwvhsk2nn', + value: 0.00000546 + }, + { + txid: '58c8576404c01c23a224053307399483d3a070599b3e9eb6d45be9714b8d6856', + vout: 1, + scriptSig: { + asm: + '30430220784f6d81fa8f54db8a4948259e8c15972a0285f8b1640c433d4e9f606dc38f0c021f14eecc2e8af2efede0867ce459c400dde54186a0e64babdbe89f795db12753[ALL|FORKID] 0209ebe6d9da5043945ed1d81bec0fcace299eba05e5f46b72d6838c790d31c505', + hex: + '4630430220784f6d81fa8f54db8a4948259e8c15972a0285f8b1640c433d4e9f606dc38f0c021f14eecc2e8af2efede0867ce459c400dde54186a0e64babdbe89f795db1275341210209ebe6d9da5043945ed1d81bec0fcace299eba05e5f46b72d6838c790d31c505' + }, + sequence: 4294967295, + address: 'bitcoincash:qppzuxemgqyxf07nz3kan33gmc83mf3z3yz295c4s7', + value: 0.68369626 + } + ], + vout: [ + { + value: 0, + n: 0, + scriptPubKey: { + asm: + 'OP_RETURN 5262419 1 1145980243 497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7 0000000005f5e100 00005ad7e49d9100', + hex: + '6a04534c500001010453454e4420497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7080000000005f5e1000800005ad7e49d9100', + type: 'nulldata' + } + }, + { + value: 0.00000546, + n: 1, + scriptPubKey: { + asm: + 'OP_DUP OP_HASH160 36be3b7d185a85b6cf6fc61d63c16f2f10e54260 OP_EQUALVERIFY OP_CHECKSIG', + hex: '76a91436be3b7d185a85b6cf6fc61d63c16f2f10e5426088ac', + reqSigs: 1, + type: 'pubkeyhash', + addresses: ['bitcoincash:qqmtuwmarpdgtdk0dlrp6c7pduh3pe2zvqrkys2ex8'] + } + }, + { + value: 0.00000546, + n: 2, + scriptPubKey: { + asm: + 'OP_DUP OP_HASH160 99e5a8229a9af0dbf3cadb25ea981df49c9d93bf OP_EQUALVERIFY OP_CHECKSIG', + hex: '76a91499e5a8229a9af0dbf3cadb25ea981df49c9d93bf88ac', + reqSigs: 1, + type: 'pubkeyhash', + addresses: ['bitcoincash:qzv7t2pzn2d0pklnetdjt65crh6fe8vnhuwvhsk2nn'] + } + }, + { + value: 0.68368564, + n: 3, + scriptPubKey: { + asm: + 'OP_DUP OP_HASH160 422e1b3b400864bfd3146dd9c628de0f1da62289 OP_EQUALVERIFY OP_CHECKSIG', + hex: '76a914422e1b3b400864bfd3146dd9c628de0f1da6228988ac', + reqSigs: 1, + type: 'pubkeyhash', + addresses: ['bitcoincash:qppzuxemgqyxf07nz3kan33gmc83mf3z3yz295c4s7'] + } + } + ], + hex: + '020000000236a7a2c08a889c012a5b838cf7e97646c7b93f0f355ae0e5515fa9f2f185c6ab020000006b483045022100e5f0f6f1212fcbb10eedb7fdc38fca6e86629b4e7e8356a3ad7371a109fc37a602204bfff37d1a34d2e2908b81c23677706fb59ff4ab639fa3299da6c303de74e1f741210245b9b3586fab3cfd46db6d116c4588004fe7fe9798216ccb8e55a89bcebc07acffffffff56688d4b71e95bd4b69e3e9b5970a0d383943907330524a2231cc0046457c85801000000694630430220784f6d81fa8f54db8a4948259e8c15972a0285f8b1640c433d4e9f606dc38f0c021f14eecc2e8af2efede0867ce459c400dde54186a0e64babdbe89f795db1275341210209ebe6d9da5043945ed1d81bec0fcace299eba05e5f46b72d6838c790d31c505ffffffff040000000000000000406a04534c500001010453454e4420497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7080000000005f5e1000800005ad7e49d910022020000000000001976a91436be3b7d185a85b6cf6fc61d63c16f2f10e5426088ac22020000000000001976a91499e5a8229a9af0dbf3cadb25ea981df49c9d93bf88acb4381304000000001976a914422e1b3b400864bfd3146dd9c628de0f1da6228988ac00000000', + blockhash: '0000000000000000015284202422a688554b7fc80c54f18122847a99c4f79607', + confirmations: 76722, + time: 1570392893, + blocktime: 1570392893 +} + +const mockOpReturnData01 = { + tokenType: 1, + txType: 'SEND', + tokenId: '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7', + amounts: ['100000000', '99883300000000'] +} + +const mockOpReturnData02 = { + tokenType: 1, + txType: 'GENESIS', + ticker: 'TOK-CH', + name: 'TokyoCash', + tokenId: '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7', + documentUri: '', + documentHash: '', + decimals: 8, + mintBatonVout: 0, + qty: '2100000000000000' +} + +const mockOpReturnData03 = { + tokenType: 1, + txType: 'SEND', + tokenId: '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7', + amounts: ['1000000000', '99883400000000'] +} + +module.exports = { + nonSlpTxDetails, + slpTxDetails, + mockOpReturnData01, + mockOpReturnData02, + mockOpReturnData03 +} diff --git a/test/unit/transaction-unit.js b/test/unit/transaction-unit.js index 4fc123c..803d90d 100644 --- a/test/unit/transaction-unit.js +++ b/test/unit/transaction-unit.js @@ -1,3 +1,119 @@ /* Unit tests for the transaction.js library. */ + +// Public npm libraries +const assert = require('chai').assert +const sinon = require('sinon') + +const BCHJS = require('../../src/bch-js') +const bchjs = new BCHJS() + +const mockData = require('./fixtures/transaction-mock.js') + +describe('#TransactionLib', () => { + let sandbox + beforeEach(() => (sandbox = sinon.createSandbox())) + afterEach(() => sandbox.restore()) + + describe('#get', () => { + it('should throw an error if txid is not specified', async () => { + try { + await bchjs.Transaction.get() + + assert.fail('Unexpected code path!') + } catch (err) { + assert.include( + err.message, + 'Input must be a string or array of strings.' + ) + } + }) + + it('should get details about a non-SLP transaction', async () => { + const txid = + '2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7' + + // Mock dependencies + sandbox + .stub(bchjs.Transaction.rawTransaction, 'getTxData') + .resolves(mockData.nonSlpTxDetails) + + const result = await bchjs.Transaction.get(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 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 () => { + // Mock dependencies + sandbox + .stub(bchjs.Transaction.rawTransaction, 'getTxData') + .resolves(mockData.slpTxDetails) + sandbox + .stub(bchjs.Transaction.slpUtils, 'decodeOpReturn') + .onCall(0) + .resolves(mockData.mockOpReturnData01) + .onCall(1) + .resolves(mockData.mockOpReturnData02) + .onCall(2) + .resolves(mockData.mockOpReturnData03) + .onCall(3) + .rejects(new Error('No OP_RETURN')) + sandbox + .stub(bchjs.Transaction.slpUtils, 'waterfallValidateTxid') + .resolves(true) + + const txid = + '266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1' + + const result = await bchjs.Transaction.get(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 catch and throw error on network error', async () => { + try { + const txid = + '2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7' + + // Force an error + sandbox + .stub(bchjs.Transaction.rawTransaction, 'getTxData') + .rejects(new Error('test error')) + + await bchjs.Transaction.get(txid) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) +}) From faa297246011494826e3d7d52e40a7783125c71d Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 24 Mar 2021 07:28:38 -0700 Subject: [PATCH 5/5] fix(Transaction.get()): Added documentation --- src/transaction.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/transaction.js b/src/transaction.js index fb22cd3..5011ad6 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -12,8 +12,28 @@ class Transaction { this.rawTransaction = new RawTransaction(config) } - // Get hydrated details about a transaction, including SLP token details if - // it's an SLP transaction. + /** + * @api Transaction.get() get() + * @apiName get + * @apiGroup Transaction + * @apiDescription + * Returns an object of transaction data, including addresses for input UTXOs. + * If it is a SLP token transaction, the token information for inputs and + * outputs will also be included. + * + * This is an API heavy call. This function will only work with a single txid. + * It does not yet support an array of TXIDs. + * + * @apiExample Example usage: + * (async () => { + * try { + * let txData = await bchjs.Transaction.get("0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098"); + * console.log(txData); + * } catch(error) { + * console.error(error) + * } + * })() + */ async get (txid) { try { if (typeof txid !== 'string') {