@psf/bch-js 5.2.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/electrumx.js +15 -2
- package/src/psf-slp-indexer.js +2 -1
- package/src/util.js +50 -0
- package/src/utxo.js +78 -9
- package/test/integration/chains/abc/utxo-integration.js +5 -3
- package/test/integration/chains/bchn/transaction-integration.js +28 -0
- package/test/integration/chains/bchn/utxo-integration.js +156 -89
- package/test/unit/fixtures/utxo-mocks.js +115 -1
- package/test/unit/utxo-unit.js +66 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@psf/bch-js",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"description": "The FullStack.cash JavaScript library for Bitcoin Cash and SLP Tokens",
|
|
5
5
|
"author": "Chris Troutner <chris.troutner@gmail.com>",
|
|
6
6
|
"contributors": [
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"test:integration:local:abc": "export RESTURL=http://localhost:3000/v5/ && mocha --timeout 30000 test/integration && mocha --timeout 30000 test/integration/chains/abc/",
|
|
19
19
|
"test:integration:local:bchn": "export RESTURL=http://localhost:3000/v5/ && mocha --timeout 30000 test/integration/ && mocha --timeout 30000 test/integration/chains/bchn/",
|
|
20
20
|
"test:integration:local:testnet": "RESTURL=http://localhost:4000/v5/ mocha --timeout 30000 test/integration/chains/testnet",
|
|
21
|
-
"test:integration:decatur:bchn": "export RESTURL=http://192.168.2.
|
|
21
|
+
"test:integration:decatur:bchn": "export RESTURL=http://192.168.2.129:3000/v5/ && mocha --timeout 30000 test/integration/ && mocha --timeout 30000 test/integration/chains/bchn/",
|
|
22
22
|
"test:integration:decatur:abc": "export RESTURL=http://192.168.2.141:3000/v5/ && mocha --timeout 30000 test/integration && mocha --timeout 30000 test/integration/chains/abc/",
|
|
23
23
|
"test:integration:temp:bchn": "export RESTURL=http://157.90.174.219:3000/v5/ && mocha --timeout 30000 test/integration/",
|
|
24
24
|
"test:temp": "export RESTURL=http://localhost:3000/v5/ && mocha --timeout 30000 -g '#Encryption' test/integration/",
|
package/src/electrumx.js
CHANGED
|
@@ -638,13 +638,24 @@ class ElectrumX {
|
|
|
638
638
|
// Sort confirmed Transactions by the block height
|
|
639
639
|
sortConfTxs (txs, sortingOrder = 'DESCENDING') {
|
|
640
640
|
try {
|
|
641
|
+
// console.log(`sortConfTxs txs: ${JSON.stringify(txs, null, 2)}`)
|
|
642
|
+
|
|
641
643
|
// Filter out unconfirmed transactions, with a height of 0 or less.
|
|
642
644
|
txs = txs.filter(elem => elem.height > 0)
|
|
643
645
|
|
|
644
646
|
if (sortingOrder === 'DESCENDING') {
|
|
645
|
-
|
|
647
|
+
// console.log('Sorting in descending order')
|
|
648
|
+
return txs.sort((a, b) => {
|
|
649
|
+
// console.log(`descending b.height: ${b.height}, a.height: ${a.height}`)
|
|
650
|
+
return b.height - a.height
|
|
651
|
+
})
|
|
646
652
|
}
|
|
647
|
-
|
|
653
|
+
|
|
654
|
+
// console.log('Sorting in ascending order')
|
|
655
|
+
return txs.sort((a, b) => {
|
|
656
|
+
// console.log(`ascending b.height: ${b.height}, a.height: ${a.height}`)
|
|
657
|
+
return a.height - b.height
|
|
658
|
+
})
|
|
648
659
|
} catch (err) {
|
|
649
660
|
console.log('Error in util.js/sortConfTxs()')
|
|
650
661
|
throw err
|
|
@@ -685,6 +696,8 @@ class ElectrumX {
|
|
|
685
696
|
// Substitute zero-conf txs with the current block-height + 1
|
|
686
697
|
async sortAllTxs (txs, sortingOrder = 'DESCENDING') {
|
|
687
698
|
try {
|
|
699
|
+
// console.log(`sortingOrder: ${sortingOrder}`)
|
|
700
|
+
|
|
688
701
|
// Calculate the height of the next block
|
|
689
702
|
const nextBlock = (await this.blockchain.getBlockCount()) + 1
|
|
690
703
|
|
package/src/psf-slp-indexer.js
CHANGED
|
@@ -132,7 +132,8 @@ class PsfSlpIndexer {
|
|
|
132
132
|
*/
|
|
133
133
|
async balance (address) {
|
|
134
134
|
try {
|
|
135
|
-
console.log('balance() address: ', address)
|
|
135
|
+
// console.log('balance() address: ', address)
|
|
136
|
+
|
|
136
137
|
// Handle single address.
|
|
137
138
|
if (typeof address === 'string') {
|
|
138
139
|
const response = await axios.post(
|
package/src/util.js
CHANGED
|
@@ -150,6 +150,56 @@ class Util {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* @api Util.chunk100() chunk100()
|
|
155
|
+
* @apiName chunk100
|
|
156
|
+
* @apiGroup Util
|
|
157
|
+
* @apiDescription chunk up an array into multiple arrays of 100 elements each.
|
|
158
|
+
* Input: arrayToSlice - a one-dimensional array of elements.
|
|
159
|
+
* Returns a two-dimensional array. An array of 100-element arrays.
|
|
160
|
+
*
|
|
161
|
+
* @apiExample Example usage:
|
|
162
|
+
* (async () => {
|
|
163
|
+
* try {
|
|
164
|
+
* const bigArray = [0,1,2,3,4,5,6,7,8,9,10,...,148, 149, 150]
|
|
165
|
+
*
|
|
166
|
+
* const chunked = bchjs.Util.chunk20(bigArray)
|
|
167
|
+
* console.log(chunked)
|
|
168
|
+
* } catch(error) {
|
|
169
|
+
* console.error(error)
|
|
170
|
+
* }
|
|
171
|
+
* })()
|
|
172
|
+
*
|
|
173
|
+
* // returns
|
|
174
|
+
* [
|
|
175
|
+
* [0,1,2,3,4,5,6,7,8,9,10,11,...,98,99],
|
|
176
|
+
* [100,101,102,...,148,149,150]
|
|
177
|
+
* ]
|
|
178
|
+
*/
|
|
179
|
+
chunk100 (arrayToSlice) {
|
|
180
|
+
try {
|
|
181
|
+
// Validate inputs
|
|
182
|
+
if (!Array.isArray(arrayToSlice)) {
|
|
183
|
+
throw new Error('input must be an array')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let offset = 0
|
|
187
|
+
const result = []
|
|
188
|
+
|
|
189
|
+
// Loop over the array and slice off chunks of 100 elements.
|
|
190
|
+
while (offset < arrayToSlice.length) {
|
|
191
|
+
const chunk = arrayToSlice.slice(offset, offset + 100)
|
|
192
|
+
result.push(chunk)
|
|
193
|
+
offset = offset + 100
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error('Error in chunk100()')
|
|
199
|
+
throw err
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
153
203
|
/**
|
|
154
204
|
* @api Util.sleep() sleep()
|
|
155
205
|
* @apiName sleep
|
package/src/utxo.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
const Electrumx = require('./electrumx')
|
|
10
10
|
const Slp = require('./slp/slp')
|
|
11
11
|
const PsfSlpIndexer = require('./psf-slp-indexer')
|
|
12
|
+
const BigNumber = require('bignumber.js')
|
|
12
13
|
|
|
13
14
|
class UTXO {
|
|
14
15
|
constructor (config = {}) {
|
|
@@ -16,6 +17,7 @@ class UTXO {
|
|
|
16
17
|
this.electrumx = new Electrumx(config)
|
|
17
18
|
this.slp = new Slp(config)
|
|
18
19
|
this.psfSlpIndexer = new PsfSlpIndexer(config)
|
|
20
|
+
this.BigNumber = BigNumber
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -309,10 +311,6 @@ class UTXO {
|
|
|
309
311
|
* - groupTokens: [] - NFT Group tokens, used to create NFT tokens.
|
|
310
312
|
* - groupMintBatons: [] - Minting baton to create more NFT Group tokens.
|
|
311
313
|
*
|
|
312
|
-
* Note: You can pass in an optional second Boolean argument. The default
|
|
313
|
-
* `false` will use the normal waterfall validation method. Set to `true`,
|
|
314
|
-
* SLP UTXOs will be validated with the whitelist filtered SLPDB. This will
|
|
315
|
-
* result is many more UTXOs in the `nullUtxos` array.
|
|
316
314
|
*
|
|
317
315
|
* @apiExample Example usage:
|
|
318
316
|
* (async () => {
|
|
@@ -359,10 +357,22 @@ class UTXO {
|
|
|
359
357
|
// console.log(`utxoData: ${JSON.stringify(utxoData, null, 2)}`)
|
|
360
358
|
const utxos = utxoData.utxos
|
|
361
359
|
|
|
360
|
+
let slpUtxos = []
|
|
361
|
+
|
|
362
362
|
// Get SLP UTXOs from the psf-slp-indexer
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
363
|
+
try {
|
|
364
|
+
const slpUtxoData = await this.psfSlpIndexer.balance(addr)
|
|
365
|
+
// console.log(`slpUtxoData: ${JSON.stringify(slpUtxoData, null, 2)}`)
|
|
366
|
+
|
|
367
|
+
slpUtxos = slpUtxoData.balance.utxos
|
|
368
|
+
} catch (err) {
|
|
369
|
+
// console.log('err: ', err)
|
|
370
|
+
|
|
371
|
+
// Exit quietly if address has no SLP UTXOs. Otherwise, throw the error.
|
|
372
|
+
if (err.error && !err.error.includes('Key not found in database')) {
|
|
373
|
+
throw err
|
|
374
|
+
}
|
|
375
|
+
}
|
|
366
376
|
|
|
367
377
|
// Loop through the Fulcrum UTXOs.
|
|
368
378
|
for (let i = 0; i < utxos.length; i++) {
|
|
@@ -399,10 +409,15 @@ class UTXO {
|
|
|
399
409
|
}
|
|
400
410
|
}
|
|
401
411
|
|
|
402
|
-
|
|
403
|
-
|
|
412
|
+
// Get token UTXOs
|
|
413
|
+
let type1TokenUtxos = utxos.filter(
|
|
404
414
|
x => x.isSlp === true && x.type === 'token'
|
|
405
415
|
)
|
|
416
|
+
|
|
417
|
+
// Hydrate the UTXOs with additional token data.
|
|
418
|
+
type1TokenUtxos = await this.hydrateTokenData(type1TokenUtxos)
|
|
419
|
+
|
|
420
|
+
const bchUtxos = utxos.filter(x => x.isSlp === false)
|
|
406
421
|
const type1BatonUtxos = utxos.filter(
|
|
407
422
|
x => x.isSlp === true && x.type === 'baton'
|
|
408
423
|
)
|
|
@@ -430,6 +445,60 @@ class UTXO {
|
|
|
430
445
|
}
|
|
431
446
|
}
|
|
432
447
|
|
|
448
|
+
// Hydrate an array of token UTXOs with token information.
|
|
449
|
+
// Returns an array of token UTXOs with additional data.
|
|
450
|
+
async hydrateTokenData (utxoAry) {
|
|
451
|
+
try {
|
|
452
|
+
// console.log('utxoAry: ', utxoAry)
|
|
453
|
+
|
|
454
|
+
// Create a list of token IDs without duplicates.
|
|
455
|
+
let tokenIds = utxoAry.map(x => x.tokenId)
|
|
456
|
+
|
|
457
|
+
// Remove duplicates. https://stackoverflow.com/questions/9229645/remove-duplicate-values-from-js-array
|
|
458
|
+
tokenIds = [...new Set(tokenIds)]
|
|
459
|
+
// console.log('tokenIds: ', tokenIds)
|
|
460
|
+
|
|
461
|
+
// Get Genesis data for each tokenId
|
|
462
|
+
const genesisData = []
|
|
463
|
+
for (let i = 0; i < tokenIds.length; i++) {
|
|
464
|
+
const thisTokenId = tokenIds[i]
|
|
465
|
+
const thisTokenData = await this.psfSlpIndexer.tokenStats(thisTokenId)
|
|
466
|
+
// console.log('thisTokenData: ', thisTokenData)
|
|
467
|
+
|
|
468
|
+
genesisData.push(thisTokenData)
|
|
469
|
+
}
|
|
470
|
+
// console.log('genesisData: ', genesisData)
|
|
471
|
+
|
|
472
|
+
// Hydrate each token UTXO with data from the genesis transaction.
|
|
473
|
+
for (let i = 0; i < utxoAry.length; i++) {
|
|
474
|
+
const thisUtxo = utxoAry[i]
|
|
475
|
+
|
|
476
|
+
// Get the genesis data for this token.
|
|
477
|
+
const genData = genesisData.filter(
|
|
478
|
+
x => x.tokenData.tokenId === thisUtxo.tokenId
|
|
479
|
+
)
|
|
480
|
+
// console.log('genData: ', genData)
|
|
481
|
+
|
|
482
|
+
thisUtxo.ticker = genData[0].tokenData.ticker
|
|
483
|
+
thisUtxo.name = genData[0].tokenData.name
|
|
484
|
+
thisUtxo.documentUri = genData[0].tokenData.documentUri
|
|
485
|
+
thisUtxo.documentHash = genData[0].tokenData.documentHash
|
|
486
|
+
thisUtxo.decimals = genData[0].tokenData.decimals
|
|
487
|
+
|
|
488
|
+
// Calculate the real token quantity
|
|
489
|
+
const qty = new BigNumber(thisUtxo.qty).dividedBy(
|
|
490
|
+
10 ** parseInt(thisUtxo.decimals)
|
|
491
|
+
)
|
|
492
|
+
thisUtxo.qtyStr = qty.toString()
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return utxoAry
|
|
496
|
+
} catch (err) {
|
|
497
|
+
console.log('Error in hydrateTokenData()')
|
|
498
|
+
throw err
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
433
502
|
/**
|
|
434
503
|
* @api Utxo.findBiggestUtxo() findBiggestUtxo()
|
|
435
504
|
* @apiName findBiggestUtxo
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
Integration tests for the utxo.js library.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
const assert = require('chai').assert
|
|
5
|
+
// const assert = require('chai').assert
|
|
6
6
|
|
|
7
|
-
const BCHJS = require('../../../../src/bch-js')
|
|
8
|
-
const bchjs = new BCHJS()
|
|
7
|
+
// const BCHJS = require('../../../../src/bch-js')
|
|
8
|
+
// const bchjs = new BCHJS()
|
|
9
9
|
|
|
10
10
|
describe('#UTXO', () => {
|
|
11
11
|
beforeEach(async () => {
|
|
@@ -14,6 +14,7 @@ describe('#UTXO', () => {
|
|
|
14
14
|
if (process.env.IS_USING_FREE_TIER) await sleep(1500)
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
+
/*
|
|
17
18
|
describe('#get', () => {
|
|
18
19
|
it('should get hydrated and filtered UTXOs for an address', async () => {
|
|
19
20
|
// const addr = 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
|
|
@@ -31,6 +32,7 @@ describe('#UTXO', () => {
|
|
|
31
32
|
assert.isArray(result[0].nullUtxos)
|
|
32
33
|
})
|
|
33
34
|
})
|
|
35
|
+
*/
|
|
34
36
|
})
|
|
35
37
|
|
|
36
38
|
function sleep (ms) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Integration tests for the transaction.js library.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const assert = require('chai').assert
|
|
6
|
+
const BCHJS = require('../../../../src/bch-js')
|
|
7
|
+
const bchjs = new BCHJS()
|
|
8
|
+
|
|
9
|
+
describe('#Transaction', () => {
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
if (process.env.IS_USING_FREE_TIER) await bchjs.Util.sleep(1000)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('#get', () => {
|
|
15
|
+
it('should get a tx details for a non-SLP TX with an OP_RETURN', async () => {
|
|
16
|
+
const txid =
|
|
17
|
+
'01517ff1587fa5ffe6f5eb91c99cf3f2d22330cd7ee847e928ce90ca95bf781b'
|
|
18
|
+
|
|
19
|
+
const result = await bchjs.Transaction.get(txid)
|
|
20
|
+
// console.log('result: ', result)
|
|
21
|
+
|
|
22
|
+
assert.property(result.txData, 'txid')
|
|
23
|
+
assert.property(result.txData, 'vin')
|
|
24
|
+
assert.property(result.txData, 'vout')
|
|
25
|
+
assert.equal(result.txData.isValidSlp, false)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -13,84 +13,170 @@ describe('#UTXO', () => {
|
|
|
13
13
|
|
|
14
14
|
if (process.env.IS_USING_FREE_TIER) await sleep(3000)
|
|
15
15
|
})
|
|
16
|
-
/*
|
|
17
|
-
describe('#get', () => {
|
|
18
|
-
it('should get hydrated and filtered UTXOs for an address', async () => {
|
|
19
|
-
// const addr = 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
|
|
20
|
-
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
|
|
21
|
-
|
|
22
|
-
const result = await bchjs.Utxo.get(addr)
|
|
23
|
-
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
17
|
+
if (process.env.TESTSLP) {
|
|
18
|
+
describe('#getOld', () => {
|
|
19
|
+
it('should get hydrated and filtered UTXOs for an address', async () => {
|
|
20
|
+
// const addr = 'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
|
|
21
|
+
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
|
|
22
|
+
|
|
23
|
+
const result = await bchjs.Utxo.getOld(addr)
|
|
24
|
+
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
25
|
+
|
|
26
|
+
assert.isArray(result)
|
|
27
|
+
assert.property(result[0], 'address')
|
|
28
|
+
assert.property(result[0], 'bchUtxos')
|
|
29
|
+
assert.property(result[0], 'nullUtxos')
|
|
30
|
+
assert.property(result[0], 'slpUtxos')
|
|
31
|
+
assert.isArray(result[0].bchUtxos)
|
|
32
|
+
assert.isArray(result[0].nullUtxos)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should handle an array of addresses', async () => {
|
|
36
|
+
const addr = [
|
|
37
|
+
'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9',
|
|
38
|
+
'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const result = await bchjs.Utxo.getOld(addr)
|
|
42
|
+
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
43
|
+
|
|
44
|
+
assert.isArray(result)
|
|
45
|
+
assert.property(result[0], 'address')
|
|
46
|
+
assert.property(result[0], 'bchUtxos')
|
|
47
|
+
assert.property(result[0], 'nullUtxos')
|
|
48
|
+
assert.property(result[0], 'slpUtxos')
|
|
49
|
+
assert.isArray(result[0].bchUtxos)
|
|
50
|
+
assert.isArray(result[0].nullUtxos)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should handle NFTs and minting batons', async () => {
|
|
54
|
+
const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj'
|
|
55
|
+
|
|
56
|
+
const result = await bchjs.Utxo.getOld(addr)
|
|
57
|
+
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
58
|
+
|
|
59
|
+
assert.isArray(result)
|
|
60
|
+
assert.property(result[0], 'address')
|
|
61
|
+
assert.property(result[0], 'bchUtxos')
|
|
62
|
+
assert.property(result[0], 'nullUtxos')
|
|
63
|
+
assert.property(result[0], 'slpUtxos')
|
|
64
|
+
assert.isArray(result[0].bchUtxos)
|
|
65
|
+
assert.isArray(result[0].nullUtxos)
|
|
66
|
+
|
|
67
|
+
assert.isArray(result[0].slpUtxos.type1.mintBatons)
|
|
68
|
+
assert.isArray(result[0].slpUtxos.type1.tokens)
|
|
69
|
+
assert.isArray(result[0].slpUtxos.nft.groupMintBatons)
|
|
70
|
+
assert.isArray(result[0].slpUtxos.nft.groupTokens)
|
|
71
|
+
assert.isArray(result[0].slpUtxos.nft.tokens)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should use the whitelist when flag is set', async () => {
|
|
75
|
+
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
|
|
76
|
+
const useWhitelist = true
|
|
77
|
+
|
|
78
|
+
const result = await bchjs.Utxo.getOld(addr, useWhitelist)
|
|
79
|
+
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
80
|
+
|
|
81
|
+
assert.isArray(result)
|
|
82
|
+
assert.property(result[0], 'address')
|
|
83
|
+
assert.property(result[0], 'bchUtxos')
|
|
84
|
+
assert.property(result[0], 'nullUtxos')
|
|
85
|
+
assert.property(result[0], 'slpUtxos')
|
|
86
|
+
assert.isArray(result[0].bchUtxos)
|
|
87
|
+
assert.isArray(result[0].nullUtxos)
|
|
88
|
+
|
|
89
|
+
// Most token UTXOs should end up in the nullUtxos array.
|
|
90
|
+
assert.isAbove(result[0].bchUtxos.length, 0)
|
|
91
|
+
assert.isAbove(result[0].nullUtxos.length, 1)
|
|
92
|
+
})
|
|
32
93
|
})
|
|
33
94
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'
|
|
37
|
-
'bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'
|
|
38
|
-
]
|
|
95
|
+
describe('#findBiggestUtxo', () => {
|
|
96
|
+
it('should sort UTXOs from Electrumx', async () => {
|
|
97
|
+
const addr = 'bitcoincash:qq54fgjn3hz0357n8a6guy4demw9xfkjk5jcj0xr0z'
|
|
39
98
|
|
|
40
|
-
|
|
41
|
-
|
|
99
|
+
const electrumxUtxos = await bchjs.Electrumx.utxo(addr)
|
|
100
|
+
// console.log(`Electrumx utxos: ${JSON.stringify(electrumxUtxos, null, 2)}`)
|
|
42
101
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
assert.property(result[0], 'bchUtxos')
|
|
46
|
-
assert.property(result[0], 'nullUtxos')
|
|
47
|
-
assert.property(result[0], 'slpUtxos')
|
|
48
|
-
assert.isArray(result[0].bchUtxos)
|
|
49
|
-
assert.isArray(result[0].nullUtxos)
|
|
50
|
-
})
|
|
102
|
+
const result = bchjs.Utxo.findBiggestUtxo(electrumxUtxos.utxos)
|
|
103
|
+
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
51
104
|
|
|
52
|
-
|
|
53
|
-
|
|
105
|
+
assert.property(result, 'satoshis')
|
|
106
|
+
assert.equal(result.satoshis, 800)
|
|
107
|
+
})
|
|
54
108
|
|
|
55
|
-
|
|
56
|
-
|
|
109
|
+
it('should sort UTXOs from Utxos.get()', async () => {
|
|
110
|
+
const addr = 'bitcoincash:qq54fgjn3hz0357n8a6guy4demw9xfkjk5jcj0xr0z'
|
|
57
111
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
assert.property(result[0], 'bchUtxos')
|
|
61
|
-
assert.property(result[0], 'nullUtxos')
|
|
62
|
-
assert.property(result[0], 'slpUtxos')
|
|
63
|
-
assert.isArray(result[0].bchUtxos)
|
|
64
|
-
assert.isArray(result[0].nullUtxos)
|
|
65
|
-
|
|
66
|
-
assert.isArray(result[0].slpUtxos.type1.mintBatons)
|
|
67
|
-
assert.isArray(result[0].slpUtxos.type1.tokens)
|
|
68
|
-
assert.isArray(result[0].slpUtxos.nft.groupMintBatons)
|
|
69
|
-
assert.isArray(result[0].slpUtxos.nft.groupTokens)
|
|
70
|
-
assert.isArray(result[0].slpUtxos.nft.tokens)
|
|
71
|
-
})
|
|
112
|
+
const utxos = await bchjs.Utxo.getOld(addr)
|
|
113
|
+
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
|
72
114
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const useWhitelist = true
|
|
115
|
+
const result = bchjs.Utxo.findBiggestUtxo(utxos[0].bchUtxos)
|
|
116
|
+
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
76
117
|
|
|
77
|
-
|
|
78
|
-
|
|
118
|
+
assert.property(result, 'satoshis')
|
|
119
|
+
assert.equal(result.satoshis, 800)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
describe('#hydrateTokenData', () => {
|
|
125
|
+
it('should hydrate token UTXOs', async () => {
|
|
126
|
+
const utxos = [
|
|
127
|
+
{
|
|
128
|
+
txid:
|
|
129
|
+
'384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c',
|
|
130
|
+
vout: 1,
|
|
131
|
+
type: 'token',
|
|
132
|
+
qty: '10000000',
|
|
133
|
+
tokenId:
|
|
134
|
+
'a436c8e1b6bee3d701c6044d190f76f774be83c36de8d34a988af4489e86dd37',
|
|
135
|
+
address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m'
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
txid:
|
|
139
|
+
'4fc789405d58ec612c69eba29aa56cf0c7f228349801114138424eb68df4479d',
|
|
140
|
+
vout: 1,
|
|
141
|
+
type: 'token',
|
|
142
|
+
qty: '100000000',
|
|
143
|
+
tokenId:
|
|
144
|
+
'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb',
|
|
145
|
+
address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m'
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
txid:
|
|
149
|
+
'42054bba4d69bfe7801ece0cffc754194b04239034fdfe9dbe321ef76c9a2d93',
|
|
150
|
+
vout: 5,
|
|
151
|
+
type: 'token',
|
|
152
|
+
qty: '4764',
|
|
153
|
+
tokenId:
|
|
154
|
+
'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f',
|
|
155
|
+
address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m'
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
txid:
|
|
159
|
+
'06938d0a0d15aa76524ffe61fe111d6d2b2ea9dd8dcd4c7c7744614ced370861',
|
|
160
|
+
vout: 5,
|
|
161
|
+
type: 'token',
|
|
162
|
+
qty: '238',
|
|
163
|
+
tokenId:
|
|
164
|
+
'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f',
|
|
165
|
+
address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m'
|
|
166
|
+
}
|
|
167
|
+
]
|
|
79
168
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
assert.property(result[0], '
|
|
84
|
-
assert.property(result[0], '
|
|
85
|
-
assert.
|
|
86
|
-
assert.
|
|
87
|
-
|
|
88
|
-
// Most token UTXOs should end up in the nullUtxos array.
|
|
89
|
-
assert.isAbove(result[0].bchUtxos.length, 0)
|
|
90
|
-
assert.isAbove(result[0].nullUtxos.length, 1)
|
|
169
|
+
const result = await bchjs.Utxo.hydrateTokenData(utxos)
|
|
170
|
+
// console.log('result: ', result)
|
|
171
|
+
|
|
172
|
+
assert.property(result[0], 'ticker')
|
|
173
|
+
assert.property(result[0], 'name')
|
|
174
|
+
assert.property(result[0], 'qtyStr')
|
|
175
|
+
assert.property(result[0], 'documentUri')
|
|
176
|
+
assert.property(result[0], 'documentHash')
|
|
91
177
|
})
|
|
92
178
|
})
|
|
93
|
-
|
|
179
|
+
|
|
94
180
|
describe('#get', () => {
|
|
95
181
|
it('should hydrate address with BCH and SLP UTXOs', async () => {
|
|
96
182
|
const addr = 'simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9'
|
|
@@ -122,36 +208,17 @@ describe('#UTXO', () => {
|
|
|
122
208
|
// Assert that minting batons are correctly identified.
|
|
123
209
|
assert.isAbove(result.slpUtxos.type1.mintBatons.length, 0)
|
|
124
210
|
})
|
|
125
|
-
})
|
|
126
|
-
/*
|
|
127
|
-
describe('#findBiggestUtxo', () => {
|
|
128
|
-
it('should sort UTXOs from Electrumx', async () => {
|
|
129
|
-
const addr = 'bitcoincash:qq54fgjn3hz0357n8a6guy4demw9xfkjk5jcj0xr0z'
|
|
130
211
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const result = bchjs.Utxo.findBiggestUtxo(electrumxUtxos.utxos)
|
|
135
|
-
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
212
|
+
it('should return UTXOs for address with no SLP tokens', async () => {
|
|
213
|
+
const addr = 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
|
|
136
214
|
|
|
137
|
-
|
|
138
|
-
assert.equal(result.satoshis, 800)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it('should sort UTXOs from Utxos.get()', async () => {
|
|
142
|
-
const addr = 'bitcoincash:qq54fgjn3hz0357n8a6guy4demw9xfkjk5jcj0xr0z'
|
|
143
|
-
|
|
144
|
-
const utxos = await bchjs.Utxo.get(addr)
|
|
145
|
-
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
|
146
|
-
|
|
147
|
-
const result = bchjs.Utxo.findBiggestUtxo(utxos[0].bchUtxos)
|
|
215
|
+
const result = await bchjs.Utxo.get(addr)
|
|
148
216
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
149
217
|
|
|
150
|
-
assert.
|
|
151
|
-
assert.equal(result.
|
|
218
|
+
assert.isAbove(result.bchUtxos.length, 0)
|
|
219
|
+
assert.equal(result.slpUtxos.type1.tokens.length, 0)
|
|
152
220
|
})
|
|
153
221
|
})
|
|
154
|
-
*/
|
|
155
222
|
})
|
|
156
223
|
|
|
157
224
|
function sleep (ms) {
|
|
@@ -328,6 +328,19 @@ const fulcrumUtxos01 = {
|
|
|
328
328
|
]
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
const fulcrumUtxos02 = {
|
|
332
|
+
success: true,
|
|
333
|
+
utxos: [
|
|
334
|
+
{
|
|
335
|
+
height: 674513,
|
|
336
|
+
tx_hash:
|
|
337
|
+
'705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04',
|
|
338
|
+
tx_pos: 3,
|
|
339
|
+
value: 38134
|
|
340
|
+
}
|
|
341
|
+
]
|
|
342
|
+
}
|
|
343
|
+
|
|
331
344
|
const psfSlpIndexerUtxos01 = {
|
|
332
345
|
balance: {
|
|
333
346
|
utxos: [
|
|
@@ -368,6 +381,101 @@ const psfSlpIndexerUtxos01 = {
|
|
|
368
381
|
}
|
|
369
382
|
}
|
|
370
383
|
|
|
384
|
+
const tokenUtxos01 = [
|
|
385
|
+
{
|
|
386
|
+
txid: '384e1b8197e8de7d38f98317af2cf5f6bcb50007c46943b3498a6fab6e8aeb7c',
|
|
387
|
+
vout: 1,
|
|
388
|
+
type: 'token',
|
|
389
|
+
qty: '10000000',
|
|
390
|
+
tokenId: 'a436c8e1b6bee3d701c6044d190f76f774be83c36de8d34a988af4489e86dd37',
|
|
391
|
+
address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m'
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
txid: '4fc789405d58ec612c69eba29aa56cf0c7f228349801114138424eb68df4479d',
|
|
395
|
+
vout: 1,
|
|
396
|
+
type: 'token',
|
|
397
|
+
qty: '100000000',
|
|
398
|
+
tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb',
|
|
399
|
+
address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m'
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
txid: '42054bba4d69bfe7801ece0cffc754194b04239034fdfe9dbe321ef76c9a2d93',
|
|
403
|
+
vout: 5,
|
|
404
|
+
type: 'token',
|
|
405
|
+
qty: '4764',
|
|
406
|
+
tokenId: 'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f',
|
|
407
|
+
address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m'
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
txid: '06938d0a0d15aa76524ffe61fe111d6d2b2ea9dd8dcd4c7c7744614ced370861',
|
|
411
|
+
vout: 5,
|
|
412
|
+
type: 'token',
|
|
413
|
+
qty: '238',
|
|
414
|
+
tokenId: 'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f',
|
|
415
|
+
address: 'bitcoincash:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvg8nfhq4m'
|
|
416
|
+
}
|
|
417
|
+
]
|
|
418
|
+
|
|
419
|
+
const genesisData01 = {
|
|
420
|
+
tokenData: {
|
|
421
|
+
type: 1,
|
|
422
|
+
ticker: 'sleven',
|
|
423
|
+
name: 'sleven',
|
|
424
|
+
tokenId: 'a436c8e1b6bee3d701c6044d190f76f774be83c36de8d34a988af4489e86dd37',
|
|
425
|
+
documentUri: 'sleven',
|
|
426
|
+
documentHash: '',
|
|
427
|
+
decimals: 7,
|
|
428
|
+
mintBatonIsActive: false,
|
|
429
|
+
tokensInCirculationBN: '770059999999',
|
|
430
|
+
tokensInCirculationStr: '770059999999',
|
|
431
|
+
blockCreated: 555483,
|
|
432
|
+
totalBurned: '7711234568',
|
|
433
|
+
totalMinted: '777771234567'
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const genesisData02 = {
|
|
438
|
+
tokenData: {
|
|
439
|
+
type: 1,
|
|
440
|
+
ticker: 'NAKAMOTO',
|
|
441
|
+
name: 'NAKAMOTO',
|
|
442
|
+
tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb',
|
|
443
|
+
documentUri: '',
|
|
444
|
+
documentHash: '',
|
|
445
|
+
decimals: 8,
|
|
446
|
+
mintBatonIsActive: false,
|
|
447
|
+
tokensInCirculationBN: '2099260968799900',
|
|
448
|
+
tokensInCirculationStr: '2099260968799900',
|
|
449
|
+
blockCreated: 555671,
|
|
450
|
+
totalBurned: '739031200100',
|
|
451
|
+
totalMinted: '2100000000000000'
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const genesisData03 = {
|
|
456
|
+
tokenData: {
|
|
457
|
+
type: 1,
|
|
458
|
+
ticker: 'AUDC',
|
|
459
|
+
name: 'AUD Coin',
|
|
460
|
+
tokenId: 'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f',
|
|
461
|
+
documentUri: 'audcoino@gmail.com',
|
|
462
|
+
documentHash: '',
|
|
463
|
+
decimals: 6,
|
|
464
|
+
mintBatonIsActive: false,
|
|
465
|
+
tokensInCirculationBN: '974791786216512742',
|
|
466
|
+
tokensInCirculationStr: '974791786216512742',
|
|
467
|
+
blockCreated: 603311,
|
|
468
|
+
totalBurned: '1025208213783487258',
|
|
469
|
+
totalMinted: '2000000000000000000'
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const noUtxoErr = {
|
|
474
|
+
success: false,
|
|
475
|
+
error:
|
|
476
|
+
'Key not found in database [bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7]'
|
|
477
|
+
}
|
|
478
|
+
|
|
371
479
|
module.exports = {
|
|
372
480
|
mockUtxoData,
|
|
373
481
|
mockHydratedUtxos,
|
|
@@ -375,5 +483,11 @@ module.exports = {
|
|
|
375
483
|
mockEveryUtxoType,
|
|
376
484
|
electrumxUtxos,
|
|
377
485
|
fulcrumUtxos01,
|
|
378
|
-
|
|
486
|
+
fulcrumUtxos02,
|
|
487
|
+
psfSlpIndexerUtxos01,
|
|
488
|
+
tokenUtxos01,
|
|
489
|
+
genesisData01,
|
|
490
|
+
genesisData02,
|
|
491
|
+
genesisData03,
|
|
492
|
+
noUtxoErr
|
|
379
493
|
}
|
package/test/unit/utxo-unit.js
CHANGED
|
@@ -189,6 +189,45 @@ describe('#utxo', () => {
|
|
|
189
189
|
})
|
|
190
190
|
})
|
|
191
191
|
|
|
192
|
+
describe('#hydrateTokenData', () => {
|
|
193
|
+
it('should hydrate token UTXOs', async () => {
|
|
194
|
+
// Mock dependencies
|
|
195
|
+
sandbox
|
|
196
|
+
.stub(bchjs.Utxo.psfSlpIndexer, 'tokenStats')
|
|
197
|
+
.onCall(0)
|
|
198
|
+
.resolves(mockData.genesisData01)
|
|
199
|
+
.onCall(1)
|
|
200
|
+
.resolves(mockData.genesisData02)
|
|
201
|
+
.onCall(2)
|
|
202
|
+
.resolves(mockData.genesisData03)
|
|
203
|
+
|
|
204
|
+
const result = await bchjs.Utxo.hydrateTokenData(mockData.tokenUtxos01)
|
|
205
|
+
// console.log('result: ', result)
|
|
206
|
+
|
|
207
|
+
assert.equal(result.length, 4)
|
|
208
|
+
assert.property(result[0], 'qtyStr')
|
|
209
|
+
assert.property(result[0], 'ticker')
|
|
210
|
+
assert.property(result[0], 'name')
|
|
211
|
+
assert.property(result[0], 'documentUri')
|
|
212
|
+
assert.property(result[0], 'documentHash')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should should catch and throw errors', async () => {
|
|
216
|
+
try {
|
|
217
|
+
// Force error
|
|
218
|
+
sandbox
|
|
219
|
+
.stub(bchjs.Utxo.psfSlpIndexer, 'tokenStats')
|
|
220
|
+
.rejects(new Error('test error'))
|
|
221
|
+
|
|
222
|
+
await bchjs.Utxo.hydrateTokenData(mockData.tokenUtxos01)
|
|
223
|
+
|
|
224
|
+
assert.fail('Unexpected code path')
|
|
225
|
+
} catch (err) {
|
|
226
|
+
assert.equal(err.message, 'test error')
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
192
231
|
describe('#get', () => {
|
|
193
232
|
it('should throw an error if input is not a string', async () => {
|
|
194
233
|
try {
|
|
@@ -210,6 +249,8 @@ describe('#utxo', () => {
|
|
|
210
249
|
sandbox
|
|
211
250
|
.stub(bchjs.Utxo.psfSlpIndexer, 'balance')
|
|
212
251
|
.resolves(mockData.psfSlpIndexerUtxos01)
|
|
252
|
+
// Mock function to return the same input. Good enough for this test.
|
|
253
|
+
sandbox.stub(bchjs.Utxo, 'hydrateTokenData').resolves(x => x)
|
|
213
254
|
|
|
214
255
|
const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj'
|
|
215
256
|
|
|
@@ -231,5 +272,30 @@ describe('#utxo', () => {
|
|
|
231
272
|
assert.equal(result.slpUtxos.type1.mintBatons.length, 1)
|
|
232
273
|
assert.equal(result.nullUtxos.length, 0)
|
|
233
274
|
})
|
|
275
|
+
|
|
276
|
+
it('should handle an address with no SLP UTXOs', async () => {
|
|
277
|
+
// mock dependencies
|
|
278
|
+
sandbox
|
|
279
|
+
.stub(bchjs.Utxo.electrumx, 'utxo')
|
|
280
|
+
.resolves(mockData.fulcrumUtxos02)
|
|
281
|
+
|
|
282
|
+
// Force psf-slp-indexer to return no UTXOs
|
|
283
|
+
sandbox
|
|
284
|
+
.stub(bchjs.Utxo.psfSlpIndexer, 'balance')
|
|
285
|
+
.rejects(mockData.noUtxoErr)
|
|
286
|
+
|
|
287
|
+
// Mock function to return the same input. Good enough for this test.
|
|
288
|
+
sandbox.stub(bchjs.Utxo, 'hydrateTokenData').resolves(() => [])
|
|
289
|
+
|
|
290
|
+
const addr = 'simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj'
|
|
291
|
+
|
|
292
|
+
const result = await bchjs.Utxo.get(addr)
|
|
293
|
+
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
294
|
+
|
|
295
|
+
assert.equal(result.bchUtxos.length, 1)
|
|
296
|
+
assert.equal(result.slpUtxos.type1.tokens.length, 0)
|
|
297
|
+
assert.equal(result.slpUtxos.type1.mintBatons.length, 0)
|
|
298
|
+
assert.equal(result.nullUtxos.length, 0)
|
|
299
|
+
})
|
|
234
300
|
})
|
|
235
301
|
})
|