Compare commits

...
10 Commits
14 changed files with 847 additions and 334 deletions
+5 -5
View File
@@ -13,11 +13,11 @@
"test:integration:abc": "export RESTURL=https://abc.fullstack.cash/v4/ && export IS_USING_FREE_TIER=true && mocha --timeout 30000 test/integration/ && mocha --timeout 30000 test/integration/chains/abc/",
"test:integration:bchn": "export RESTURL=https://bchn.fullstack.cash/v4/ && export IS_USING_FREE_TIER=true && mocha --timeout 30000 test/integration/ && mocha --timeout 30000 test/integration/chains/bchn/",
"test:integration:testnet": "IS_USING_FREE_TIER=true mocha --timeout 30000 test/integration/chains/testnet/",
"test:integration:local": "RESTURL=http://localhost:3000/v4/ mocha --timeout 30000 test/integration",
"test:integration:local:bchn": "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/testnet",
"test:temp": "export RESTURL=http://localhost:3000/v4/ && mocha --timeout 30000 -g '#hydrateUtxosWL' test/integration/",
"test:temp2": "mocha --timeout=30000 -g '#tokenUtxoDetailsWL' test/unit/",
"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=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)
}
}
+4 -1
View File
@@ -653,6 +653,7 @@ class Utils {
const path = `${this.restURL}slp/validateTxid3`
// console.log(`txid: ${JSON.stringify(txid, null, 2)}`)
// console.log(`path: ${JSON.stringify(path, null, 2)}`)
// Handle a single TXID or an array of TXIDs.
let txids
@@ -1110,7 +1111,7 @@ class Utils {
// Validate using a 'waterfall' of validators.
utxo.isValid = await this.waterfallValidateTxid(utxo.txid)
// console.log(`isValid: ${JSON.stringify(isValid, null, 2)}`)
// console.log(`isValid: ${JSON.stringify(utxo.isValid, null, 2)}`)
}
}
@@ -1449,6 +1450,8 @@ class Utils {
async waterfallValidateTxid (txid) {
try {
// console.log('txid: ', txid)
const cachedTxValidation = {}
// If the value has been cached, use the cached version first.
+284
View File
@@ -0,0 +1,284 @@
/*
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 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
* }
* ],
* "nullUtxos": [],
* "slpUtxos": {
* "type1": {
* "mintBatons": [
* {
* "height": 674512,
* "tx_hash": "acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230",
* "tx_pos": 2,
* "value": 546,
* "txid": "acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230",
* "vout": 2,
* "utxoType": "minting-baton",
* "tokenId": "acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230",
* "tokenTicker": "SLPTEST",
* "tokenName": "SLP Test Token",
* "tokenDocumentUrl": "https://FullStack.cash",
* "tokenDocumentHash": "",
* "decimals": 8,
* "tokenType": 1,
* "isValid": true
* }
* ],
* "tokens": [
* {
* "height": 674512,
* "tx_hash": "acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230",
* "tx_pos": 1,
* "value": 546,
* "txid": "acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230",
* "vout": 1,
* "utxoType": "token",
* "tokenQty": "100",
* "tokenId": "acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230",
* "tokenTicker": "SLPTEST",
* "tokenName": "SLP Test Token",
* "tokenDocumentUrl": "https://FullStack.cash",
* "tokenDocumentHash": "",
* "decimals": 8,
* "tokenType": 1,
* "isValid": true
* }
* ]
* },
* "nft": {
* "groupMintBatons": [
* {
* "height": 674513,
* "tx_hash": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04",
* "tx_pos": 2,
* "value": 546,
* "txid": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04",
* "vout": 2,
* "utxoType": "minting-baton",
* "transactionType": "mint",
* "tokenId": "a9a2458a0f9f0761d5b8725c256f2e7fa35b9de4dec6f47b46e9f20d92d0e395",
* "tokenType": 129,
* "tokenTicker": "NFTGT",
* "tokenName": "NFT Test Group Token",
* "tokenDocumentUrl": "https://FullStack.cash",
* "tokenDocumentHash": "",
* "decimals": 0,
* "mintBatonVout": 2,
* "isValid": true
* }
* ],
* "groupTokens": [
* {
* "height": 674513,
* "tx_hash": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04",
* "tx_pos": 1,
* "value": 546,
* "txid": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04",
* "vout": 1,
* "utxoType": "token",
* "tokenQty": "10",
* "transactionType": "mint",
* "tokenId": "a9a2458a0f9f0761d5b8725c256f2e7fa35b9de4dec6f47b46e9f20d92d0e395",
* "tokenType": 129,
* "tokenTicker": "NFTGT",
* "tokenName": "NFT Test Group Token",
* "tokenDocumentUrl": "https://FullStack.cash",
* "tokenDocumentHash": "",
* "decimals": 0,
* "mintBatonVout": 2,
* "isValid": true
* }
* ],
* "tokens": [
* {
* "height": 674512,
* "tx_hash": "eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b",
* "tx_pos": 1,
* "value": 546,
* "txid": "eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b",
* "vout": 1,
* "utxoType": "token",
* "tokenQty": "1",
* "tokenId": "eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b",
* "tokenTicker": "NFT004",
* "tokenName": "NFT Child",
* "tokenDocumentUrl": "https://FullStack.cash",
* "tokenDocumentHash": "",
* "decimals": 0,
* "tokenType": 65,
* "isValid": true
* }
* ]
* }
* }
* }
* ]
*
*
*/
async get (address, useWhitelist = false) {
try {
// Convert address to an array if it is a string.
if (typeof address === 'string') address = [address]
// Throw an error if there are more than 20 addresses passed in at a time.
if (address.length > 20) throw new Error('Too many elements, 20 max.')
// Covert each address to a BCH address.
const addr = address.map(elem => this.slp.Address.toCashAddress(elem))
// 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.
let hydratedUtxos
if (useWhitelist) {
hydratedUtxos = await this.slp.Utils.hydrateUtxosWL(utxoData.utxos)
} else {
hydratedUtxos = await this.slp.Utils.hydrateUtxos(utxoData.utxos)
}
// console.log(`hydratedUtxos: ${JSON.stringify(hydratedUtxos, null, 2)}`)
const retAry = [] // Return array
// Loop through each address.
for (let i = 0; i < hydratedUtxos.slpUtxos.length; i++) {
const thisAddr = hydratedUtxos.slpUtxos[i]
const addrObj = {
address: thisAddr.address
}
// Filter out the different types of UTXOs.
addrObj.bchUtxos = thisAddr.utxos.filter(elem => elem.isValid === false)
addrObj.nullUtxos = thisAddr.utxos.filter(elem => elem.isValid === null)
// Break down the SLP UTXOs.
addrObj.slpUtxos = {
type1: {},
nft: {}
}
// Token Type 1 Minting Batons.
addrObj.slpUtxos.type1.mintBatons = thisAddr.utxos.filter(elem => {
const isValid = elem.isValid === true
const tokenTypeIs1 = elem.tokenType === 1
const isMintingBaton = elem.utxoType === 'minting-baton'
return isValid && tokenTypeIs1 && isMintingBaton
})
// Token Type 1 tokens.
addrObj.slpUtxos.type1.tokens = thisAddr.utxos.filter(elem => {
const isValid = elem.isValid === true
const tokenTypeIs1 = elem.tokenType === 1
const isToken = elem.utxoType === 'token'
return isValid && tokenTypeIs1 && isToken
})
// NFT Group Minting Batons
addrObj.slpUtxos.nft.groupMintBatons = thisAddr.utxos.filter(elem => {
const isValid = elem.isValid === true
const tokenTypeIs129 = elem.tokenType === 129
const isMintingBaton = elem.utxoType === 'minting-baton'
return isValid && tokenTypeIs129 && isMintingBaton
})
// NFT Group tokens
addrObj.slpUtxos.nft.groupTokens = thisAddr.utxos.filter(elem => {
const isValid = elem.isValid === true
const tokenTypeIs129 = elem.tokenType === 129
const isToken = elem.utxoType === 'token'
return isValid && tokenTypeIs129 && isToken
})
// NFT (Child) tokens
addrObj.slpUtxos.nft.tokens = thisAddr.utxos.filter(elem => {
const isValid = elem.isValid === true
const tokenTypeIs65 = elem.tokenType === 65
const isToken = elem.utxoType === 'token'
return isValid && tokenTypeIs65 && isToken
})
retAry.push(addrObj)
}
return retAry
} catch (err) {
console.error('Error in bchjs.utxo.get()')
throw err
}
}
}
module.exports = UTXO
@@ -8,7 +8,7 @@
const chai = require('chai')
const assert = chai.assert
const BCHJS = require('../../../../src/bch-js')
const bchjs = new BCHJS()
const bchjs = new BCHJS({ restURL: process.env.RESTURL })
// Inspect utility used for debugging.
const util = require('util')
@@ -44,6 +44,7 @@ describe('#rawtransaction', () => {
assert.hasAllKeys(err, ['error'])
assert.include(err.error, 'bad-txns-inputs-missingorspent')
// assert.include(err.error, 'Missing inputs')
}
})
@@ -61,6 +62,7 @@ describe('#rawtransaction', () => {
assert.hasAllKeys(err, ['error'])
assert.include(err.error, 'bad-txns-inputs-missingorspent')
// assert.include(err.error, 'Missing inputs')
}
})
})
+4 -1
View File
@@ -27,7 +27,7 @@ describe('#SLP', () => {
// Introduce a delay so that the BVT doesn't trip the rate limits.
if (process.env.IS_USING_FREE_TIER) await sleep(1000)
bchjs = new BCHJS()
bchjs = new BCHJS({ restURL: process.env.RESTURL })
})
describe('#util', () => {
@@ -113,6 +113,7 @@ describe('#SLP', () => {
// Token send on BCHN network
assert.equal(data[4].tx_hash, utxos[4].tx_hash)
// assert.equal(data[4].isValid, true)
assert.equal(data[4].isValid, null)
// Token send on ABC network
@@ -180,6 +181,7 @@ describe('#SLP', () => {
assert.equal(result[0].valid, null)
assert.equal(result[1].valid, null)
assert.equal(result[2].valid, true)
// assert.equal(result[2].valid, null)
assert.equal(result[3].valid, true)
assert.equal(result[4].valid, null)
})
@@ -209,6 +211,7 @@ describe('#SLP', () => {
assert.equal(result[0].valid, null)
assert.equal(result[1].valid, null)
assert.equal(result[2].valid, true)
// assert.equal(result[2].valid, null)
assert.equal(result[3].valid, true)
assert.equal(result[4].valid, null)
assert.equal(result[5].valid, true)
@@ -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,97 @@
/*
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)
assert.property(result[0], 'address')
assert.property(result[0], 'bchUtxos')
assert.property(result[0], 'nullUtxos')
assert.property(result[0], 'slpUtxos')
assert.isArray(result[0].bchUtxos)
assert.isArray(result[0].nullUtxos)
})
it('should handle an array of addresses', async () => {
const addr = [
'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9',
'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
]
const result = await bchjs.Utxo.get(addr)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result)
assert.property(result[0], 'address')
assert.property(result[0], 'bchUtxos')
assert.property(result[0], 'nullUtxos')
assert.property(result[0], 'slpUtxos')
assert.isArray(result[0].bchUtxos)
assert.isArray(result[0].nullUtxos)
})
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.isArray(result)
assert.property(result[0], 'address')
assert.property(result[0], 'bchUtxos')
assert.property(result[0], 'nullUtxos')
assert.property(result[0], 'slpUtxos')
assert.isArray(result[0].bchUtxos)
assert.isArray(result[0].nullUtxos)
assert.isArray(result[0].slpUtxos.type1.mintBatons)
assert.isArray(result[0].slpUtxos.type1.tokens)
assert.isArray(result[0].slpUtxos.nft.groupMintBatons)
assert.isArray(result[0].slpUtxos.nft.groupTokens)
assert.isArray(result[0].slpUtxos.nft.tokens)
})
it('should use the whitelist when flag is set', async () => {
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
const useWhitelist = true
const result = await bchjs.Utxo.get(addr, useWhitelist)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result)
assert.property(result[0], 'address')
assert.property(result[0], 'bchUtxos')
assert.property(result[0], 'nullUtxos')
assert.property(result[0], 'slpUtxos')
assert.isArray(result[0].bchUtxos)
assert.isArray(result[0].nullUtxos)
// Most token UTXOs should end up in the nullUtxos array.
assert.isAbove(result[0].bchUtxos.length, 0)
assert.isAbove(result[0].nullUtxos.length, 1)
})
})
})
function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
@@ -1,71 +0,0 @@
/*
Integration tests for the bchjs. Only covers calls made to
rest.bitcoin.com.
TODO
*/
const chai = require('chai')
const assert = chai.assert
const BCHJS = require('../../../../src/bch-js')
const bchjs = new BCHJS()
// Inspect utility used for debugging.
const util = require('util')
util.inspect.defaultOptions = {
showHidden: true,
colors: true,
depth: 3
}
describe('#rawtransaction', () => {
beforeEach(async () => {
if (process.env.IS_USING_FREE_TIER) await sleep(1000)
})
/*
Testing sentRawTransaction isn't really possible with an integration test,
as the endpoint really needs an e2e test to be properly tested. The tests
below expect error messages returned from the server, but at least test
that the server is responding on those endpoints, and responds consistently.
*/
describe('sendRawTransaction', () => {
it('should send a single transaction hex', async () => {
try {
const hex =
'01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000'
await bchjs.RawTransactions.sendRawTransaction(hex)
// console.log(`result ${JSON.stringify(result, null, 2)}`)
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
// console.log(`err: ${util.inspect(err)}`)
assert.hasAllKeys(err, ['error'])
assert.include(err.error, 'Missing inputs')
}
})
it('should send an array of tx hexes', async () => {
try {
const hexes = [
'01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000',
'01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000'
]
const result = await bchjs.RawTransactions.sendRawTransaction(hexes)
console.log(`result ${JSON.stringify(result, null, 2)}`)
} catch (err) {
// console.log(`err: ${util.inspect(err)}`)
assert.hasAllKeys(err, ['error'])
assert.include(err.error, 'Missing inputs')
}
})
})
})
function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
@@ -1,246 +0,0 @@
/*
Integration tests for the bchjs covering SLP tokens.
These tests are specific to the ABC chain.
*/
const chai = require('chai')
const assert = chai.assert
const BCHJS = require('../../../../src/bch-js')
let bchjs
// Inspect utility used for debugging.
const util = require('util')
util.inspect.defaultOptions = {
showHidden: true,
colors: true,
depth: 1
}
describe('#SLP', () => {
// before(() => {
// console.log(`bchjs.SLP.restURL: ${bchjs.SLP.restURL}`)
// console.log(`bchjs.SLP.apiToken: ${bchjs.SLP.apiToken}`)
// })
beforeEach(async () => {
// Introduce a delay so that the BVT doesn't trip the rate limits.
if (process.env.IS_USING_FREE_TIER) await sleep(1000)
bchjs = new BCHJS()
})
describe('#util', () => {
describe('#tokenUtxoDetails', () => {
it('should handle a range of UTXO types', async () => {
const utxos = [
// Malformed SLP tx
{
note: 'Malformed SLP tx',
tx_hash:
'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a',
tx_pos: 1,
value: 546
},
// Normal TX (non-SLP)
{
note: 'Normal TX (non-SLP)',
tx_hash:
'01cdaec2f8b311fc2d6ecc930247bd45fa696dc204ab684596e281fe1b06c1f0',
tx_pos: 0,
value: 400000
},
// Valid PSF SLP tx
{
note: 'Valid PSF SLP tx',
tx_hash:
'daf4d8b8045e7a90b7af81bfe2370178f687da0e545511bce1c9ae539eba5ffd',
tx_pos: 1,
value: 546
},
// Valid SLP token not in whitelist
{
note: 'Valid SLP token not in whitelist',
tx_hash:
'3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488',
tx_pos: 1,
value: 546
},
// Token send on BCHN network.
{
note: 'Token send on BCHN network',
tx_hash:
'402c663379d9699b6e2dd38737061e5888c5a49fca77c97ab98e79e08959e019',
tx_pos: 1,
value: 546
},
// Token send on ABC network.
{
note: 'Token send on ABC network',
tx_hash:
'336bfe2168aac4c3303508a9e8548a0d33797a83b85b76a12d845c8d6674f79d',
tx_pos: 1,
value: 546
},
// Known invalid SLP token send of PSF tokens.
{
note: 'Known invalid SLP token send of PSF tokens',
tx_hash:
'2bf691ad3679d928fef880b8a45b93b233f8fa0d0a92cf792313dbe77b1deb74',
tx_pos: 1,
value: 546
}
]
const data = await bchjs.SLP.Utils.tokenUtxoDetails(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
// Malformed SLP tx
assert.equal(data[0].tx_hash, utxos[0].tx_hash)
assert.equal(data[0].isValid, false)
// Normal TX (non-SLP)
assert.equal(data[1].tx_hash, utxos[1].tx_hash)
assert.equal(data[1].isValid, false)
// Valid PSF SLP tx
assert.equal(data[2].tx_hash, utxos[2].tx_hash)
assert.equal(data[2].isValid, true)
// Valid SLP token not in whitelist
assert.equal(data[3].tx_hash, utxos[3].tx_hash)
assert.equal(data[3].isValid, true)
// Token send on BCHN network
assert.equal(data[4].tx_hash, utxos[4].tx_hash)
assert.equal(data[4].isValid, true)
// Token send on ABC network
assert.equal(data[5].tx_hash, utxos[5].tx_hash)
assert.equal(data[5].isValid, null)
// Known invalid SLP token send of PSF tokens
assert.equal(data[6].tx_hash, utxos[6].tx_hash)
assert.equal(data[6].isValid, false)
})
})
describe('#validateTxid3', () => {
it('should invalidate a known invalid TXID', async () => {
const txid =
'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a'
const result = await bchjs.SLP.Utils.validateTxid3(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result)
assert.property(result[0], 'txid')
assert.equal(result[0].txid, txid)
assert.property(result[0], 'valid')
assert.equal(result[0].valid, null)
})
it('should validate a known valid TXID', async () => {
const txid =
'daf4d8b8045e7a90b7af81bfe2370178f687da0e545511bce1c9ae539eba5ffd'
const result = await bchjs.SLP.Utils.validateTxid3(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result)
assert.property(result[0], 'txid')
assert.equal(result[0].txid, txid)
assert.property(result[0], 'valid')
assert.equal(result[0].valid, true)
})
it('should handle a mix of valid, invalid, and non-SLP txs', async () => {
const txids = [
// Malformed SLP tx
'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a',
// Normal TX (non-SLP)
'01cdaec2f8b311fc2d6ecc930247bd45fa696dc204ab684596e281fe1b06c1f0',
// Valid PSF SLP tx
'daf4d8b8045e7a90b7af81bfe2370178f687da0e545511bce1c9ae539eba5ffd',
// Valid SLP token not in whitelist
'3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488',
// Unprocessed SLP TX
'402c663379d9699b6e2dd38737061e5888c5a49fca77c97ab98e79e08959e019'
]
const result = await bchjs.SLP.Utils.validateTxid3(txids)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result)
assert.equal(result[0].txid, txids[0])
assert.equal(result[0].valid, null)
assert.equal(result[1].txid, txids[1])
assert.equal(result[1].valid, null)
assert.equal(result[2].txid, txids[2])
assert.equal(result[2].valid, true)
// True in validateTxid()
assert.equal(result[3].txid, txids[3])
assert.equal(result[3].valid, true)
assert.equal(result[4].txid, txids[4])
assert.equal(result[4].valid, true)
})
})
describe('#validateTxid', () => {
it('should handle a mix of valid, invalid, and non-SLP txs', async () => {
const txids = [
// Malformed SLP tx
'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a',
// Normal TX (non-SLP)
'01cdaec2f8b311fc2d6ecc930247bd45fa696dc204ab684596e281fe1b06c1f0',
// Valid PSF SLP tx
'daf4d8b8045e7a90b7af81bfe2370178f687da0e545511bce1c9ae539eba5ffd',
// Valid SLP token not in whitelist
'3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488',
// Token send on BCHN network.
'402c663379d9699b6e2dd38737061e5888c5a49fca77c97ab98e79e08959e019',
// Token send on ABC network.
'336bfe2168aac4c3303508a9e8548a0d33797a83b85b76a12d845c8d6674f79d'
]
const result = await bchjs.SLP.Utils.validateTxid(txids)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result)
assert.equal(result[0].txid, txids[0])
assert.equal(result[0].valid, null)
assert.equal(result[1].txid, txids[1])
assert.equal(result[1].valid, null)
assert.equal(result[2].txid, txids[2])
assert.equal(result[2].valid, true)
// True in validateTxid() but null in validateTxid3()
assert.equal(result[3].txid, txids[3])
assert.equal(result[3].valid, true)
assert.equal(result[4].txid, txids[4])
assert.equal(result[4].valid, true)
assert.equal(result[5].txid, txids[5])
assert.equal(result[5].valid, null)
})
})
})
})
// Promise-based sleep function
function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
+1 -1
View File
@@ -823,7 +823,7 @@ describe('#SLP', () => {
assert.equal(result.slpUtxos[1].utxos[0].isValid, true)
assert.equal(result.slpUtxos[1].utxos[0].tokenTicker, 'TROUT')
assert.equal(result.slpUtxos[2].utxos[0].isValid, false)
assert.equal(result.slpUtxos[2].utxos[1].isValid, null)
// assert.equal(result.slpUtxos[2].utxos[1].isValid, null)
assert.equal(result.slpUtxos[2].utxos[1].tokenTicker, 'VALENTINE')
} catch (err) {
console.error(
+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: {
+261
View File
@@ -0,0 +1,261 @@
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'
}
]
}
const mockTwoHydratedAddrs = {
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'
},
{
utxos: [
{
height: 602405,
tx_hash:
'2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7',
tx_pos: 0,
value: 1000,
txid:
'2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7',
vout: 0,
isValid: false
}
],
address: 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
}
]
}
const mockEveryUtxoType = {
slpUtxos: [
{
utxos: [
{
height: 674512,
tx_hash:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
tx_pos: 1,
value: 546,
txid:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
vout: 1,
utxoType: 'token',
tokenQty: '100',
tokenId:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
tokenTicker: 'SLPTEST',
tokenName: 'SLP Test Token',
tokenDocumentUrl: 'https://FullStack.cash',
tokenDocumentHash: '',
decimals: 8,
tokenType: 1,
isValid: true
},
{
height: 674512,
tx_hash:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
tx_pos: 2,
value: 546,
txid:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
vout: 2,
utxoType: 'minting-baton',
tokenId:
'acbb0d3ceef55aa3e5fafc19335ae4bf2f8edba3c0567547dfd402391db32230',
tokenTicker: 'SLPTEST',
tokenName: 'SLP Test Token',
tokenDocumentUrl: 'https://FullStack.cash',
tokenDocumentHash: '',
decimals: 8,
tokenType: 1,
isValid: true
},
{
height: 674512,
tx_hash:
'eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b',
tx_pos: 1,
value: 546,
txid:
'eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b',
vout: 1,
utxoType: 'token',
tokenQty: '1',
tokenId:
'eeddccc4d716f04157ea132ac93a48040fea34a6b57f3d8f0cccb7d1a731ab2b',
tokenTicker: 'NFT004',
tokenName: 'NFT Child',
tokenDocumentUrl: 'https://FullStack.cash',
tokenDocumentHash: '',
decimals: 0,
tokenType: 65,
isValid: true
},
{
height: 674513,
tx_hash:
'705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04',
tx_pos: 1,
value: 546,
txid:
'705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04',
vout: 1,
utxoType: 'token',
tokenQty: '10',
transactionType: 'mint',
tokenId:
'a9a2458a0f9f0761d5b8725c256f2e7fa35b9de4dec6f47b46e9f20d92d0e395',
tokenType: 129,
tokenTicker: 'NFTGT',
tokenName: 'NFT Test Group Token',
tokenDocumentUrl: 'https://FullStack.cash',
tokenDocumentHash: '',
decimals: 0,
mintBatonVout: 2,
isValid: true
},
{
height: 674513,
tx_hash:
'705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04',
tx_pos: 2,
value: 546,
txid:
'705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04',
vout: 2,
utxoType: 'minting-baton',
transactionType: 'mint',
tokenId:
'a9a2458a0f9f0761d5b8725c256f2e7fa35b9de4dec6f47b46e9f20d92d0e395',
tokenType: 129,
tokenTicker: 'NFTGT',
tokenName: 'NFT Test Group Token',
tokenDocumentUrl: 'https://FullStack.cash',
tokenDocumentHash: '',
decimals: 0,
mintBatonVout: 2,
isValid: true
},
{
height: 674513,
tx_hash:
'705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04',
tx_pos: 3,
value: 38134,
txid:
'705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04',
vout: 3,
isValid: false
}
],
address: 'bitcoincash:qrm0c67wwqh0w7wjxua2gdt2xggnm90xws00a3lezv'
}
]
}
module.exports = {
mockUtxoData,
mockHydratedUtxos,
mockTwoHydratedAddrs,
mockEveryUtxoType
}
+148
View File
@@ -0,0 +1,148 @@
/*
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)
assert.property(result[0], 'address')
assert.property(result[0], 'bchUtxos')
assert.property(result[0], 'nullUtxos')
assert.property(result[0], 'slpUtxos')
assert.isArray(result[0].bchUtxos)
assert.isArray(result[0].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')
}
})
it('should handle an array of addresses', async () => {
// Mock dependencies.
sandbox.stub(bchjs.Utxo.electrumx, 'utxo').resolves({ utxos: {} })
sandbox
.stub(bchjs.Utxo.slp.Utils, 'hydrateUtxos')
.resolves(mockData.mockTwoHydratedAddrs)
const addr = [
'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9',
'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
]
const result = await bchjs.Utxo.get(addr)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result)
assert.equal(result.length, 2)
assert.property(result[0], 'address')
assert.property(result[0], 'bchUtxos')
assert.property(result[0], 'nullUtxos')
assert.property(result[0], 'slpUtxos')
assert.isArray(result[0].bchUtxos)
assert.isArray(result[0].nullUtxos)
})
it('should throw an error for array of 21 elements', async () => {
try {
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
const addrs = []
for (let i = 0; i < 21; i++) {
addrs.push(addr)
}
await bchjs.Utxo.get(addrs)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Too many elements, 20 max.')
}
})
it('should handle NFTs and minting batons', async () => {
// Mock dependencies.
sandbox.stub(bchjs.Utxo.electrumx, 'utxo').resolves({ utxos: {} })
sandbox
.stub(bchjs.Utxo.slp.Utils, 'hydrateUtxos')
.resolves(mockData.mockEveryUtxoType)
const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj'
const result = await bchjs.Utxo.get(addr)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result)
assert.property(result[0], 'address')
assert.property(result[0], 'bchUtxos')
assert.property(result[0], 'nullUtxos')
assert.property(result[0], 'slpUtxos')
assert.isArray(result[0].bchUtxos)
assert.isArray(result[0].nullUtxos)
assert.isArray(result[0].slpUtxos.type1.mintBatons)
assert.isArray(result[0].slpUtxos.type1.tokens)
assert.isArray(result[0].slpUtxos.nft.groupMintBatons)
assert.isArray(result[0].slpUtxos.nft.groupTokens)
assert.isArray(result[0].slpUtxos.nft.tokens)
})
it('should use the whitelist when flag is set', async () => {
// Mock dependencies.
sandbox.stub(bchjs.Utxo.electrumx, 'utxo').resolves(mockData.mockUtxoData)
sandbox
.stub(bchjs.Utxo.slp.Utils, 'hydrateUtxosWL')
.resolves(mockData.mockHydratedUtxos)
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
const useWhitelist = true
const result = await bchjs.Utxo.get(addr, useWhitelist)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result)
assert.property(result[0], 'address')
assert.property(result[0], 'bchUtxos')
assert.property(result[0], 'nullUtxos')
assert.property(result[0], 'slpUtxos')
assert.isArray(result[0].bchUtxos)
assert.isArray(result[0].nullUtxos)
})
})
})