From dc85bb4cfe5a01fbe027c7059f0484b19c6bbb4d Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 3 Nov 2021 07:53:42 -0700 Subject: [PATCH] Making progress on Transaction.get3() --- src/transaction.js | 231 +++++++++++++++++++++++++ test/unit/fixtures/transaction-mock.js | 11 +- test/unit/transaction-unit.js | 149 +++++++++------- 3 files changed, 332 insertions(+), 59 deletions(-) diff --git a/src/transaction.js b/src/transaction.js index c0e3d84..4345410 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -435,6 +435,237 @@ class Transaction { throw err } } + + // Refactoring code in a more structured way + async get3(txid) { + try { + if (typeof txid !== 'string') { + throw new Error( + 'Input to Transaction.get() must be a string containing a TXID.' + ) + } + + // Get TX data + 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 + + // Set default as not an SLP tx + txDetails.isSlpTx = false + + // Get Token Data + const txTokenData = await this.getTokenInfo(txid) + console.log(`txTokenData: ${JSON.stringify(txTokenData, null, 2)}`) + + // If not a token, return the tx data. Processing is complete. + if(!txTokenData) return txDetails + + // Mark TX as an SLP tx. This does not mean it's valid, it just means + // the OP_RETURN passes a basic check. + txDetails.isSlpTx = true + + // Get Genesis data + const genesisData = await this.getTokenInfo(txTokenData.tokenId) + console.log(`genesisData: ${JSON.stringify(genesisData, null, 2)}`) + + // Add token information to the tx details object. + txDetails.tokenTxType = txTokenData.txType + txDetails.tokenId = txTokenData.tokenId + txDetails.tokenTicker = genesisData.ticker + txDetails.tokenName = genesisData.name + txDetails.tokenDecimals = genesisData.decimals + txDetails.tokenUri = genesisData.documentUri + txDetails.tokenDocHash = genesisData.documentHash + // console.log(`txDetails before processing input and outputs: ${JSON.stringify(txDetails, null, 2)}`) + + // Process TX Outputs + // Add the token quantity to each output. + // 'i' starts at 1, because vout[0] is the OP_RETURN + for (let i = 0; i < txDetails.vout.length; i++) { + const thisVout = txDetails.vout[i] + if (txTokenData.txType === 'SEND') { + console.log(`txTokenData: ${JSON.stringify(txTokenData, null, 2)}`) + + // First output is OP_RETURN, so tokenQty is null. + if(i === 0) { + thisVout.tokenQty = null + thisVout.tokenQtyStr = null + continue + } + + // Non SLP outputs. + if(i > txTokenData.amounts.length) { + thisVout.tokenQty = null + thisVout.tokenQtyStr = null + continue + } + + const rawQty = txTokenData.amounts[i-1] + + // 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].tokenQtyStr = realQty + txDetails.vout[i].tokenQty = parseFloat(realQty) + + console.log( + `thisVout ${i}: ${JSON.stringify(txDetails.vout[i], null, 2)}` + ) + } else if(txTokenData.txType === 'GENESIS' || txTokenData.txType === 'MINT') { + console.log(`txTokenData: ${JSON.stringify(txTokenData, null, 2)}`) + + let tokenQty = 0 // Default value + + // Only vout[1] of a Genesis or Mint transaction represents the tokens. + // Any other outputs in that transaction are normal BCH UTXOs. + if (i === 1) { + tokenQty = txTokenData.qty + console.log(`tokenQty: ${JSON.stringify(tokenQty, null, 2)}`) + + // 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) + + thisVout.tokenQtyStr = realQty + thisVout.tokenQty = parseFloat(realQty) + console.log(`thisVout[${i}]: ${JSON.stringify(thisVout, null, 2)}`) + } else if(i === txTokenData.mintBatonVout) { + // Optional Mint baton + thisVout.tokenQtyStr = "0" + thisVout.tokenQty = 0 + thisVout.isMintBaton = true + } else { + thisVout.tokenQtyStr = "0" + thisVout.tokenQty = 0 + } + } else { + throw new Error('Unknown SLP TX type for TX') + } + } + + // Process TX inputs + for(let i=0; i < txDetails.vin.length; i++) { + const thisVin = txDetails.vin[i] + + const vinTokenData = await this.getTokenInfo(thisVin.txid) + console.log(`vinTokenData: ${JSON.stringify(vinTokenData, null, 2)}`) + + // Corner case: Ensure the token ID is the same. + const vinTokenIdIsTheSame = vinTokenData.tokenId === txDetails.tokenId + + // If the input is not a token input, or if the tokenID is not the same, + // then mark the token output as null. + if(!vinTokenData || !vinTokenIdIsTheSame) { + thisVin.tokenQty = 0 + thisVin.tokenQtyStr = "0" + thisVin.tokenId = null + continue + } + + if(vinTokenData.txType === 'SEND') { + console.log(`vinTokenData: ${JSON.stringify(vinTokenData, null, 2)}`) + + const tokenQty = vinTokenData.amounts[thisVin.vout - 1] + // console.log(`tokenQty: ${JSON.stringify(tokenQty, null, 2)}`) + + // 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) + thisVin.tokenId = vinTokenData.tokenId + } else if(vinTokenData.txType === 'GENESIS' || vinTokenData.txType === 'MINT') { + console.log(`vinTokenData: ${JSON.stringify(vinTokenData, null, 2)}`) + + let tokenQty = 0 // Default value + + // 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 = vinTokenData.qty + // console.log(`tokenQty: ${JSON.stringify(tokenQty, null, 2)}`) + + // 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) + thisVin.tokenId = vinTokenData.tokenId + } else if(thisVin.vout === vinTokenData.mintBatonVout) { + // Optional Mint baton + thisVin.tokenQtyStr = "0" + thisVin.tokenQty = 0 + thisVin.tokenId = vinTokenData.tokenId + thisVin.isMintBaton = true + } else { + thisVin.tokenQtyStr = "0" + thisVin.tokenQty = 0 + thisVin.tokenId = null + } + } else { + console.log(`vinTokenData: ${JSON.stringify(vinTokenData, null, 2)}`) + throw new Error('Unknown token type in input') + } + } + + // if SEND + // if GENSIS + // if MINT + // Update Inputs + // if not SLP + // Ensure tokenID matches + // if SEND + // if GENESIS + // if MINT + + return txDetails + } catch(err) { + console.error('Error in get3()') + + // 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 + } + } + + // A wrapper for decodeOpReturn(). Returns false if txid is not an SLP tx. + // Returns the token data if the txid is an SLP tx. + async getTokenInfo (txid) { + try { + const tokenData = await this.slpUtils.decodeOpReturn(txid) + return tokenData + } catch(err) { + return false + } + } } module.exports = Transaction diff --git a/test/unit/fixtures/transaction-mock.js b/test/unit/fixtures/transaction-mock.js index 46dc4f1..408e070 100644 --- a/test/unit/fixtures/transaction-mock.js +++ b/test/unit/fixtures/transaction-mock.js @@ -656,6 +656,14 @@ const mintTestInputTx02 = { blocktime: 1534391953 } +const mintTestOpReturnData04 = { + "tokenType": 1, + "txType": "MINT", + "tokenId": "938cc18e618967d787897bbc64b9a8d201b94ec7c69b1a9949eab0433ba5cdf8", + "mintBatonVout": 2, + "qty": "234123" +} + module.exports = { nonSlpTxDetails, slpTxDetails, @@ -674,5 +682,6 @@ module.exports = { sendTestOpReturnData02, sendTestOpReturnData03, sendTestOpReturnData04, - mintTestInputTx02 + mintTestInputTx02, + mintTestOpReturnData04 } diff --git a/test/unit/transaction-unit.js b/test/unit/transaction-unit.js index 76fe170..7fff76a 100644 --- a/test/unit/transaction-unit.js +++ b/test/unit/transaction-unit.js @@ -5,15 +5,21 @@ // Public npm libraries const assert = require('chai').assert const sinon = require('sinon') +const cloneDeep = require('lodash.clonedeep') const BCHJS = require('../../src/bch-js') const bchjs = new BCHJS() -const mockData = require('./fixtures/transaction-mock.js') +const mockDataLib = require('./fixtures/transaction-mock.js') describe('#TransactionLib', () => { - let sandbox - beforeEach(() => (sandbox = sinon.createSandbox())) + let sandbox, mockData + + beforeEach(() => { + sandbox = sinon.createSandbox() + + mockData = cloneDeep(mockDataLib) + }) afterEach(() => sandbox.restore()) describe('#get', () => { @@ -266,10 +272,10 @@ describe('#TransactionLib', () => { }) }) - describe('#get2', () => { + describe('#get3', () => { it('should throw an error if txid is not specified', async () => { try { - await bchjs.Transaction.get2() + await bchjs.Transaction.get3() assert.fail('Unexpected code path!') } catch (err) { @@ -291,8 +297,9 @@ describe('#TransactionLib', () => { sandbox .stub(bchjs.Transaction.blockchain, 'getBlockHeader') .resolves({ height: 602405 }) + sandbox.stub(bchjs.Transaction,'getTokenInfo').resolves(false) - const result = await bchjs.Transaction.get2(txid) + const result = await bchjs.Transaction.get3(txid) // console.log(`result: ${JSON.stringify(result, null, 2)}`) // Assert that there are stanardized properties. @@ -305,14 +312,13 @@ describe('#TransactionLib', () => { // 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) // Assert blockheight is added assert.equal(result.blockheight, 602405) + assert.equal(result.isSlpTx, false) }) - it('should get details about a SLP transaction', async () => { + it('should get details about a SLP SEND tx with SEND input', async () => { // Mock dependencies sandbox .stub(bchjs.Transaction.rawTransaction, 'getTxData') @@ -334,7 +340,7 @@ describe('#TransactionLib', () => { const txid = '266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1' - const result = await bchjs.Transaction.get2(txid) + const result = await bchjs.Transaction.get3(txid) // console.log(`result: ${JSON.stringify(result, null, 2)}`) // Assert that there are stanardized properties. @@ -344,19 +350,27 @@ describe('#TransactionLib', () => { 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 outputs have expected properties 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.equal(result.vout[0].tokenQtyStr, null) + assert.equal(result.vout[1].tokenQty, 1) + assert.equal(result.vout[1].tokenQtyStr, "1") + assert.equal(result.vout[2].tokenQty, 998833) + assert.equal(result.vout[2].tokenQtyStr, "998833") + assert.equal(result.vout[3].tokenQty, null) + assert.equal(result.vout[3].tokenQtyStr, null) - // Assert that tokenIds and tokenQty are included in inputs. - assert.property(result.vin[0], 'tokenId') - assert.equal(result.vin[0].tokenQtyStr, '998834') + // Assert that inputs have expected properties + assert.equal(result.vin[0].tokenQtyStr, "998834") + assert.equal(result.vin[0].tokenQty, 998834) + assert.equal(result.vin[0].tokenId, "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7") + assert.equal(result.vin[1].tokenQtyStr, "0") + assert.equal(result.vin[1].tokenQty, 0) + assert.equal(result.vin[1].tokenId, null) // Assert blockheight is added assert.equal(result.blockheight, 603424) + assert.equal(result.isSlpTx, true) }) it('should catch and throw error on network error', async () => { @@ -369,7 +383,7 @@ describe('#TransactionLib', () => { .stub(bchjs.Transaction.rawTransaction, 'getTxData') .rejects(new Error('test error')) - await bchjs.Transaction.get(txid) + await bchjs.Transaction.get3(txid) assert.fail('Unexpected code path') } catch (err) { @@ -380,7 +394,7 @@ describe('#TransactionLib', () => { // 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 () => { + it('should get details about a SLP SEND tx with GENSIS input', async () => { // Mock dependencies sandbox .stub(bchjs.Transaction.rawTransaction, 'getTxData') @@ -402,7 +416,7 @@ describe('#TransactionLib', () => { const txid = '874306bda204d3a5dd15e03ea5732cccdca4c33a52df35162cdd64e30ea7f04e' - const result = await bchjs.Transaction.get2(txid) + const result = await bchjs.Transaction.get3(txid) // console.log(`result: ${JSON.stringify(result, null, 2)}`) // Assert that there are stanardized properties. @@ -412,28 +426,30 @@ describe('#TransactionLib', () => { 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 outputs have expected properties and values 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.equal(result.vout[0].tokenQtyStr, null) + assert.equal(result.vout[1].tokenQty, 5000000) + assert.equal(result.vout[1].tokenQtyStr, "5000000") + assert.equal(result.vout[2].tokenQty, 5000000) + assert.equal(result.vout[2].tokenQtyStr, "5000000") + assert.equal(result.vout[3].tokenQty, null) + assert.equal(result.vout[3].tokenQtyStr, null) - // Assert inputs values unique to a Genesis input have the proper values. + // Assert inputs have expected properties and values assert.equal(result.vin[0].tokenQty, 10000000) - assert.equal(result.vin[1].tokenQty, null) - - // Assert that tokenIds and tokenQty are included in inputs. - assert.property(result.vin[0], 'tokenId') - assert.equal(result.vin[0].tokenQtyStr, '10000000') - assert.equal(result.vin[1].tokenQty, null) - assert.equal(result.vin[1].tokenQtyStr, null) + assert.equal(result.vin[0].tokenQtyStr, "10000000") + assert.equal(result.vin[0].tokenId, "323a1e35ae0b356316093d20f2d9fbc995d19314b5c0148b78dc8d9c0dab9d35") + assert.equal(result.vin[1].tokenQty, 0) + assert.equal(result.vin[1].tokenQtyStr, "0") + assert.equal(result.vin[1].tokenId, null) // Assert blockheight is added assert.equal(result.blockheight, 543409) + assert.equal(result.isSlpTx, true) }) - it('should get input details when input is a mint tx', async () => { + it('should get details about a SLP SEND tx with MINT (and GENESIS) input', async () => { // Mock dependencies sandbox .stub(bchjs.Transaction.rawTransaction, 'getTxData') @@ -457,7 +473,7 @@ describe('#TransactionLib', () => { const txid = '4640a734063ea79fa587a3cac38a70a2f6f3db0011e23514024185982110d0fa' - const result = await bchjs.Transaction.get2(txid) + const result = await bchjs.Transaction.get3(txid) // console.log(`result: ${JSON.stringify(result, null, 2)}`) // Assert that there are stanardized properties. @@ -467,25 +483,31 @@ describe('#TransactionLib', () => { 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 expected output properties and values exist. 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.equal(result.vout[1].tokenQty, 43547.68657) + assert.equal(result.vout[1].tokenQtyStr, "43547.68657") + assert.equal(result.vout[2].tokenQty, null) - // Assert inputs values unique to a Mint input have the proper values. + // Assert expected input properties and values exist. assert.equal(result.vin[0].tokenQty, 43545.34534) + assert.equal(result.vin[0].tokenQtyStr, "43545.34534") + assert.equal(result.vin[0].tokenId, "938cc18e618967d787897bbc64b9a8d201b94ec7c69b1a9949eab0433ba5cdf8") assert.equal(result.vin[1].tokenQty, 2.34123) - assert.equal(result.vin[2].tokenQty, null) + assert.equal(result.vin[1].tokenQtyStr, "2.34123") + assert.equal(result.vin[1].tokenId, "938cc18e618967d787897bbc64b9a8d201b94ec7c69b1a9949eab0433ba5cdf8") + assert.equal(result.vin[2].tokenQty, 0) + assert.equal(result.vin[2].tokenQtyStr, "0") + assert.equal(result.vin[2].tokenId, null) // Assert blockheight is added assert.equal(result.blockheight, 543614) + assert.equal(result.isSlpTx, true) }) // 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 () => { + it('should get details about a SLP SEND tx with MINT (and SEND) input', async () => { // Mock dependencies sandbox .stub(bchjs.Transaction.rawTransaction, 'getTxData') @@ -509,7 +531,7 @@ describe('#TransactionLib', () => { const txid = '6bc111fbf5b118021d68355ca19a0e77fa358dd931f284b2550f79a51ab4792a' - const result = await bchjs.Transaction.get2(txid) + const result = await bchjs.Transaction.get3(txid) // console.log(`result: ${JSON.stringify(result, null, 2)}`) // Assert that there are stanardized properties. @@ -519,23 +541,32 @@ describe('#TransactionLib', () => { 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 the outputs have expected properties and values. 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.equal(result.vout[1].tokenQty, 1000000) + assert.equal(result.vout[1].tokenQtyStr, "1000000") + assert.equal(result.vout[2].tokenQty, 198000000) + assert.equal(result.vout[2].tokenQtyStr, "198000000") + assert.equal(result.vout[3].tokenQty, null) - // Assert inputs values unique to a Mint input have the proper values. + // Assert the inputs have expected properties and values. assert.equal(result.vin[0].tokenQty, 100000000) - assert.equal(result.vin[1].tokenQty, null) + assert.equal(result.vin[0].tokenQtyStr, "100000000") + assert.equal(result.vin[0].tokenId, "550d19eb820e616a54b8a73372c4420b5a0567d8dc00f613b71c5234dc884b35") + assert.equal(result.vin[1].tokenQty, 0) + assert.equal(result.vin[1].tokenQtyStr, 0) + assert.equal(result.vin[1].tokenId, null) assert.equal(result.vin[2].tokenQty, 99000000) + assert.equal(result.vin[2].tokenQtyStr, "99000000") + assert.equal(result.vin[2].tokenId, "550d19eb820e616a54b8a73372c4420b5a0567d8dc00f613b71c5234dc884b35") // Assert blockheight is added assert.equal(result.blockheight, 543957) + assert.equal(result.isSlpTx, true) }) - it('should hydrate problematic tx', async () => { + // This was a problematic TX + it('should process MINT TX with GENESIS input', async () => { // Mock dependencies sandbox .stub(bchjs.Transaction.rawTransaction, 'getTxData') @@ -543,10 +574,10 @@ describe('#TransactionLib', () => { sandbox .stub(bchjs.Transaction.blockchain, 'getBlockHeader') .resolves({ height: 543614 }) - // sandbox - // .stub(bchjs.Transaction.slpUtils, 'decodeOpReturn') - // .onCall(0) - // .resolves(mockData.sendTestOpReturnData01) + sandbox + .stub(bchjs.Transaction.slpUtils, 'decodeOpReturn') + .onCall(0) + .resolves(mockData.mintTestOpReturnData04) // .onCall(1) // .resolves(mockData.sendTestOpReturnData02) // .onCall(2) @@ -559,8 +590,10 @@ describe('#TransactionLib', () => { const txid = 'ee9d3cf5153599c134147e3fac9844c68e216843f4452a1ce15a29452af6db34' - const result = await bchjs.Transaction.get2(txid) + const result = await bchjs.Transaction.get3(txid) console.log(`result: ${JSON.stringify(result, null, 2)}`) }) + + // It should process a GENESIS tx }) })