From 0bc96fded5438115874fe9f972da999d9f0cb84c Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 19 Jan 2022 10:33:36 -0800 Subject: [PATCH] fix(Utxo.get()): Hydrating SLP UTXOs with additional token data --- src/utxo.js | 65 ++++++++++++- .../chains/bchn/utxo-integration.js | 54 ++++++++++- test/unit/fixtures/utxo-mocks.js | 95 ++++++++++++++++++- test/unit/utxo-unit.js | 41 ++++++++ 4 files changed, 251 insertions(+), 4 deletions(-) diff --git a/src/utxo.js b/src/utxo.js index ad21d57..8d1332d 100644 --- a/src/utxo.js +++ b/src/utxo.js @@ -9,6 +9,7 @@ const Electrumx = require('./electrumx') const Slp = require('./slp/slp') const PsfSlpIndexer = require('./psf-slp-indexer') +const BigNumber = require('bignumber.js') class UTXO { constructor (config = {}) { @@ -16,6 +17,7 @@ class UTXO { this.electrumx = new Electrumx(config) this.slp = new Slp(config) this.psfSlpIndexer = new PsfSlpIndexer(config) + this.BigNumber = BigNumber } /** @@ -399,10 +401,15 @@ class UTXO { } } - const bchUtxos = utxos.filter(x => x.isSlp === false) - const type1TokenUtxos = utxos.filter( + // Get token UTXOs + let type1TokenUtxos = utxos.filter( x => x.isSlp === true && x.type === 'token' ) + + // Hydrate the UTXOs with additional token data. + type1TokenUtxos = await this.hydrateTokenData(type1TokenUtxos) + + const bchUtxos = utxos.filter(x => x.isSlp === false) const type1BatonUtxos = utxos.filter( x => x.isSlp === true && x.type === 'baton' ) @@ -430,6 +437,60 @@ class UTXO { } } + // Hydrate an array of token UTXOs with token information. + // Returns an array of token UTXOs with additional data. + async hydrateTokenData (utxoAry) { + try { + // console.log('utxoAry: ', utxoAry) + + // Create a list of token IDs without duplicates. + let tokenIds = utxoAry.map(x => x.tokenId) + + // Remove duplicates. https://stackoverflow.com/questions/9229645/remove-duplicate-values-from-js-array + tokenIds = [...new Set(tokenIds)] + // console.log('tokenIds: ', tokenIds) + + // Get Genesis data for each tokenId + const genesisData = [] + for (let i = 0; i < tokenIds.length; i++) { + const thisTokenId = tokenIds[i] + const thisTokenData = await this.psfSlpIndexer.tokenStats(thisTokenId) + // console.log('thisTokenData: ', thisTokenData) + + genesisData.push(thisTokenData) + } + // console.log('genesisData: ', genesisData) + + // Hydrate each token UTXO with data from the genesis transaction. + for (let i = 0; i < utxoAry.length; i++) { + const thisUtxo = utxoAry[i] + + // Get the genesis data for this token. + const genData = genesisData.filter( + x => x.tokenData.tokenId === thisUtxo.tokenId + ) + // console.log('genData: ', genData) + + thisUtxo.ticker = genData[0].tokenData.ticker + thisUtxo.name = genData[0].tokenData.name + thisUtxo.documentUri = genData[0].tokenData.documentUri + thisUtxo.documentHash = genData[0].tokenData.documentHash + thisUtxo.decimals = genData[0].tokenData.decimals + + // Calculate the real token quantity + const qty = new BigNumber(thisUtxo.qty).dividedBy( + 10 ** parseInt(thisUtxo.decimals) + ) + thisUtxo.qtyStr = qty.toString() + } + + return utxoAry + } catch (err) { + console.log('Error in hydrateTokenData()') + throw err + } + } + /** * @api Utxo.findBiggestUtxo() findBiggestUtxo() * @apiName findBiggestUtxo diff --git a/test/integration/chains/bchn/utxo-integration.js b/test/integration/chains/bchn/utxo-integration.js index 1a2e3a0..65e9e32 100644 --- a/test/integration/chains/bchn/utxo-integration.js +++ b/test/integration/chains/bchn/utxo-integration.js @@ -91,6 +91,57 @@ describe('#UTXO', () => { }) }) */ + + describe('#hydrateTokenData', () => { + it('should hydrate token UTXOs', async () => { + const utxos = [ + { + txid: + '384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c', + vout: 1, + type: 'token', + qty: '10000000', + tokenId: + 'a436c8e1b6bee3d701c6044d190f76f774be83c36de8d34a988af4489e86dd37', + address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m' + }, + { + txid: + '4fc789405d58ec612c69eba29aa56cf0c7f228349801114138424eb68df4479d', + vout: 1, + type: 'token', + qty: '100000000', + tokenId: + 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', + address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m' + }, + { + txid: + '42054bba4d69bfe7801ece0cffc754194b04239034fdfe9dbe321ef76c9a2d93', + vout: 5, + type: 'token', + qty: '4764', + tokenId: + 'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f', + address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m' + }, + { + txid: + '06938d0a0d15aa76524ffe61fe111d6d2b2ea9dd8dcd4c7c7744614ced370861', + vout: 5, + type: 'token', + qty: '238', + tokenId: + 'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f', + address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m' + } + ] + + const result = await bchjs.Utxo.hydrateTokenData(utxos) + console.log('result: ', result) + }) + }) + describe('#get', () => { it('should hydrate address with BCH and SLP UTXOs', async () => { const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' @@ -117,12 +168,13 @@ describe('#UTXO', () => { const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj' const result = await bchjs.Utxo.get(addr) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) + console.log(`result: ${JSON.stringify(result, null, 2)}`) // Assert that minting batons are correctly identified. assert.isAbove(result.slpUtxos.type1.mintBatons.length, 0) }) }) + /* describe('#findBiggestUtxo', () => { it('should sort UTXOs from Electrumx', async () => { diff --git a/test/unit/fixtures/utxo-mocks.js b/test/unit/fixtures/utxo-mocks.js index df49eae..06a7488 100644 --- a/test/unit/fixtures/utxo-mocks.js +++ b/test/unit/fixtures/utxo-mocks.js @@ -368,6 +368,95 @@ const psfSlpIndexerUtxos01 = { } } +const tokenUtxos01 = [ + { + txid: '384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c', + vout: 1, + type: 'token', + qty: '10000000', + tokenId: 'a436c8e1b6bee3d701c6044d190f76f774be83c36de8d34a988af4489e86dd37', + address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m' + }, + { + txid: '4fc789405d58ec612c69eba29aa56cf0c7f228349801114138424eb68df4479d', + vout: 1, + type: 'token', + qty: '100000000', + tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', + address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m' + }, + { + txid: '42054bba4d69bfe7801ece0cffc754194b04239034fdfe9dbe321ef76c9a2d93', + vout: 5, + type: 'token', + qty: '4764', + tokenId: 'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f', + address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m' + }, + { + txid: '06938d0a0d15aa76524ffe61fe111d6d2b2ea9dd8dcd4c7c7744614ced370861', + vout: 5, + type: 'token', + qty: '238', + tokenId: 'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f', + address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m' + } +] + +const genesisData01 = { + tokenData: { + type: 1, + ticker: 'sleven', + name: 'sleven', + tokenId: 'a436c8e1b6bee3d701c6044d190f76f774be83c36de8d34a988af4489e86dd37', + documentUri: 'sleven', + documentHash: '', + decimals: 7, + mintBatonIsActive: false, + tokensInCirculationBN: '770059999999', + tokensInCirculationStr: '770059999999', + blockCreated: 555483, + totalBurned: '7711234568', + totalMinted: '777771234567' + } +} + +const genesisData02 = { + tokenData: { + type: 1, + ticker: 'NAKAMOTO', + name: 'NAKAMOTO', + tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', + documentUri: '', + documentHash: '', + decimals: 8, + mintBatonIsActive: false, + tokensInCirculationBN: '2099260968799900', + tokensInCirculationStr: '2099260968799900', + blockCreated: 555671, + totalBurned: '739031200100', + totalMinted: '2100000000000000' + } +} + +const genesisData03 = { + tokenData: { + type: 1, + ticker: 'AUDC', + name: 'AUD Coin', + tokenId: 'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f', + documentUri: 'audcoino@gmail.com', + documentHash: '', + decimals: 6, + mintBatonIsActive: false, + tokensInCirculationBN: '974791786216512742', + tokensInCirculationStr: '974791786216512742', + blockCreated: 603311, + totalBurned: '1025208213783487258', + totalMinted: '2000000000000000000' + } +} + module.exports = { mockUtxoData, mockHydratedUtxos, @@ -375,5 +464,9 @@ module.exports = { mockEveryUtxoType, electrumxUtxos, fulcrumUtxos01, - psfSlpIndexerUtxos01 + psfSlpIndexerUtxos01, + tokenUtxos01, + genesisData01, + genesisData02, + genesisData03 } diff --git a/test/unit/utxo-unit.js b/test/unit/utxo-unit.js index e4d3b63..7253283 100644 --- a/test/unit/utxo-unit.js +++ b/test/unit/utxo-unit.js @@ -189,6 +189,45 @@ describe('#utxo', () => { }) }) + describe('#hydrateTokenData', () => { + it('should hydrate token UTXOs', async () => { + // Mock dependencies + sandbox + .stub(bchjs.Utxo.psfSlpIndexer, 'tokenStats') + .onCall(0) + .resolves(mockData.genesisData01) + .onCall(1) + .resolves(mockData.genesisData02) + .onCall(2) + .resolves(mockData.genesisData03) + + const result = await bchjs.Utxo.hydrateTokenData(mockData.tokenUtxos01) + // console.log('result: ', result) + + assert.equal(result.length, 4) + assert.property(result[0], 'qtyStr') + assert.property(result[0], 'ticker') + assert.property(result[0], 'name') + assert.property(result[0], 'documentUri') + assert.property(result[0], 'documentHash') + }) + + it('should should catch and throw errors', async () => { + try { + // Force error + sandbox + .stub(bchjs.Utxo.psfSlpIndexer, 'tokenStats') + .rejects(new Error('test error')) + + await bchjs.Utxo.hydrateTokenData(mockData.tokenUtxos01) + + assert.fail('Unexpected code path') + } catch (err) { + assert.equal(err.message, 'test error') + } + }) + }) + describe('#get', () => { it('should throw an error if input is not a string', async () => { try { @@ -210,6 +249,8 @@ describe('#utxo', () => { sandbox .stub(bchjs.Utxo.psfSlpIndexer, 'balance') .resolves(mockData.psfSlpIndexerUtxos01) + // Mock function to return the same input. Good enough for this test. + sandbox.stub(bchjs.Utxo, 'hydrateTokenData').resolves(x => x) const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj'