feat(utxo): Added Utxo.get() for easy getting of utxos

This commit is contained in:
Chris Troutner
2021-02-11 19:22:25 -08:00
parent 20e685206d
commit 68bab5dd26
8 changed files with 306 additions and 11 deletions
+2 -3
View File
@@ -16,9 +16,8 @@
"test:integration:local:abc": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 test/integration && mocha --timeout 30000 test/integration/chains/abc/",
"test:integration:local:bchn": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 test/integration/ && mocha --timeout 30000 test/integration/chains/bchn/",
"test:integration:local:testnet": "RESTURL=http://localhost:4000/v4/ mocha --timeout 30000 test/integration/chains/testnet",
"test:temp": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#hydrateUtxos' test/integration/",
"test:temp2": "mocha --timeout=30000 -g '#tokenUtxoDetailsWL' test/unit/",
"test:temp3": "RESTURL=http://localhost:3000/v4/ mocha --timeout 30000 test/integration/chains/abc/",
"test:temp": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#UTXO' test/integration/chains/bchn/",
"test:temp2": "mocha --timeout=30000 -g '#utxo' test/unit/",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"coverage:report": "nyc --reporter=html mocha --timeout 25000 test/unit/",
"docs": "./node_modules/.bin/apidoc -i src/ -o docs",
+2
View File
@@ -31,6 +31,7 @@ const Schnorr = require('./schnorr')
const SLP = require('./slp/slp')
const IPFS = require('./ipfs')
const Encryption = require('./encryption')
const Utxo = require('./utxo')
// Indexers
const Ninsight = require('./ninsight')
@@ -113,6 +114,7 @@ class BCHJS {
this.SLP.HDNode = this.HDNode
this.IPFS = new IPFS()
this.Utxo = new Utxo(libConfig)
}
}
+109
View File
@@ -0,0 +1,109 @@
/*
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')
class UTXO {
constructor (config) {
// Encapsulate dependencies for easier mocking.
this.electrumx = new Electrumx(config)
this.slp = new Slp(config)
}
/**
* @api Utxo.get() get()
* @apiName get
* @apiGroup UTXO
* @apiDescription Get UTXOs for an address
*
* Given an address, this function will return an object with three arrays:
* - bchUtxos - UTXOs confirmed to be spendable as normal BCH
* - slpUtxos - UTXOs confirmed to be colored as valid SLP tokens
* - nullUtxo - UTXOs that did not pass SLP validation. Should be ignored and
* not spent, to be safe.
*
* @apiExample Example usage:
* (async () => {
* try {
* let utxos = await bchjs.Utxo.get('simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9');
* console.log(utxos);
* } catch(error) {
* console.error(error)
* }
* })()
*
* // returns
* {
* "bchUtxos": [
* {
* "height": 674331,
* "tx_hash": "5e86cd911110e4f5db0cc3e8f459d5e8850b49adf57059a71daee674b2867b31",
* "tx_pos": 0,
* "value": 1000,
* "txid": "5e86cd911110e4f5db0cc3e8f459d5e8850b49adf57059a71daee674b2867b31",
* "vout": 0,
* "isValid": false
* }
* ],
* "slpUtxos": [
* {
* "height": 569108,
* "tx_hash": "384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c",
* "tx_pos": 1,
* "value": 546,
* "txid": "384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c",
* "vout": 1,
* "utxoType": "token",
* "transactionType": "send",
* "tokenId": "a436c8e1b6bee3d701c6044d190f76f774be83c36de8d34a988af4489e86dd37",
* "tokenTicker": "sleven",
* "tokenName": "sleven",
* "tokenDocumentUrl": "sleven",
* "tokenDocumentHash": "",
* "decimals": 7,
* "tokenType": 1,
* "tokenQty": "1",
* "isValid": true
* }
* ],
* "nullUtxos": []
* }
*
*
*/
async get (address) {
try {
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)}`)
// Hydate the utxos with token information.
const hydratedUtxos = await this.slp.Utils.hydrateUtxos(utxoData.utxos)
// console.log(`hydratedUtxos: ${JSON.stringify(hydratedUtxos, null, 2)}`)
const retObj = {} // return object
// Filter out the different types of UTXOs.
retObj.bchUtxos = hydratedUtxos.slpUtxos[0].utxos.filter(elem => elem.isValid === false)
retObj.slpUtxos = hydratedUtxos.slpUtxos[0].utxos.filter(elem => elem.isValid === true)
retObj.nullUtxos = hydratedUtxos.slpUtxos[0].utxos.filter(elem => elem.isValid === null)
// Note: true, false, and null should be only values. An element with
// isValid set to any other value should be ignored.
return retObj
} catch (err) {
console.error('Error in bchjs.utxo.get()')
throw err
}
}
}
module.exports = UTXO
@@ -0,0 +1,34 @@
/*
Integration tests for the utxo.js library.
*/
const assert = require('chai').assert
const BCHJS = require('../../../../src/bch-js')
const bchjs = new BCHJS()
describe('#UTXO', () => {
beforeEach(async () => {
// sandbox = sinon.createSandbox()
if (process.env.IS_USING_FREE_TIER) await sleep(1000)
})
describe('#get', () => {
it('should get hydrated and filtered UTXOs for an address', async () => {
// const addr = 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
const result = await bchjs.Utxo.get(addr)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result.bchUtxos)
assert.isArray(result.slpUtxos)
assert.isArray(result.nullUtxos)
})
})
})
function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
@@ -0,0 +1,34 @@
/*
Integration tests for the utxo.js library.
*/
const assert = require('chai').assert
const BCHJS = require('../../../../src/bch-js')
const bchjs = new BCHJS()
describe('#UTXO', () => {
beforeEach(async () => {
// sandbox = sinon.createSandbox()
if (process.env.IS_USING_FREE_TIER) await sleep(1000)
})
describe('#get', () => {
it('should get hydrated and filtered UTXOs for an address', async () => {
// const addr = 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
const result = await bchjs.Utxo.get(addr)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result.bchUtxos)
assert.isArray(result.slpUtxos)
assert.isArray(result.nullUtxos)
})
})
})
function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
+4 -8
View File
@@ -5,11 +5,11 @@ const bchjs = new BCHJS()
const sinon = require('sinon')
describe('#Control', () => {
describe('#getNetworkInfo', () => {
let sandbox
beforeEach(() => (sandbox = sinon.createSandbox()))
afterEach(() => sandbox.restore())
let sandbox
beforeEach(() => (sandbox = sinon.createSandbox()))
afterEach(() => sandbox.restore())
describe('#getNetworkInfo', () => {
it('should get info', done => {
const data = {
version: 170000,
@@ -36,10 +36,6 @@ describe('#Control', () => {
})
describe('#getMemoryInfo', () => {
let sandbox
beforeEach(() => (sandbox = sinon.createSandbox()))
afterEach(() => sandbox.restore())
it('should get memory info', done => {
const data = {
locked: {
+72
View File
@@ -0,0 +1,72 @@
const mockUtxoData = {
success: true,
utxos: [
{
utxos: [
{
height: 569108,
tx_hash:
'384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c',
tx_pos: 1,
value: 546
},
{
height: 674331,
tx_hash:
'5e86cd911110e4f5db0cc3e8f459d5e8850b49adf57059a71daee674b2867b31',
tx_pos: 0,
value: 1000
}
],
address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m'
}
]
}
const mockHydratedUtxos = {
slpUtxos: [
{
utxos: [
{
height: 569108,
tx_hash:
'384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c',
tx_pos: 1,
value: 546,
txid:
'384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c',
vout: 1,
utxoType: 'token',
transactionType: 'send',
tokenId:
'a436c8e1b6bee3d701c6044d190f76f774be83c36de8d34a988af4489e86dd37',
tokenTicker: 'sleven',
tokenName: 'sleven',
tokenDocumentUrl: 'sleven',
tokenDocumentHash: '',
decimals: 7,
tokenType: 1,
tokenQty: '1',
isValid: true
},
{
height: 674331,
tx_hash:
'5e86cd911110e4f5db0cc3e8f459d5e8850b49adf57059a71daee674b2867b31',
tx_pos: 0,
value: 1000,
txid:
'5e86cd911110e4f5db0cc3e8f459d5e8850b49adf57059a71daee674b2867b31',
vout: 0,
isValid: false
}
],
address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m'
}
]
}
module.exports = {
mockUtxoData,
mockHydratedUtxos
}
+49
View File
@@ -0,0 +1,49 @@
/*
Unit tests for the utxo.js library.
*/
const sinon = require('sinon')
const assert = require('chai').assert
const BCHJS = require('../../src/bch-js')
const bchjs = new BCHJS()
const mockData = require('./fixtures/utxo-mocks')
describe('#utxo', () => {
let sandbox
beforeEach(() => (sandbox = sinon.createSandbox()))
afterEach(() => sandbox.restore())
describe('#get', () => {
it('should get hydrated and filtered UTXOs for an address', async () => {
// Mock dependencies.
sandbox.stub(bchjs.Utxo.electrumx, 'utxo').resolves(mockData.mockUtxoData)
sandbox.stub(bchjs.Utxo.slp.Utils, 'hydrateUtxos').resolves(mockData.mockHydratedUtxos)
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
const result = await bchjs.Utxo.get(addr)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result.bchUtxos)
assert.isArray(result.slpUtxos)
assert.isArray(result.nullUtxos)
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox.stub(bchjs.Utxo.electrumx, 'utxo').rejects(new Error('test error'))
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
await bchjs.Utxo.get(addr)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
})