Compare commits

..
2 Commits
Author SHA1 Message Date
Chris Troutner 48f7f75c9c Merge pull request #210 from Permissionless-Software-Foundation/ct-unstable
fix(Utxo.get()): Hydrating SLP UTXOs with additional token data
2022-01-19 10:37:45 -08:00
Chris Troutner 0bc96fded5 fix(Utxo.get()): Hydrating SLP UTXOs with additional token data 2022-01-19 10:33:36 -08:00
4 changed files with 251 additions and 4 deletions
+63 -2
View File
@@ -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
@@ -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 () => {
+94 -1
View File
@@ -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
}
+41
View File
@@ -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'