From ef7056ee7a8f9469efb1bb8d57f22a32a359dbc2 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 31 Oct 2021 16:29:50 -0700 Subject: [PATCH 1/2] fix(Transaction.get2()): Added non-validating tx get --- src/transaction.js | 192 ++++++++++++++++++++++++++++ test/unit/transaction-unit.js | 233 ++++++++++++++++++++++++++++++++++ 2 files changed, 425 insertions(+) diff --git a/src/transaction.js b/src/transaction.js index 1e9f26b..a0cacad 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -35,6 +35,10 @@ class Transaction { * } * })() */ + + // CT 10/31/21: TODO: this function should be refactored to use get2(), but + // add waterfall validation of the TX and its inputs. + async get (txid) { try { if (typeof txid !== 'string') { @@ -216,6 +220,194 @@ class Transaction { throw err } } + + /** + * @api Transaction.get2() get2() + * @apiName get2 + * @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. + * + * This is the same as get(), except it omits DAG validation of the TXID. + * + * @apiExample Example usage: + * (async () => { + * try { + * let txData = await bchjs.Transaction.get2("0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098"); + * console.log(txData); + * } catch(error) { + * console.error(error) + * } + * })() + */ + async get2 (txid) { + try { + if (typeof txid !== 'string') { + throw new Error( + 'Input to Transaction.get() must be a string containing a TXID.' + ) + } + + const txDetails = await this.rawTransaction.getTxData(txid) + // console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`) + + // 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)}`) + + // 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++) { + const rawQty = outTokenData.amounts[i] + // const realQty = Number(rawQty) / Math.pow(10, txDetails.tokenDecimals) + + // Calculate the real quantity using a BigNumber, then convert it to a + // floating point number. + let realQty = new BigNumber(rawQty).dividedBy( + 10 ** parseInt(txDetails.tokenDecimals) + ) + realQty = realQty.toString() + // realQty = parseFloat(realQty) + + txDetails.vout[i + 1].tokenQtyStr = realQty + txDetails.vout[i + 1].tokenQty = parseFloat(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. + for (let i = 0; i < txDetails.vin.length; i++) { + const thisVin = txDetails.vin[i] + // console.log(`thisVin: ${JSON.stringify(thisVin, null, 2)}`) + + 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)}` + // ) + + let tokenQty = 0 + + if (inTokenData.txType === 'SEND') { + // 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. + tokenQty = inTokenData.amounts[thisVin.vout - 1] + // console.log(`tokenQty: ${JSON.stringify(tokenQty, null, 2)}`) + + // + } else if (inTokenData.txType === 'GENESIS') { + // Only vout[1] of a Genesis transaction represents the tokens. + // Any other outputs in that transaction are normal BCH UTXOs. + if (thisVin.vout === 1) { + tokenQty = inTokenData.qty + // console.log(`tokenQty: ${JSON.stringify(tokenQty, null, 2)}`) + } + } else if (inTokenData.txType === 'MINT') { + // vout=1 (second output) recieves the newly minted tokens. + if (thisVin.vout === 1) { + tokenQty = inTokenData.qty + } else { + tokenQty = null + } + + // + } else { + console.log( + 'Unexpected code path in Transaction.get2(). What is the txType?' + ) + console.log(inTokenData) + throw new Error('Unexpected code path') + } + + if (tokenQty) { + // Calculate the real quantity using a BigNumber, then convert it to a + // floating point number. + let realQty = new BigNumber(tokenQty).dividedBy( + 10 ** parseInt(txDetails.tokenDecimals) + ) + realQty = realQty.toString() + // realQty = parseFloat(realQty) + + thisVin.tokenQtyStr = realQty + thisVin.tokenQty = parseFloat(realQty) + // txDetails.vin[i].tokenQty = tokenQty + + // Add token ID to input + thisVin.tokenId = inTokenData.tokenId + } else { + thisVin.tokenQty = null + } + } catch (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 + } + } + + // TODO: Convert the block hash to a block height. Add block height + // value to the transaction. + } catch (err) { + // console.log('Error: ', err) + + // This case handles rate limit errors. + if (err.response && err.response.data && err.response.data.error) { + throw new Error(err.response.data.error) + } + + // 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(): ', err) + + // This case handles rate limit errors. + if (err.response && err.response.data && err.response.data.error) { + throw new Error(err.response.data.error) + } + + if (err.error) throw new Error(err.error) + throw err + } + } } module.exports = Transaction diff --git a/test/unit/transaction-unit.js b/test/unit/transaction-unit.js index d282c80..c707161 100644 --- a/test/unit/transaction-unit.js +++ b/test/unit/transaction-unit.js @@ -265,4 +265,237 @@ describe('#TransactionLib', () => { assert.equal(result.vin[2].tokenQty, 99000000) }) }) + + describe('#get2', () => { + it('should throw an error if txid is not specified', async () => { + try { + await bchjs.Transaction.get2() + + assert.fail('Unexpected code path!') + } catch (err) { + assert.include( + err.message, + 'Input to Transaction.get() must be a string containing a TXID.' + ) + } + }) + + 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.get2(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.get2(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') + }) + + 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') + } + }) + + // This test case was created in response to a bug. When the input TX + // was a Genesis SLP transaction, the inputs of the transaction were not + // being hydrated properly. + it('should get input details when input is a genesis tx', async () => { + // Mock dependencies + sandbox + .stub(bchjs.Transaction.rawTransaction, 'getTxData') + .resolves(mockData.genesisTestInputTx) + sandbox + .stub(bchjs.Transaction.slpUtils, 'decodeOpReturn') + .onCall(0) + .resolves(mockData.genesisTestOpReturnData01) + .onCall(1) + .resolves(mockData.genesisTestOpReturnData02) + .onCall(2) + .resolves(mockData.genesisTestOpReturnData02) + .onCall(3) + .resolves(mockData.genesisTestOpReturnData02) + + const txid = + '874306bda204d3a5dd15e03ea5732cccdca4c33a52df35162cdd64e30ea7f04e' + + const result = await bchjs.Transaction.get2(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 inputs values unique to a Genesis input have the proper values. + assert.equal(result.vin[0].tokenQty, 10000000) + assert.equal(result.vin[1].tokenQty, null) + }) + + it('should get input details when input is a mint tx', async () => { + // Mock dependencies + sandbox + .stub(bchjs.Transaction.rawTransaction, 'getTxData') + .resolves(mockData.mintTestInputTx) + sandbox + .stub(bchjs.Transaction.slpUtils, 'decodeOpReturn') + .onCall(0) + .resolves(mockData.mintTestOpReturnData01) + .onCall(1) + .resolves(mockData.mintTestOpReturnData02) + .onCall(2) + .resolves(mockData.mintTestOpReturnData02) + .onCall(3) + .resolves(mockData.mintTestOpReturnData03) + .onCall(4) + .resolves(mockData.mintTestOpReturnData03) + + const txid = + '4640a734063ea79fa587a3cac38a70a2f6f3db0011e23514024185982110d0fa' + + const result = await bchjs.Transaction.get2(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 inputs values unique to a Mint input have the proper values. + assert.equal(result.vin[0].tokenQty, 43545.34534) + assert.equal(result.vin[1].tokenQty, 2.34123) + assert.equal(result.vin[2].tokenQty, null) + }) + + // This test case was generated from the problematic transaction that + // used inputs in a 'non-standard' way. + it('should correctly assign quantities to mixed mint inputs', async () => { + // Mock dependencies + sandbox + .stub(bchjs.Transaction.rawTransaction, 'getTxData') + .resolves(mockData.sendTestInputTx01) + sandbox + .stub(bchjs.Transaction.slpUtils, 'decodeOpReturn') + .onCall(0) + .resolves(mockData.sendTestOpReturnData01) + .onCall(1) + .resolves(mockData.sendTestOpReturnData02) + .onCall(2) + .resolves(mockData.sendTestOpReturnData03) + .onCall(3) + .resolves(mockData.sendTestOpReturnData03) + .onCall(4) + .resolves(mockData.sendTestOpReturnData04) + + const txid = + '6bc111fbf5b118021d68355ca19a0e77fa358dd931f284b2550f79a51ab4792a' + + const result = await bchjs.Transaction.get2(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 inputs values unique to a Mint input have the proper values. + assert.equal(result.vin[0].tokenQty, 100000000) + assert.equal(result.vin[1].tokenQty, null) + assert.equal(result.vin[2].tokenQty, 99000000) + }) + }) }) From 360fca0475e186b8277735479893831d97a3fd94 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 31 Oct 2021 16:58:40 -0700 Subject: [PATCH 2/2] fix(Transaction.get2()): Added block height to tx data --- src/transaction.js | 14 ++++++++++---- test/unit/transaction-unit.js | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/transaction.js b/src/transaction.js index a0cacad..a9e1883 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -2,15 +2,18 @@ High-level functions for working with Transactions */ +const BigNumber = require('bignumber.js') + const RawTransaction = require('./raw-transactions') const SlpUtils = require('./slp/utils') -const BigNumber = require('bignumber.js') +const Blockchain = require('./blockchain') class Transaction { constructor (config) { // Encapsulate dependencies this.slpUtils = new SlpUtils(config) this.rawTransaction = new RawTransaction(config) + this.blockchain = new Blockchain(config) } /** @@ -256,6 +259,12 @@ class Transaction { const txDetails = await this.rawTransaction.getTxData(txid) // console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`) + // Get the block height the transaction was mined in. + const blockHeader = await this.blockchain.getBlockHeader( + txDetails.blockhash + ) + txDetails.blockheight = blockHeader.height + // 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 @@ -379,9 +388,6 @@ class Transaction { continue } } - - // TODO: Convert the block hash to a block height. Add block height - // value to the transaction. } catch (err) { // console.log('Error: ', err) diff --git a/test/unit/transaction-unit.js b/test/unit/transaction-unit.js index c707161..f45f99d 100644 --- a/test/unit/transaction-unit.js +++ b/test/unit/transaction-unit.js @@ -288,6 +288,9 @@ describe('#TransactionLib', () => { sandbox .stub(bchjs.Transaction.rawTransaction, 'getTxData') .resolves(mockData.nonSlpTxDetails) + sandbox + .stub(bchjs.Transaction.blockchain, 'getBlockHeader') + .resolves({ height: 602405 }) const result = await bchjs.Transaction.get2(txid) // console.log(`result: ${JSON.stringify(result, null, 2)}`) @@ -304,6 +307,9 @@ describe('#TransactionLib', () => { assert.property(result.vin[0], 'value') assert.property(result, 'isValidSLPTx') assert.equal(result.isValidSLPTx, false) + + // Assert blockheight is added + assert.equal(result.blockheight, 602405) }) it('should get details about a SLP transaction', async () => { @@ -311,6 +317,9 @@ describe('#TransactionLib', () => { sandbox .stub(bchjs.Transaction.rawTransaction, 'getTxData') .resolves(mockData.slpTxDetails) + sandbox + .stub(bchjs.Transaction.blockchain, 'getBlockHeader') + .resolves({ height: 603424 }) sandbox .stub(bchjs.Transaction.slpUtils, 'decodeOpReturn') .onCall(0) @@ -321,9 +330,6 @@ describe('#TransactionLib', () => { .resolves(mockData.mockOpReturnData03) .onCall(3) .rejects(new Error('No OP_RETURN')) - // sandbox - // .stub(bchjs.Transaction.slpUtils, 'waterfallValidateTxid') - // .resolves(true) const txid = '266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1' @@ -344,6 +350,9 @@ describe('#TransactionLib', () => { assert.property(result.vin[0], 'address') assert.property(result.vin[0], 'value') assert.property(result.vin[0], 'tokenQty') + + // Assert blockheight is added + assert.equal(result.blockheight, 603424) }) it('should catch and throw error on network error', async () => { @@ -372,6 +381,9 @@ describe('#TransactionLib', () => { sandbox .stub(bchjs.Transaction.rawTransaction, 'getTxData') .resolves(mockData.genesisTestInputTx) + sandbox + .stub(bchjs.Transaction.blockchain, 'getBlockHeader') + .resolves({ height: 543409 }) sandbox .stub(bchjs.Transaction.slpUtils, 'decodeOpReturn') .onCall(0) @@ -406,6 +418,9 @@ describe('#TransactionLib', () => { // Assert inputs values unique to a Genesis input have the proper values. assert.equal(result.vin[0].tokenQty, 10000000) assert.equal(result.vin[1].tokenQty, null) + + // Assert blockheight is added + assert.equal(result.blockheight, 543409) }) it('should get input details when input is a mint tx', async () => { @@ -413,6 +428,9 @@ describe('#TransactionLib', () => { sandbox .stub(bchjs.Transaction.rawTransaction, 'getTxData') .resolves(mockData.mintTestInputTx) + sandbox + .stub(bchjs.Transaction.blockchain, 'getBlockHeader') + .resolves({ height: 543614 }) sandbox .stub(bchjs.Transaction.slpUtils, 'decodeOpReturn') .onCall(0) @@ -450,6 +468,9 @@ describe('#TransactionLib', () => { assert.equal(result.vin[0].tokenQty, 43545.34534) assert.equal(result.vin[1].tokenQty, 2.34123) assert.equal(result.vin[2].tokenQty, null) + + // Assert blockheight is added + assert.equal(result.blockheight, 543614) }) // This test case was generated from the problematic transaction that @@ -459,6 +480,9 @@ describe('#TransactionLib', () => { sandbox .stub(bchjs.Transaction.rawTransaction, 'getTxData') .resolves(mockData.sendTestInputTx01) + sandbox + .stub(bchjs.Transaction.blockchain, 'getBlockHeader') + .resolves({ height: 543957 }) sandbox .stub(bchjs.Transaction.slpUtils, 'decodeOpReturn') .onCall(0) @@ -496,6 +520,9 @@ describe('#TransactionLib', () => { assert.equal(result.vin[0].tokenQty, 100000000) assert.equal(result.vin[1].tokenQty, null) assert.equal(result.vin[2].tokenQty, 99000000) + + // Assert blockheight is added + assert.equal(result.blockheight, 543957) }) }) })