From d2e847d3fb619cf49a8244c0808e02442e665391 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 23 Mar 2021 17:11:37 -0700 Subject: [PATCH] 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. +*/