2021-02-11 19:22:25 -08:00
|
|
|
/*
|
|
|
|
|
High-level functions for working with UTXOs
|
|
|
|
|
|
|
|
|
|
TODO:
|
|
|
|
|
- Make a getWL() clone of get(), but uses hydrateUtxosWL()
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// Local libraries
|
|
|
|
|
const Electrumx = require('./electrumx')
|
|
|
|
|
const Slp = require('./slp/slp')
|
2022-01-18 11:41:52 -08:00
|
|
|
const PsfSlpIndexer = require('./psf-slp-indexer')
|
2022-01-19 10:33:36 -08:00
|
|
|
const BigNumber = require('bignumber.js')
|
2021-02-11 19:22:25 -08:00
|
|
|
|
|
|
|
|
class UTXO {
|
2022-01-18 11:41:52 -08:00
|
|
|
constructor (config = {}) {
|
2021-02-11 19:22:25 -08:00
|
|
|
// Encapsulate dependencies for easier mocking.
|
|
|
|
|
this.electrumx = new Electrumx(config)
|
|
|
|
|
this.slp = new Slp(config)
|
2022-01-18 11:41:52 -08:00
|
|
|
this.psfSlpIndexer = new PsfSlpIndexer(config)
|
2022-01-19 10:33:36 -08:00
|
|
|
this.BigNumber = BigNumber
|
2021-02-11 19:22:25 -08:00
|
|
|
}
|
|
|
|
|
|
2022-01-18 11:41:52 -08:00
|
|
|
/**
|
|
|
|
|
* @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.
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* @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
|
|
|
|
|
|
2022-01-19 14:46:00 -08:00
|
|
|
let slpUtxos = []
|
|
|
|
|
|
2022-01-18 11:41:52 -08:00
|
|
|
// Get SLP UTXOs from the psf-slp-indexer
|
2022-01-19 14:46:00 -08:00
|
|
|
try {
|
|
|
|
|
const slpUtxoData = await this.psfSlpIndexer.balance(addr)
|
|
|
|
|
// console.log(`slpUtxoData: ${JSON.stringify(slpUtxoData, null, 2)}`)
|
|
|
|
|
|
|
|
|
|
slpUtxos = slpUtxoData.balance.utxos
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// console.log('err: ', err)
|
|
|
|
|
|
|
|
|
|
// Exit quietly if address has no SLP UTXOs. Otherwise, throw the error.
|
|
|
|
|
if (err.error && !err.error.includes('Key not found in database')) {
|
|
|
|
|
throw err
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-01-18 11:41:52 -08:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
2022-03-29 15:59:55 -07:00
|
|
|
// tokenQty is property expected by SLP.tokentype1.js library
|
|
|
|
|
thisUtxo.tokenQty = thisUtxo.qyStr
|
|
|
|
|
|
2022-01-18 11:41:52 -08:00
|
|
|
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.address = addr
|
2022-01-31 20:57:36 -08:00
|
|
|
|
|
|
|
|
// Check the transaction to see if its a 'null' token, ignored by
|
|
|
|
|
// the indexer.
|
|
|
|
|
const txData = await this.psfSlpIndexer.tx(thisUtxo.tx_hash)
|
|
|
|
|
// console.log(`txData: ${JSON.stringify(txData, null, 2)}`)
|
|
|
|
|
if (txData.txData.isValidSlp === null) {
|
|
|
|
|
thisUtxo.isSlp = null
|
|
|
|
|
} else {
|
|
|
|
|
thisUtxo.isSlp = false
|
|
|
|
|
}
|
|
|
|
|
// console.log(`thisUtxo.isSlp: ${thisUtxo.isSlp}`)
|
2022-01-18 11:41:52 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-19 10:33:36 -08:00
|
|
|
// Get token UTXOs
|
|
|
|
|
let type1TokenUtxos = utxos.filter(
|
2022-01-18 11:41:52 -08:00
|
|
|
x => x.isSlp === true && x.type === 'token'
|
|
|
|
|
)
|
2022-01-19 10:33:36 -08:00
|
|
|
|
|
|
|
|
// Hydrate the UTXOs with additional token data.
|
|
|
|
|
type1TokenUtxos = await this.hydrateTokenData(type1TokenUtxos)
|
|
|
|
|
|
|
|
|
|
const bchUtxos = utxos.filter(x => x.isSlp === false)
|
2022-01-18 11:41:52 -08:00
|
|
|
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) {
|
2022-01-31 20:57:36 -08:00
|
|
|
console.error('Error in bchjs.Utxo.get(): ', err)
|
2022-01-18 11:41:52 -08:00
|
|
|
|
|
|
|
|
if (err.error) throw new Error(err.error)
|
|
|
|
|
throw err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-19 10:33:36 -08:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-13 14:55:12 -08:00
|
|
|
/**
|
|
|
|
|
* @api Utxo.findBiggestUtxo() findBiggestUtxo()
|
|
|
|
|
* @apiName findBiggestUtxo
|
|
|
|
|
* @apiGroup UTXO
|
|
|
|
|
* @apiDescription Get the biggest UTXO in an array.
|
|
|
|
|
*
|
|
|
|
|
* Given an array of BCH UTXOs, this method will return the biggest UTXO.
|
|
|
|
|
* This is often the simplest way to pick a UTXO for generating a transaction.
|
|
|
|
|
*
|
|
|
|
|
* @apiExample Example usage:
|
|
|
|
|
* (async () => {
|
|
|
|
|
* try {
|
|
|
|
|
* const utxos = await bchjs.Utxo.get('bitcoincash:qq54fgjn3hz0357n8a6guy4demw9xfkjk5jcj0xr0z');
|
|
|
|
|
* const utxo = bchjs.Utxo.findBiggestUtxo(utxos[0].bchUtxos)
|
|
|
|
|
* console.log(utxo);
|
|
|
|
|
* } catch(error) {
|
|
|
|
|
* console.error(error)
|
|
|
|
|
* }
|
|
|
|
|
* })()
|
|
|
|
|
*
|
|
|
|
|
* // returns
|
|
|
|
|
* {
|
|
|
|
|
* "height": 655431,
|
|
|
|
|
* "tx_hash": "7a091716f8137e94f87e7760648cd34a17e32754ef95f7c7bda38a635c9b2b1b",
|
|
|
|
|
* "tx_pos": 0,
|
|
|
|
|
* "value": 800,
|
|
|
|
|
* "txid": "7a091716f8137e94f87e7760648cd34a17e32754ef95f7c7bda38a635c9b2b1b",
|
|
|
|
|
* "vout": 0,
|
|
|
|
|
* "isValid": false,
|
|
|
|
|
* "satoshis": 800
|
|
|
|
|
* }
|
|
|
|
|
*/
|
2021-02-13 14:38:01 -08:00
|
|
|
// Returns the utxo with the biggest balance from an array of utxos.
|
|
|
|
|
findBiggestUtxo (utxos) {
|
|
|
|
|
let largestAmount = 0
|
|
|
|
|
let largestIndex = 0
|
|
|
|
|
|
|
|
|
|
if (!Array.isArray(utxos)) {
|
|
|
|
|
throw new Error('utxos input to findBiggestUtxo() must be an array')
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-13 14:38:37 -08:00
|
|
|
for (let i = 0; i < utxos.length; i++) {
|
2021-02-13 14:38:01 -08:00
|
|
|
const thisUtxo = utxos[i]
|
|
|
|
|
|
|
|
|
|
// Give Elecrumx utxos a satoshis property.
|
|
|
|
|
if (thisUtxo.value) {
|
|
|
|
|
if (!thisUtxo.satoshis) thisUtxo.satoshis = Number(thisUtxo.value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!thisUtxo.satoshis) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
'UTXOs require a satoshis or value property for findBiggestUtxo()'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (thisUtxo.satoshis > largestAmount) {
|
|
|
|
|
largestAmount = thisUtxo.satoshis
|
|
|
|
|
largestIndex = i
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return utxos[largestIndex]
|
|
|
|
|
}
|
2021-02-11 19:22:25 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = UTXO
|