mirror of
https://github.com/Permissionless-Software-Foundation/bch-js.git
synced 2026-09-21 16:51:59 -07:00
feat(Utxo.get()): Moving old function to getOld. New get() uses psf-slp-indexer
This commit is contained in:
@@ -132,6 +132,7 @@ class PsfSlpIndexer {
|
||||
*/
|
||||
async balance (address) {
|
||||
try {
|
||||
console.log('balance() address: ', address)
|
||||
// Handle single address.
|
||||
if (typeof address === 'string') {
|
||||
const response = await axios.post(
|
||||
|
||||
+150
-6
@@ -8,19 +8,21 @@
|
||||
// Local libraries
|
||||
const Electrumx = require('./electrumx')
|
||||
const Slp = require('./slp/slp')
|
||||
const PsfSlpIndexer = require('./psf-slp-indexer')
|
||||
|
||||
class UTXO {
|
||||
constructor (config) {
|
||||
constructor (config = {}) {
|
||||
// Encapsulate dependencies for easier mocking.
|
||||
this.electrumx = new Electrumx(config)
|
||||
this.slp = new Slp(config)
|
||||
this.psfSlpIndexer = new PsfSlpIndexer(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* @api Utxo.get() get()
|
||||
* @apiName get
|
||||
* @api Utxo.getOld() getOld()
|
||||
* @apiName getOld
|
||||
* @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
|
||||
* properties:
|
||||
@@ -45,7 +47,7 @@ class UTXO {
|
||||
* @apiExample Example usage:
|
||||
* (async () => {
|
||||
* try {
|
||||
* let utxos = await bchjs.Utxo.get('simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj');
|
||||
* let utxos = await bchjs.Utxo.getOld('simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj');
|
||||
* console.log(utxos);
|
||||
* } catch(error) {
|
||||
* console.error(error)
|
||||
@@ -181,7 +183,7 @@ class UTXO {
|
||||
*
|
||||
*
|
||||
*/
|
||||
async get (address, useWhitelist = false) {
|
||||
async getOld (address, useWhitelist = false) {
|
||||
try {
|
||||
if (!address) {
|
||||
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()
|
||||
* @apiName findBiggestUtxo
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('#UTXO', () => {
|
||||
|
||||
if (process.env.IS_USING_FREE_TIER) await sleep(3000)
|
||||
})
|
||||
|
||||
/*
|
||||
describe('#get', () => {
|
||||
it('should get hydrated and filtered UTXOs for an address', async () => {
|
||||
// const addr = 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
|
||||
@@ -90,7 +90,40 @@ describe('#UTXO', () => {
|
||||
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', () => {
|
||||
it('should sort UTXOs from Electrumx', async () => {
|
||||
const addr = 'bitcoincash:qq54fgjn3hz0357n8a6guy4demw9xfkjk5jcj0xr0z'
|
||||
@@ -118,6 +151,7 @@ describe('#UTXO', () => {
|
||||
assert.equal(result.satoshis, 800)
|
||||
})
|
||||
})
|
||||
*/
|
||||
})
|
||||
|
||||
function sleep (ms) {
|
||||
|
||||
@@ -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 = {
|
||||
mockUtxoData,
|
||||
mockHydratedUtxos,
|
||||
mockTwoHydratedAddrs,
|
||||
mockEveryUtxoType,
|
||||
electrumxUtxos
|
||||
electrumxUtxos,
|
||||
fulcrumUtxos01,
|
||||
psfSlpIndexerUtxos01
|
||||
}
|
||||
|
||||
+51
-7
@@ -15,7 +15,7 @@ describe('#utxo', () => {
|
||||
beforeEach(() => (sandbox = sinon.createSandbox()))
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#get', () => {
|
||||
describe('#getOld', () => {
|
||||
it('should get hydrated and filtered UTXOs for an address', async () => {
|
||||
// Mock dependencies.
|
||||
sandbox.stub(bchjs.Utxo.electrumx, 'utxo').resolves(mockData.mockUtxoData)
|
||||
@@ -25,7 +25,7 @@ describe('#utxo', () => {
|
||||
|
||||
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)}`)
|
||||
|
||||
assert.isArray(result)
|
||||
@@ -46,7 +46,7 @@ describe('#utxo', () => {
|
||||
|
||||
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
|
||||
|
||||
await bchjs.Utxo.get(addr)
|
||||
await bchjs.Utxo.getOld(addr)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
@@ -66,7 +66,7 @@ describe('#utxo', () => {
|
||||
'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
|
||||
]
|
||||
|
||||
const result = await bchjs.Utxo.get(addr)
|
||||
const result = await bchjs.Utxo.getOld(addr)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
assert.isArray(result)
|
||||
@@ -88,7 +88,7 @@ describe('#utxo', () => {
|
||||
addrs.push(addr)
|
||||
}
|
||||
|
||||
await bchjs.Utxo.get(addrs)
|
||||
await bchjs.Utxo.getOld(addrs)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
@@ -105,7 +105,7 @@ describe('#utxo', () => {
|
||||
|
||||
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)}`)
|
||||
|
||||
assert.isArray(result)
|
||||
@@ -133,7 +133,7 @@ describe('#utxo', () => {
|
||||
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
|
||||
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)}`)
|
||||
|
||||
assert.isArray(result)
|
||||
@@ -188,4 +188,48 @@ describe('#utxo', () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user