diff --git a/src/utxo.js b/src/utxo.js index a92acdb..c2a50c6 100644 --- a/src/utxo.js +++ b/src/utxo.js @@ -10,6 +10,7 @@ const Electrumx = require('./electrumx') const Slp = require('./slp/slp') const PsfSlpIndexer = require('./psf-slp-indexer') const BigNumber = require('bignumber.js') +const Blockchain = require('./blockchain') class UTXO { constructor (config = {}) { @@ -18,6 +19,7 @@ class UTXO { this.slp = new Slp(config) this.psfSlpIndexer = new PsfSlpIndexer(config) this.BigNumber = BigNumber + this.blockchain = new Blockchain(config) } /** @@ -207,7 +209,7 @@ class UTXO { return outObj } catch (err) { - console.error('Error in bchjs.Utxo.get(): ', err) + console.error('Error in bchjs.Utxo.get()') if (err.error) throw new Error(err.error) throw err @@ -336,6 +338,57 @@ class UTXO { return utxos[largestIndex] } + + /** + * @api Utxo.isValid() isValid() + * @apiName isValid + * @apiGroup UTXO + * @apiDescription Validate that UTXO exists and is still spendable. + * + * Given a UTXO, this method will return true if the UTXO is still in the + * mempool and still valid for spending. It will return false if the UTXO + * has been spent. + * + * @apiExample Example usage: + * (async () => { + * try { + * const utxos = await bchjs.Utxo.get('bitcoincash:qq54fgjn3hz0357n8a6guy4demw9xfkjk5jcj0xr0z'); + * const isValid = bchjs.Utxo.isValid(utxos.bchUtxos[0]) + * console.log(isValid); + * } catch(error) { + * console.error(error) + * } + * })() + * + * // returns + * true + */ + async isValid (utxo) { + try { + // console.log('utxo: ', utxo) + + // Convert different properties from different indexers + const txid = utxo.txid || utxo.tx_hash + const vout = utxo.vout | utxo.tx_pos + + // Query the full node + const txOut = await this.blockchain.getTxOut(txid, vout, true) + // console.log('txOut: ', txOut) + + // Simplify results to either true or false. + let isValid = null + if (txOut === null) { + isValid = false + } else { + isValid = true + } + + return isValid + } catch (err) { + console.log('Error in Utxo.isValid()') + throw err + } + } } module.exports = UTXO diff --git a/test/integration/chains/bchn/utxo-integration.js b/test/integration/chains/bchn/utxo-integration.js index 647fc2f..2f550f6 100644 --- a/test/integration/chains/bchn/utxo-integration.js +++ b/test/integration/chains/bchn/utxo-integration.js @@ -122,6 +122,65 @@ describe('#UTXO', () => { assert.isAbove(result.slpUtxos.nft.tokens.length, 0) }) }) + + describe('#isValid', () => { + it('should return true for valid UTXO with fullnode properties', async () => { + const utxo = { + txid: 'b94e1ff82eb5781f98296f0af2488ff06202f12ee92b0175963b8dba688d1b40', + vout: 0 + } + + const result = await bchjs.Utxo.isValid(utxo) + // console.log('result: ', result) + + assert.equal(result, true) + }) + + it('should return true for valid UTXO with fulcrum properties', async () => { + const utxo = { + tx_hash: 'b94e1ff82eb5781f98296f0af2488ff06202f12ee92b0175963b8dba688d1b40', + tx_pos: 0 + } + + const result = await bchjs.Utxo.isValid(utxo) + // console.log('result: ', result) + + assert.equal(result, true) + }) + + it('should return true for valid UTXO with fullnode properties', async () => { + const utxo = { + txid: '17754221b29f189532d4fc2ae89fb467ad2dede30fdec4854eb2129b3ba90d7a', + vout: 0 + } + + const result = await bchjs.Utxo.isValid(utxo) + // console.log('result: ', result) + + assert.equal(result, false) + }) + + it('should return true for valid UTXO with fulcrum properties', async () => { + const utxo = { + tx_hash: '17754221b29f189532d4fc2ae89fb467ad2dede30fdec4854eb2129b3ba90d7a', + tx_pos: 0 + } + + const result = await bchjs.Utxo.isValid(utxo) + // console.log('result: ', result + + assert.equal(result, false) + }) + + it('should process output of Utxo.get()', async () => { + const utxo = await bchjs.Utxo.get('bitcoincash:qr4yscpw9jgq8ltajfeknpj32kamkf9knujffcdhyq') + // console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`) + + const result = await bchjs.Utxo.isValid(utxo.bchUtxos[0]) + + assert.equal(result, true) + }) + }) }) function sleep (ms) { diff --git a/test/unit/utxo-unit.js b/test/unit/utxo-unit.js index 3fd3ad3..aab01ef 100644 --- a/test/unit/utxo-unit.js +++ b/test/unit/utxo-unit.js @@ -174,4 +174,36 @@ describe('#utxo', () => { assert.equal(result.nullUtxos.length, 0) }) }) + + describe('#isValid', () => { + it('should return false if getTxOut() returns null', async () => { + // Mock dependencies + sandbox.stub(bchjs.Utxo.blockchain, 'getTxOut').resolves(null) + + const utxo = { + tx_hash: '17754221b29f189532d4fc2ae89fb467ad2dede30fdec4854eb2129b3ba90d7a', + tx_pos: 0 + } + + const result = await bchjs.Utxo.isValid(utxo) + // console.log('result: ', result + + assert.equal(result, false) + }) + + it('should return true if getTxOut() returns non-null output', async () => { + // Mock dependencies + sandbox.stub(bchjs.Utxo.blockchain, 'getTxOut').resolves({ a: 'b' }) + + const utxo = { + tx_hash: 'b94e1ff82eb5781f98296f0af2488ff06202f12ee92b0175963b8dba688d1b40', + tx_pos: 0 + } + + const result = await bchjs.Utxo.isValid(utxo) + // console.log('result: ', result + + assert.equal(result, true) + }) + }) })