@psf/bch-js 4.22.0 → 5.2.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@psf/bch-js",
3
- "version": "4.22.0",
3
+ "version": "5.2.1",
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": [
@@ -14,11 +14,11 @@
14
14
  "test:integration:nft": "export RESTURL=https://bchn.fullstack.cash/v5/ && export IS_USING_FREE_TIER=true && mocha --timeout 30000 -g '#nft1' test/integration/chains/bchn/slp.js",
15
15
  "test:integration:abc": "export RESTURL=https://abc.fullstack.cash/v5/ && export IS_USING_FREE_TIER=true && mocha --timeout 30000 test/integration/ && mocha --timeout 30000 test/integration/chains/abc/",
16
16
  "test:integration:bchn": "export RESTURL=https://bchn.fullstack.cash/v5/ && export IS_USING_FREE_TIER=true && mocha --timeout 30000 test/integration/ && mocha --timeout 30000 test/integration/chains/bchn/",
17
- "test:integration:testnet": "IS_USING_FREE_TIER=true mocha --timeout 30000 test/integration/chains/testnet/",
17
+ "test:integration:bchn:slpdb": "export TESTSLP=1 && export RESTURL=https://bchn.fullstack.cash/v5/ && export IS_USING_FREE_TIER=true && mocha --timeout 30000 test/integration/ && mocha --timeout 30000 test/integration/chains/bchn/",
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.129:3000/v5/ && mocha --timeout 30000 test/integration/ && mocha --timeout 30000 test/integration/chains/bchn/",
21
+ "test:integration:decatur:bchn": "export RESTURL=http://192.168.2.139: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/bch-js.js CHANGED
@@ -36,7 +36,6 @@ const DSProof = require('./dsproof')
36
36
  const Ecash = require('./ecash')
37
37
 
38
38
  // Indexers
39
- const Ninsight = require('./ninsight')
40
39
  const Electrumx = require('./electrumx')
41
40
  const PsfSlpIndexer = require('./psf-slp-indexer')
42
41
 
@@ -83,9 +82,6 @@ class BCHJS {
83
82
 
84
83
  // console.log(`apiToken: ${this.apiToken}`)
85
84
 
86
- // Bitcoin.com Ninsight indexer
87
- this.Ninsight = new Ninsight(config)
88
-
89
85
  // ElectrumX indexer
90
86
  this.Electrumx = new Electrumx(libConfig)
91
87
 
@@ -1,14 +1,22 @@
1
1
  /*
2
2
  This library interacts with the PSF slp indexer REST API endpoints operated
3
3
  by FullStack.cash
4
+
5
+ TODO:
6
+ - detect TXs from tokens in the blacklist.
4
7
  */
8
+
5
9
  // Public npm libraries
6
10
  const axios = require('axios')
7
11
 
12
+ // Local libraries
13
+ const RawTransaction = require('./raw-transactions')
14
+ const SlpUtils = require('./slp/utils')
15
+
8
16
  // let _this
9
17
 
10
18
  class PsfSlpIndexer {
11
- constructor (config) {
19
+ constructor (config = {}) {
12
20
  this.restURL = config.restURL
13
21
  this.apiToken = config.apiToken
14
22
  this.authToken = config.authToken
@@ -29,6 +37,10 @@ class PsfSlpIndexer {
29
37
  }
30
38
  }
31
39
 
40
+ // Encapsulate dependencies
41
+ this.rawTransaction = new RawTransaction(config)
42
+ this.slpUtils = new SlpUtils(config)
43
+
32
44
  // _this = this
33
45
  }
34
46
 
@@ -120,6 +132,7 @@ class PsfSlpIndexer {
120
132
  */
121
133
  async balance (address) {
122
134
  try {
135
+ console.log('balance() address: ', address)
123
136
  // Handle single address.
124
137
  if (typeof address === 'string') {
125
138
  const response = await axios.post(
@@ -283,8 +296,73 @@ class PsfSlpIndexer {
283
296
  }
284
297
  throw new Error('Input txid must be a string.')
285
298
  } catch (error) {
286
- if (error.response && error.response.data) throw error.response.data
287
- else throw error
299
+ // console.log('error: ', error)
300
+
301
+ // Case: txid is not stored in the psf-slp-indexer tx database.
302
+ // Response: If it's not in the database, then it can be assumed the TX
303
+ // is not a token TX?
304
+ if (
305
+ error.response &&
306
+ error.response.data &&
307
+ error.response.data.error &&
308
+ error.response.data.error.includes('Key not found in database')
309
+ ) {
310
+ // console.log(
311
+ // 'TX not found in psf-slp-indexer. Retrieving from full node.'
312
+ // )
313
+
314
+ // Check if this txid belongs to a blacklisted token.
315
+ const isInBlacklist = await this.checkBlacklist(txid)
316
+
317
+ // Get the TX Details from the full node.
318
+ const txDetails = await this.rawTransaction.getTxData(txid)
319
+ // console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`)
320
+
321
+ if (isInBlacklist) {
322
+ txDetails.isValidSlp = null
323
+ } else {
324
+ txDetails.isValidSlp = false
325
+ }
326
+
327
+ const outObj = {
328
+ txData: txDetails
329
+ }
330
+
331
+ return outObj
332
+ } else throw error
333
+ }
334
+ }
335
+
336
+ // Check if the txid has an OP_RETURN containing a tokenID that is in the
337
+ // blacklist. In that case, the isValidSlp property should be marked as
338
+ // null, and not false.
339
+ async checkBlacklist (txid) {
340
+ try {
341
+ // TODO: Add endpoint to psf-slp-indexer to retrieve current blacklist.
342
+ // This should be done once at startup, and not each time this function
343
+ // is called.
344
+ const blacklist = [
345
+ 'dd21be4532d93661e8ffe16db6535af0fb8ee1344d1fef81a193e2b4cfa9fbc9'
346
+ ]
347
+
348
+ const outTokenData = await this.slpUtils.decodeOpReturn(txid)
349
+ // console.log('outTokenData: ', outTokenData)
350
+
351
+ // Loop through each token in the blacklist.
352
+ for (let i = 0; i < blacklist.length; i++) {
353
+ // If a match is found, return true.
354
+ if (outTokenData.tokenId === blacklist[i]) {
355
+ return true
356
+ }
357
+ }
358
+
359
+ // By default, return false.
360
+ return false
361
+ } catch (err) {
362
+ // console.log(err)
363
+
364
+ // Exit quietly.
365
+ return false
288
366
  }
289
367
  }
290
368
  }
package/src/slp/utils.js CHANGED
@@ -11,7 +11,7 @@ const Util = require('../util')
11
11
  let _this
12
12
 
13
13
  class Utils {
14
- constructor (config) {
14
+ constructor (config = {}) {
15
15
  this.restURL = config.restURL
16
16
  this.apiToken = config.apiToken
17
17
  this.slpParser = slpParser
@@ -2,18 +2,27 @@
2
2
  High-level functions for working with Transactions
3
3
  */
4
4
 
5
+ // Global npm libraries
5
6
  const BigNumber = require('bignumber.js')
6
7
 
8
+ // Local libraries
7
9
  const RawTransaction = require('./raw-transactions')
8
10
  const SlpUtils = require('./slp/utils')
9
11
  const Blockchain = require('./blockchain')
12
+ const PsfSlpIndexer = require('./psf-slp-indexer')
10
13
 
11
14
  class Transaction {
12
- constructor (config) {
15
+ constructor (config = {}) {
13
16
  // Encapsulate dependencies
14
17
  this.slpUtils = new SlpUtils(config)
15
18
  this.rawTransaction = new RawTransaction(config)
16
19
  this.blockchain = new Blockchain(config)
20
+ this.psfSlpIndexer = new PsfSlpIndexer(config)
21
+ }
22
+
23
+ // Proxy the call to the psf-slp-indexer.
24
+ async get (txid) {
25
+ return await this.psfSlpIndexer.tx(txid)
17
26
  }
18
27
 
19
28
  /**
@@ -42,7 +51,7 @@ class Transaction {
42
51
  // CT 10/31/21: TODO: this function should be refactored to use get2(), but
43
52
  // add waterfall validation of the TX and its inputs.
44
53
 
45
- async get (txid) {
54
+ async getOld (txid) {
46
55
  try {
47
56
  if (typeof txid !== 'string') {
48
57
  throw new Error(
package/src/utxo.js CHANGED
@@ -8,19 +8,23 @@
8
8
  // Local libraries
9
9
  const Electrumx = require('./electrumx')
10
10
  const Slp = require('./slp/slp')
11
+ const PsfSlpIndexer = require('./psf-slp-indexer')
12
+ const BigNumber = require('bignumber.js')
11
13
 
12
14
  class UTXO {
13
- constructor (config) {
15
+ constructor (config = {}) {
14
16
  // Encapsulate dependencies for easier mocking.
15
17
  this.electrumx = new Electrumx(config)
16
18
  this.slp = new Slp(config)
19
+ this.psfSlpIndexer = new PsfSlpIndexer(config)
20
+ this.BigNumber = BigNumber
17
21
  }
18
22
 
19
23
  /**
20
- * @api Utxo.get() get()
21
- * @apiName get
24
+ * @api Utxo.getOld() getOld()
25
+ * @apiName getOld
22
26
  * @apiGroup UTXO
23
- * @apiDescription Get UTXOs for an address
27
+ * @apiDescription Get UTXOs for an address (from SLPDB)
24
28
  *
25
29
  * Given an address, this function will return an object with thre following
26
30
  * properties:
@@ -45,7 +49,7 @@ class UTXO {
45
49
  * @apiExample Example usage:
46
50
  * (async () => {
47
51
  * try {
48
- * let utxos = await bchjs.Utxo.get('simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj');
52
+ * let utxos = await bchjs.Utxo.getOld('simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj');
49
53
  * console.log(utxos);
50
54
  * } catch(error) {
51
55
  * console.error(error)
@@ -181,7 +185,7 @@ class UTXO {
181
185
  *
182
186
  *
183
187
  */
184
- async get (address, useWhitelist = false) {
188
+ async getOld (address, useWhitelist = false) {
185
189
  try {
186
190
  if (!address) {
187
191
  throw new Error('Address must be an array or a string')
@@ -286,6 +290,207 @@ class UTXO {
286
290
  }
287
291
  }
288
292
 
293
+ /**
294
+ * @api Utxo.get() get()
295
+ * @apiName get
296
+ * @apiGroup UTXO
297
+ * @apiDescription Get UTXOs for an address (from psf-slp-indexer)
298
+ *
299
+ * Given an address, this function will return an object with thre following
300
+ * properties:
301
+ * - address: "" - the address these UTXOs are associated with
302
+ * - bchUtxos: [] - UTXOs confirmed to be spendable as normal BCH
303
+ * - nullUtxo: [] - UTXOs that did not pass SLP validation. Should be ignored and
304
+ * not spent, to be safe.
305
+ * - slpUtxos: {} - UTXOs confirmed to be colored as valid SLP tokens
306
+ * - type1: {}
307
+ * - tokens: [] - SLP token Type 1 tokens.
308
+ * - mintBatons: [] - SLP token Type 1 mint batons.
309
+ * - nft: {}
310
+ * - tokens: [] - NFT tokens
311
+ * - groupTokens: [] - NFT Group tokens, used to create NFT tokens.
312
+ * - groupMintBatons: [] - Minting baton to create more NFT Group tokens.
313
+ *
314
+ * Note: You can pass in an optional second Boolean argument. The default
315
+ * `false` will use the normal waterfall validation method. Set to `true`,
316
+ * SLP UTXOs will be validated with the whitelist filtered SLPDB. This will
317
+ * result is many more UTXOs in the `nullUtxos` array.
318
+ *
319
+ * @apiExample Example usage:
320
+ * (async () => {
321
+ * try {
322
+ * let utxos = await bchjs.Utxo.get('simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj');
323
+ * console.log(utxos);
324
+ * } catch(error) {
325
+ * console.error(error)
326
+ * }
327
+ * })()
328
+ *
329
+ * // returns
330
+ * [
331
+ * {
332
+ * "address": "bitcoincash:qrm0c67wwqh0w7wjxua2gdt2xggnm90xws00a3lezv",
333
+ * "bchUtxos": [
334
+ * {
335
+ * "height": 674513,
336
+ * "tx_hash": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04",
337
+ * "tx_pos": 3,
338
+ * "value": 38134,
339
+ * "txid": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04",
340
+ * "vout": 3,
341
+ * "isValid": false
342
+ * }
343
+ * ],
344
+ */
345
+ // This version of get() uses the psf-slp-indexer. It will replace the older
346
+ // get() function that uses SLPDB.
347
+ // TODO: NFT UTXOs are identified as non-token UTXOs, which will cause a wallet
348
+ // to burn them. The psf-slp-indexer needs to be updated to mark these UTXOs.
349
+ async get (address) {
350
+ try {
351
+ // Convert address to an array if it is a string.
352
+ if (typeof address !== 'string') {
353
+ throw new Error('address input must be a string')
354
+ }
355
+
356
+ // Ensure the address is a BCH address.
357
+ const addr = this.slp.Address.toCashAddress(address)
358
+
359
+ // Get the UTXOs associated with the address.
360
+ const utxoData = await this.electrumx.utxo(addr)
361
+ // console.log(`utxoData: ${JSON.stringify(utxoData, null, 2)}`)
362
+ const utxos = utxoData.utxos
363
+
364
+ // Get SLP UTXOs from the psf-slp-indexer
365
+ const slpUtxoData = await this.psfSlpIndexer.balance(addr)
366
+ // console.log(`slpUtxoData: ${JSON.stringify(slpUtxoData, null, 2)}`)
367
+ const slpUtxos = slpUtxoData.balance.utxos
368
+
369
+ // Loop through the Fulcrum UTXOs.
370
+ for (let i = 0; i < utxos.length; i++) {
371
+ const thisUtxo = utxos[i]
372
+
373
+ // Loop through the UTXOs from psf-slp-indexer.
374
+ for (let j = 0; j < slpUtxos.length; j++) {
375
+ const thisSlpUtxo = slpUtxos[j]
376
+
377
+ // If the non-hydrated UTXO matches the SLP UTXO, then combine the data
378
+ // and mark the UTXO as an SLP token.
379
+ if (
380
+ thisUtxo.tx_hash === thisSlpUtxo.txid &&
381
+ thisUtxo.tx_pos === thisSlpUtxo.vout
382
+ ) {
383
+ thisUtxo.txid = thisUtxo.tx_hash
384
+ thisUtxo.vout = thisUtxo.tx_pos
385
+ thisUtxo.isSlp = true
386
+ thisUtxo.type = thisSlpUtxo.type
387
+ thisUtxo.qty = thisSlpUtxo.qty
388
+ thisUtxo.tokenId = thisSlpUtxo.tokenId
389
+ thisUtxo.address = thisSlpUtxo.address
390
+
391
+ break
392
+ }
393
+ }
394
+
395
+ // If there was no match, then this is a normal BCH UTXO. Mark it as such.
396
+ if (!thisUtxo.isSlp) {
397
+ thisUtxo.txid = thisUtxo.tx_hash
398
+ thisUtxo.vout = thisUtxo.tx_pos
399
+ thisUtxo.isSlp = false
400
+ thisUtxo.address = addr
401
+ }
402
+ }
403
+
404
+ // Get token UTXOs
405
+ let type1TokenUtxos = utxos.filter(
406
+ x => x.isSlp === true && x.type === 'token'
407
+ )
408
+
409
+ // Hydrate the UTXOs with additional token data.
410
+ type1TokenUtxos = await this.hydrateTokenData(type1TokenUtxos)
411
+
412
+ const bchUtxos = utxos.filter(x => x.isSlp === false)
413
+ const type1BatonUtxos = utxos.filter(
414
+ x => x.isSlp === true && x.type === 'baton'
415
+ )
416
+ const nullUtxos = utxos.filter(x => x.isSlp === null)
417
+
418
+ const outObj = {
419
+ address: addr,
420
+ bchUtxos,
421
+ slpUtxos: {
422
+ type1: {
423
+ tokens: type1TokenUtxos,
424
+ mintBatons: type1BatonUtxos
425
+ },
426
+ nft: {} // Allocated for future support of NFT spec.
427
+ },
428
+ nullUtxos
429
+ }
430
+
431
+ return outObj
432
+ } catch (err) {
433
+ // console.error('Error in bchjs.utxo.get2(): ', err)
434
+
435
+ if (err.error) throw new Error(err.error)
436
+ throw err
437
+ }
438
+ }
439
+
440
+ // Hydrate an array of token UTXOs with token information.
441
+ // Returns an array of token UTXOs with additional data.
442
+ async hydrateTokenData (utxoAry) {
443
+ try {
444
+ // console.log('utxoAry: ', utxoAry)
445
+
446
+ // Create a list of token IDs without duplicates.
447
+ let tokenIds = utxoAry.map(x => x.tokenId)
448
+
449
+ // Remove duplicates. https://stackoverflow.com/questions/9229645/remove-duplicate-values-from-js-array
450
+ tokenIds = [...new Set(tokenIds)]
451
+ // console.log('tokenIds: ', tokenIds)
452
+
453
+ // Get Genesis data for each tokenId
454
+ const genesisData = []
455
+ for (let i = 0; i < tokenIds.length; i++) {
456
+ const thisTokenId = tokenIds[i]
457
+ const thisTokenData = await this.psfSlpIndexer.tokenStats(thisTokenId)
458
+ // console.log('thisTokenData: ', thisTokenData)
459
+
460
+ genesisData.push(thisTokenData)
461
+ }
462
+ // console.log('genesisData: ', genesisData)
463
+
464
+ // Hydrate each token UTXO with data from the genesis transaction.
465
+ for (let i = 0; i < utxoAry.length; i++) {
466
+ const thisUtxo = utxoAry[i]
467
+
468
+ // Get the genesis data for this token.
469
+ const genData = genesisData.filter(
470
+ x => x.tokenData.tokenId === thisUtxo.tokenId
471
+ )
472
+ // console.log('genData: ', genData)
473
+
474
+ thisUtxo.ticker = genData[0].tokenData.ticker
475
+ thisUtxo.name = genData[0].tokenData.name
476
+ thisUtxo.documentUri = genData[0].tokenData.documentUri
477
+ thisUtxo.documentHash = genData[0].tokenData.documentHash
478
+ thisUtxo.decimals = genData[0].tokenData.decimals
479
+
480
+ // Calculate the real token quantity
481
+ const qty = new BigNumber(thisUtxo.qty).dividedBy(
482
+ 10 ** parseInt(thisUtxo.decimals)
483
+ )
484
+ thisUtxo.qtyStr = qty.toString()
485
+ }
486
+
487
+ return utxoAry
488
+ } catch (err) {
489
+ console.log('Error in hydrateTokenData()')
490
+ throw err
491
+ }
492
+ }
493
+
289
494
  /**
290
495
  * @api Utxo.findBiggestUtxo() findBiggestUtxo()
291
496
  * @apiName findBiggestUtxo
@@ -54,9 +54,9 @@ describe('#psf-slp-indexer', () => {
54
54
  })
55
55
 
56
56
  describe('#tx', () => {
57
- it('should get hydrated tx data', async () => {
57
+ it('should get hydrated tx data for an SLP transaction', async () => {
58
58
  const txid =
59
- '83361c34cac2ea7f9ca287fca57a96cc0763719f0cdf4850f9696c1e68eb635c'
59
+ '947ccb2a0d62ca287bc4b0993874ab0f9f6afd454193e631e2bf84dca66731fc'
60
60
 
61
61
  const result = await bchjs.PsfSlpIndexer.tx(txid)
62
62
  // console.log('result: ', result)
@@ -64,6 +64,53 @@ describe('#psf-slp-indexer', () => {
64
64
  assert.property(result.txData, 'vin')
65
65
  assert.property(result.txData, 'vout')
66
66
  assert.property(result.txData, 'isValidSlp')
67
+ assert.equal(result.txData.isValidSlp, true)
68
+ })
69
+
70
+ it('should mark non-SLP transaction as false', async () => {
71
+ const txid =
72
+ '03d6e6b63647ce7b02ecc73dc6d41b485be14a3e20eed4474b8a840358ddf14e'
73
+
74
+ const result = await bchjs.PsfSlpIndexer.tx(txid)
75
+ // console.log('result: ', result)
76
+
77
+ assert.property(result.txData, 'vin')
78
+ assert.property(result.txData, 'vout')
79
+ assert.property(result.txData, 'isValidSlp')
80
+ assert.equal(result.txData.isValidSlp, false)
81
+ })
82
+
83
+ // FlexUSD transactions
84
+ // Currently FlexUSD UTXOs are reported as invalid SLP UTXOs, which means
85
+ // the wallet will burn them. There is a TODO in the code. This test will
86
+ // need to be changed when it is done.
87
+ it('should mark blacklisted token as null', async () => {
88
+ const txid =
89
+ '302113c11b90edc5f36c073d2f8a75e1e0eaf59b56235491a843d3819cd6a85f'
90
+
91
+ const result = await bchjs.PsfSlpIndexer.tx(txid)
92
+ // console.log('result: ', result)
93
+
94
+ assert.property(result.txData, 'vin')
95
+ assert.property(result.txData, 'vout')
96
+ assert.property(result.txData, 'isValidSlp')
97
+ assert.equal(result.txData.isValidSlp, null)
98
+ })
99
+
100
+ it('should throw error for non-existent txid', async () => {
101
+ try {
102
+ const txid =
103
+ '302113c11b90edc5f36c073d2f8a75e1e0eaf59b56235491a843d3819cd6a85e'
104
+
105
+ await bchjs.PsfSlpIndexer.tx(txid)
106
+ // console.log('result: ', result)
107
+
108
+ assert.fail('Unexpected code path')
109
+ } catch (err) {
110
+ // console.log(err)
111
+
112
+ assert.include(err.message, 'No such mempool or blockchain transaction')
113
+ }
67
114
  })
68
115
  })
69
116
  })