feat(Utxo.get()): Moving old function to getOld. New get() uses psf-slp-indexer

This commit is contained in:
Chris Troutner
2022-01-18 11:41:52 -08:00
parent 6b838ef9cd
commit 1056c3724c
5 changed files with 328 additions and 15 deletions
+1
View File
@@ -132,6 +132,7 @@ class PsfSlpIndexer {
*/ */
async balance (address) { async balance (address) {
try { try {
console.log('balance() address: ', address)
// Handle single address. // Handle single address.
if (typeof address === 'string') { if (typeof address === 'string') {
const response = await axios.post( const response = await axios.post(
+150 -6
View File
@@ -8,19 +8,21 @@
// Local libraries // Local libraries
const Electrumx = require('./electrumx') const Electrumx = require('./electrumx')
const Slp = require('./slp/slp') const Slp = require('./slp/slp')
const PsfSlpIndexer = require('./psf-slp-indexer')
class UTXO { class UTXO {
constructor (config) { constructor (config = {}) {
// Encapsulate dependencies for easier mocking. // Encapsulate dependencies for easier mocking.
this.electrumx = new Electrumx(config) this.electrumx = new Electrumx(config)
this.slp = new Slp(config) this.slp = new Slp(config)
this.psfSlpIndexer = new PsfSlpIndexer(config)
} }
/** /**
* @api Utxo.get() get() * @api Utxo.getOld() getOld()
* @apiName get * @apiName getOld
* @apiGroup UTXO * @apiGroup UTXO
* @apiDescription Get UTXOs for an address * @apiDescription Get UTXOs for an address (from SLPDB)
* *
* Given an address, this function will return an object with thre following * Given an address, this function will return an object with thre following
* properties: * properties:
@@ -45,7 +47,7 @@ class UTXO {
* @apiExample Example usage: * @apiExample Example usage:
* (async () => { * (async () => {
* try { * try {
* let utxos = await bchjs.Utxo.get('simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj'); * let utxos = await bchjs.Utxo.getOld('simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj');
* console.log(utxos); * console.log(utxos);
* } catch(error) { * } catch(error) {
* console.error(error) * console.error(error)
@@ -181,7 +183,7 @@ class UTXO {
* *
* *
*/ */
async get (address, useWhitelist = false) { async getOld (address, useWhitelist = false) {
try { try {
if (!address) { if (!address) {
throw new Error('Address must be an array or a string') throw new Error('Address must be an array or a string')
@@ -286,6 +288,148 @@ class UTXO {
} }
} }
/**
* @api Utxo.get() get()
* @apiName get
* @apiGroup UTXO
* @apiDescription Get UTXOs for an address (from psf-slp-indexer)
*
* Given an address, this function will return an object with thre following
* properties:
* - address: "" - the address these UTXOs are associated with
* - bchUtxos: [] - UTXOs confirmed to be spendable as normal BCH
* - nullUtxo: [] - UTXOs that did not pass SLP validation. Should be ignored and
* not spent, to be safe.
* - slpUtxos: {} - UTXOs confirmed to be colored as valid SLP tokens
* - type1: {}
* - tokens: [] - SLP token Type 1 tokens.
* - mintBatons: [] - SLP token Type 1 mint batons.
* - nft: {}
* - tokens: [] - NFT tokens
* - groupTokens: [] - NFT Group tokens, used to create NFT tokens.
* - groupMintBatons: [] - Minting baton to create more NFT Group tokens.
*
* Note: You can pass in an optional second Boolean argument. The default
* `false` will use the normal waterfall validation method. Set to `true`,
* SLP UTXOs will be validated with the whitelist filtered SLPDB. This will
* result is many more UTXOs in the `nullUtxos` array.
*
* @apiExample Example usage:
* (async () => {
* try {
* let utxos = await bchjs.Utxo.get('simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj');
* console.log(utxos);
* } catch(error) {
* console.error(error)
* }
* })()
*
* // returns
* [
* {
* "address": "bitcoincash:qrm0c67wwqh0w7wjxua2gdt2xggnm90xws00a3lezv",
* "bchUtxos": [
* {
* "height": 674513,
* "tx_hash": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04",
* "tx_pos": 3,
* "value": 38134,
* "txid": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04",
* "vout": 3,
* "isValid": false
* }
* ],
*/
// This version of get() uses the psf-slp-indexer. It will replace the older
// get() function that uses SLPDB.
// TODO: NFT UTXOs are identified as non-token UTXOs, which will cause a wallet
// to burn them. The psf-slp-indexer needs to be updated to mark these UTXOs.
async get (address) {
try {
// Convert address to an array if it is a string.
if (typeof address !== 'string') {
throw new Error('address input must be a string')
}
// Ensure the address is a BCH address.
const addr = this.slp.Address.toCashAddress(address)
// Get the UTXOs associated with the address.
const utxoData = await this.electrumx.utxo(addr)
// console.log(`utxoData: ${JSON.stringify(utxoData, null, 2)}`)
const utxos = utxoData.utxos
// Get SLP UTXOs from the psf-slp-indexer
const slpUtxoData = await this.psfSlpIndexer.balance(addr)
// console.log(`slpUtxoData: ${JSON.stringify(slpUtxoData, null, 2)}`)
const slpUtxos = slpUtxoData.balance.utxos
// Loop through the Fulcrum UTXOs.
for (let i = 0; i < utxos.length; i++) {
const thisUtxo = utxos[i]
// Loop through the UTXOs from psf-slp-indexer.
for (let j = 0; j < slpUtxos.length; j++) {
const thisSlpUtxo = slpUtxos[j]
// If the non-hydrated UTXO matches the SLP UTXO, then combine the data
// and mark the UTXO as an SLP token.
if (
thisUtxo.tx_hash === thisSlpUtxo.txid &&
thisUtxo.tx_pos === thisSlpUtxo.vout
) {
thisUtxo.txid = thisUtxo.tx_hash
thisUtxo.vout = thisUtxo.tx_pos
thisUtxo.isSlp = true
thisUtxo.type = thisSlpUtxo.type
thisUtxo.qty = thisSlpUtxo.qty
thisUtxo.tokenId = thisSlpUtxo.tokenId
thisUtxo.address = thisSlpUtxo.address
break
}
}
// If there was no match, then this is a normal BCH UTXO. Mark it as such.
if (!thisUtxo.isSlp) {
thisUtxo.txid = thisUtxo.tx_hash
thisUtxo.vout = thisUtxo.tx_pos
thisUtxo.isSlp = false
thisUtxo.address = addr
}
}
const bchUtxos = utxos.filter(x => x.isSlp === false)
const type1TokenUtxos = utxos.filter(
x => x.isSlp === true && x.type === 'token'
)
const type1BatonUtxos = utxos.filter(
x => x.isSlp === true && x.type === 'baton'
)
const nullUtxos = utxos.filter(x => x.isSlp === null)
const outObj = {
address: addr,
bchUtxos,
slpUtxos: {
type1: {
tokens: type1TokenUtxos,
mintBatons: type1BatonUtxos
},
nft: {} // Allocated for future support of NFT spec.
},
nullUtxos
}
return outObj
} catch (err) {
// console.error('Error in bchjs.utxo.get2(): ', err)
if (err.error) throw new Error(err.error)
throw err
}
}
/** /**
* @api Utxo.findBiggestUtxo() findBiggestUtxo() * @api Utxo.findBiggestUtxo() findBiggestUtxo()
* @apiName findBiggestUtxo * @apiName findBiggestUtxo
@@ -13,7 +13,7 @@ describe('#UTXO', () => {
if (process.env.IS_USING_FREE_TIER) await sleep(3000) if (process.env.IS_USING_FREE_TIER) await sleep(3000)
}) })
/*
describe('#get', () => { describe('#get', () => {
it('should get hydrated and filtered UTXOs for an address', async () => { it('should get hydrated and filtered UTXOs for an address', async () => {
// const addr = 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9' // const addr = 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
@@ -90,7 +90,40 @@ describe('#UTXO', () => {
assert.isAbove(result[0].nullUtxos.length, 1) assert.isAbove(result[0].nullUtxos.length, 1)
}) })
}) })
*/
describe('#get', () => {
it('should hydrate address with BCH and SLP UTXOs', async () => {
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
const result = await bchjs.Utxo.get(addr)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
// Assert expected properties exist.
assert.property(result, 'address')
assert.property(result, 'bchUtxos')
assert.property(result, 'slpUtxos')
assert.property(result.slpUtxos, 'type1')
assert.property(result.slpUtxos, 'nft')
assert.property(result, 'nullUtxos')
assert.isAbove(result.bchUtxos.length, 0)
assert.isAbove(result.slpUtxos.type1.tokens.length, 0)
assert.equal(result.slpUtxos.type1.mintBatons.length, 0)
})
// TODO: NFTs are currently not identified as different than normal BCH UTXOs.
// The psf-slp-indexer needs to be updated to fix this issue.
it('should handle NFTs and minting batons', async () => {
const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj'
const result = await bchjs.Utxo.get(addr)
// 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', () => { describe('#findBiggestUtxo', () => {
it('should sort UTXOs from Electrumx', async () => { it('should sort UTXOs from Electrumx', async () => {
const addr = 'bitcoincash:qq54fgjn3hz0357n8a6guy4demw9xfkjk5jcj0xr0z' const addr = 'bitcoincash:qq54fgjn3hz0357n8a6guy4demw9xfkjk5jcj0xr0z'
@@ -118,6 +151,7 @@ describe('#UTXO', () => {
assert.equal(result.satoshis, 800) assert.equal(result.satoshis, 800)
}) })
}) })
*/
}) })
function sleep (ms) { function sleep (ms) {
+91 -1
View File
@@ -280,10 +280,100 @@ const electrumxUtxos = {
] ]
} }
const fulcrumUtxos01 = {
success: true,
utxos: [
{
height: 674512,
tx_hash:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
tx_pos: 1,
value: 546
},
{
height: 674512,
tx_hash:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
tx_pos: 2,
value: 546
},
{
height: 674512,
tx_hash:
'eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b',
tx_pos: 1,
value: 546
},
{
height: 674513,
tx_hash:
'705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04',
tx_pos: 1,
value: 546
},
{
height: 674513,
tx_hash:
'705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04',
tx_pos: 2,
value: 546
},
{
height: 674513,
tx_hash:
'705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04',
tx_pos: 3,
value: 38134
}
]
}
const psfSlpIndexerUtxos01 = {
balance: {
utxos: [
{
txid:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
vout: 1,
type: 'token',
qty: '10000000000',
tokenId:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
address: 'bitcoincash:qrm0c67wwqh0w7wjxua2gdt2xggnm90xws00a3lezv'
},
{
txid:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
vout: 2,
type: 'baton',
tokenId:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
address: 'bitcoincash:qrm0c67wwqh0w7wjxua2gdt2xggnm90xws00a3lezv'
}
],
txs: [
{
txid:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
height: 674512
}
],
balances: [
{
tokenId:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
qty: '10000000000'
}
]
}
}
module.exports = { module.exports = {
mockUtxoData, mockUtxoData,
mockHydratedUtxos, mockHydratedUtxos,
mockTwoHydratedAddrs, mockTwoHydratedAddrs,
mockEveryUtxoType, mockEveryUtxoType,
electrumxUtxos electrumxUtxos,
fulcrumUtxos01,
psfSlpIndexerUtxos01
} }
+51 -7
View File
@@ -15,7 +15,7 @@ describe('#utxo', () => {
beforeEach(() => (sandbox = sinon.createSandbox())) beforeEach(() => (sandbox = sinon.createSandbox()))
afterEach(() => sandbox.restore()) afterEach(() => sandbox.restore())
describe('#get', () => { describe('#getOld', () => {
it('should get hydrated and filtered UTXOs for an address', async () => { it('should get hydrated and filtered UTXOs for an address', async () => {
// Mock dependencies. // Mock dependencies.
sandbox.stub(bchjs.Utxo.electrumx, 'utxo').resolves(mockData.mockUtxoData) sandbox.stub(bchjs.Utxo.electrumx, 'utxo').resolves(mockData.mockUtxoData)
@@ -25,7 +25,7 @@ describe('#utxo', () => {
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
const result = await bchjs.Utxo.get(addr) const result = await bchjs.Utxo.getOld(addr)
// console.log(`result: ${JSON.stringify(result, null, 2)}`) // console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result) assert.isArray(result)
@@ -46,7 +46,7 @@ describe('#utxo', () => {
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
await bchjs.Utxo.get(addr) await bchjs.Utxo.getOld(addr)
assert.fail('Unexpected code path') assert.fail('Unexpected code path')
} catch (err) { } catch (err) {
@@ -66,7 +66,7 @@ describe('#utxo', () => {
'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9' 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
] ]
const result = await bchjs.Utxo.get(addr) const result = await bchjs.Utxo.getOld(addr)
// console.log(`result: ${JSON.stringify(result, null, 2)}`) // console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result) assert.isArray(result)
@@ -88,7 +88,7 @@ describe('#utxo', () => {
addrs.push(addr) addrs.push(addr)
} }
await bchjs.Utxo.get(addrs) await bchjs.Utxo.getOld(addrs)
assert.fail('Unexpected code path') assert.fail('Unexpected code path')
} catch (err) { } catch (err) {
@@ -105,7 +105,7 @@ describe('#utxo', () => {
const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj' const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj'
const result = await bchjs.Utxo.get(addr) const result = await bchjs.Utxo.getOld(addr)
// console.log(`result: ${JSON.stringify(result, null, 2)}`) // console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result) assert.isArray(result)
@@ -133,7 +133,7 @@ describe('#utxo', () => {
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9' const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
const useWhitelist = true const useWhitelist = true
const result = await bchjs.Utxo.get(addr, useWhitelist) const result = await bchjs.Utxo.getOld(addr, useWhitelist)
// console.log(`result: ${JSON.stringify(result, null, 2)}`) // console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result) assert.isArray(result)
@@ -188,4 +188,48 @@ describe('#utxo', () => {
assert.equal(result.satoshis, 800) assert.equal(result.satoshis, 800)
}) })
}) })
describe('#get', () => {
it('should throw an error if input is not a string', async () => {
try {
const addr = 123
await bchjs.Utxo.get(addr)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'address input must be a string')
}
})
it('should return UTXO information', async () => {
// mock dependencies
sandbox
.stub(bchjs.Utxo.electrumx, 'utxo')
.resolves(mockData.fulcrumUtxos01)
sandbox
.stub(bchjs.Utxo.psfSlpIndexer, 'balance')
.resolves(mockData.psfSlpIndexerUtxos01)
const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj'
const result = await bchjs.Utxo.get(addr)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
// Assert expected properties exist
assert.property(result, 'address')
assert.property(result, 'bchUtxos')
assert.property(result, 'slpUtxos')
assert.property(result.slpUtxos, 'type1')
assert.property(result.slpUtxos.type1, 'tokens')
assert.property(result.slpUtxos.type1, 'mintBatons')
assert.property(result.slpUtxos, 'nft')
assert.property(result, 'nullUtxos')
assert.equal(result.bchUtxos.length, 4)
assert.equal(result.slpUtxos.type1.tokens.length, 1)
assert.equal(result.slpUtxos.type1.mintBatons.length, 1)
assert.equal(result.nullUtxos.length, 0)
})
})
}) })