@psf/bch-js 4.22.1 → 5.2.2

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.1",
3
+ "version": "5.2.2",
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.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,8 @@ class PsfSlpIndexer {
120
132
  */
121
133
  async balance (address) {
122
134
  try {
135
+ // console.log('balance() address: ', address)
136
+
123
137
  // Handle single address.
124
138
  if (typeof address === 'string') {
125
139
  const response = await axios.post(
@@ -283,8 +297,73 @@ class PsfSlpIndexer {
283
297
  }
284
298
  throw new Error('Input txid must be a string.')
285
299
  } catch (error) {
286
- if (error.response && error.response.data) throw error.response.data
287
- else throw error
300
+ // console.log('error: ', error)
301
+
302
+ // Case: txid is not stored in the psf-slp-indexer tx database.
303
+ // Response: If it's not in the database, then it can be assumed the TX
304
+ // is not a token TX?
305
+ if (
306
+ error.response &&
307
+ error.response.data &&
308
+ error.response.data.error &&
309
+ error.response.data.error.includes('Key not found in database')
310
+ ) {
311
+ // console.log(
312
+ // 'TX not found in psf-slp-indexer. Retrieving from full node.'
313
+ // )
314
+
315
+ // Check if this txid belongs to a blacklisted token.
316
+ const isInBlacklist = await this.checkBlacklist(txid)
317
+
318
+ // Get the TX Details from the full node.
319
+ const txDetails = await this.rawTransaction.getTxData(txid)
320
+ // console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`)
321
+
322
+ if (isInBlacklist) {
323
+ txDetails.isValidSlp = null
324
+ } else {
325
+ txDetails.isValidSlp = false
326
+ }
327
+
328
+ const outObj = {
329
+ txData: txDetails
330
+ }
331
+
332
+ return outObj
333
+ } else throw error
334
+ }
335
+ }
336
+
337
+ // Check if the txid has an OP_RETURN containing a tokenID that is in the
338
+ // blacklist. In that case, the isValidSlp property should be marked as
339
+ // null, and not false.
340
+ async checkBlacklist (txid) {
341
+ try {
342
+ // TODO: Add endpoint to psf-slp-indexer to retrieve current blacklist.
343
+ // This should be done once at startup, and not each time this function
344
+ // is called.
345
+ const blacklist = [
346
+ 'dd21be4532d93661e8ffe16db6535af0fb8ee1344d1fef81a193e2b4cfa9fbc9'
347
+ ]
348
+
349
+ const outTokenData = await this.slpUtils.decodeOpReturn(txid)
350
+ // console.log('outTokenData: ', outTokenData)
351
+
352
+ // Loop through each token in the blacklist.
353
+ for (let i = 0; i < blacklist.length; i++) {
354
+ // If a match is found, return true.
355
+ if (outTokenData.tokenId === blacklist[i]) {
356
+ return true
357
+ }
358
+ }
359
+
360
+ // By default, return false.
361
+ return false
362
+ } catch (err) {
363
+ // console.log(err)
364
+
365
+ // Exit quietly.
366
+ return false
288
367
  }
289
368
  }
290
369
  }
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,215 @@ 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
+ *
315
+ * @apiExample Example usage:
316
+ * (async () => {
317
+ * try {
318
+ * let utxos = await bchjs.Utxo.get('simpleledger:qrm0c67wwqh0w7wjxua2gdt2xggnm90xwsr5k22euj');
319
+ * console.log(utxos);
320
+ * } catch(error) {
321
+ * console.error(error)
322
+ * }
323
+ * })()
324
+ *
325
+ * // returns
326
+ * [
327
+ * {
328
+ * "address": "bitcoincash:qrm0c67wwqh0w7wjxua2gdt2xggnm90xws00a3lezv",
329
+ * "bchUtxos": [
330
+ * {
331
+ * "height": 674513,
332
+ * "tx_hash": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04",
333
+ * "tx_pos": 3,
334
+ * "value": 38134,
335
+ * "txid": "705bcc442e5a2770e560b528f52a47b1dcc9ce9ab6a8de9dfdefa55177f00d04",
336
+ * "vout": 3,
337
+ * "isValid": false
338
+ * }
339
+ * ],
340
+ */
341
+ // This version of get() uses the psf-slp-indexer. It will replace the older
342
+ // get() function that uses SLPDB.
343
+ // TODO: NFT UTXOs are identified as non-token UTXOs, which will cause a wallet
344
+ // to burn them. The psf-slp-indexer needs to be updated to mark these UTXOs.
345
+ async get (address) {
346
+ try {
347
+ // Convert address to an array if it is a string.
348
+ if (typeof address !== 'string') {
349
+ throw new Error('address input must be a string')
350
+ }
351
+
352
+ // Ensure the address is a BCH address.
353
+ const addr = this.slp.Address.toCashAddress(address)
354
+
355
+ // Get the UTXOs associated with the address.
356
+ const utxoData = await this.electrumx.utxo(addr)
357
+ // console.log(`utxoData: ${JSON.stringify(utxoData, null, 2)}`)
358
+ const utxos = utxoData.utxos
359
+
360
+ let slpUtxos = []
361
+
362
+ // Get SLP UTXOs from the psf-slp-indexer
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
+ }
376
+
377
+ // Loop through the Fulcrum UTXOs.
378
+ for (let i = 0; i < utxos.length; i++) {
379
+ const thisUtxo = utxos[i]
380
+
381
+ // Loop through the UTXOs from psf-slp-indexer.
382
+ for (let j = 0; j < slpUtxos.length; j++) {
383
+ const thisSlpUtxo = slpUtxos[j]
384
+
385
+ // If the non-hydrated UTXO matches the SLP UTXO, then combine the data
386
+ // and mark the UTXO as an SLP token.
387
+ if (
388
+ thisUtxo.tx_hash === thisSlpUtxo.txid &&
389
+ thisUtxo.tx_pos === thisSlpUtxo.vout
390
+ ) {
391
+ thisUtxo.txid = thisUtxo.tx_hash
392
+ thisUtxo.vout = thisUtxo.tx_pos
393
+ thisUtxo.isSlp = true
394
+ thisUtxo.type = thisSlpUtxo.type
395
+ thisUtxo.qty = thisSlpUtxo.qty
396
+ thisUtxo.tokenId = thisSlpUtxo.tokenId
397
+ thisUtxo.address = thisSlpUtxo.address
398
+
399
+ break
400
+ }
401
+ }
402
+
403
+ // If there was no match, then this is a normal BCH UTXO. Mark it as such.
404
+ if (!thisUtxo.isSlp) {
405
+ thisUtxo.txid = thisUtxo.tx_hash
406
+ thisUtxo.vout = thisUtxo.tx_pos
407
+ thisUtxo.isSlp = false
408
+ thisUtxo.address = addr
409
+ }
410
+ }
411
+
412
+ // Get token UTXOs
413
+ let type1TokenUtxos = utxos.filter(
414
+ x => x.isSlp === true && x.type === 'token'
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)
421
+ const type1BatonUtxos = utxos.filter(
422
+ x => x.isSlp === true && x.type === 'baton'
423
+ )
424
+ const nullUtxos = utxos.filter(x => x.isSlp === null)
425
+
426
+ const outObj = {
427
+ address: addr,
428
+ bchUtxos,
429
+ slpUtxos: {
430
+ type1: {
431
+ tokens: type1TokenUtxos,
432
+ mintBatons: type1BatonUtxos
433
+ },
434
+ nft: {} // Allocated for future support of NFT spec.
435
+ },
436
+ nullUtxos
437
+ }
438
+
439
+ return outObj
440
+ } catch (err) {
441
+ // console.error('Error in bchjs.utxo.get2(): ', err)
442
+
443
+ if (err.error) throw new Error(err.error)
444
+ throw err
445
+ }
446
+ }
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
+
289
502
  /**
290
503
  * @api Utxo.findBiggestUtxo() findBiggestUtxo()
291
504
  * @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
  })