Making progress on Transaction.get3()

This commit is contained in:
Chris Troutner
2021-11-03 07:53:42 -07:00
parent 92960b6957
commit dc85bb4cfe
3 changed files with 332 additions and 59 deletions
+231
View File
@@ -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
+10 -1
View File
@@ -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
}
+91 -58
View File
@@ -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
})
})