@psf/bch-js 6.1.0 → 6.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@psf/bch-js",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "description": "A JavaScript library for working with Bitcoin Cash, eCash, and SLP Tokens",
5
5
  "author": "Chris Troutner <chris.troutner@gmail.com>",
6
6
  "contributors": [
package/src/slp/utils.js CHANGED
@@ -3,7 +3,6 @@
3
3
  // Public npm libraries
4
4
  const axios = require('axios')
5
5
  const slpParser = require('slp-parser')
6
- const BigNumber = require('bignumber.js')
7
6
 
8
7
  // Local libraries
9
8
  const Util = require('../util')
@@ -183,526 +182,6 @@ class Utils {
183
182
  throw error
184
183
  }
185
184
  }
186
-
187
- /**
188
- * @api SLP.Utils.tokenUtxoDetails() tokenUtxoDetails()
189
- * @apiName tokenUtxoDetails
190
- * @apiGroup SLP Utils
191
- * @apiDescription Hydrate a UTXO with SLP token metadata.
192
- *
193
- * Expects an array of UTXO objects as input. Returns an array of equal size.
194
- * Returns UTXO data hydrated with token information.
195
- *
196
- * - If the UTXO does not belong to a SLP transaction, it will return an
197
- * `isValid` property set to `false`.
198
- *
199
- * - If the UTXO is part of an SLP transaction, it will return the UTXO object
200
- * with additional SLP information attached. An `isValid` property will be
201
- * included.
202
- * - If the `isValid` property is `true`, the UTXO is a valid SLP UTXO.
203
- * - If the `isValid` property is `null`, then SLPDB has not yet processed
204
- * that txid and validity has not been confirmed, or a 429 rate-limit error
205
- * was enountered during the processing of the request.
206
- *
207
- * An optional second input object, `usrObj`, allows the user to inject an
208
- * artifical delay while processing UTXOs. If `usrObj.utxoDelay` is set to
209
- * a number, the call will delay by that number of milliseconds between
210
- * processing UTXOs.
211
- *
212
- * This is an API-heavy call. If you get a lot of `null` values, then slow down
213
- * the calls by using the usrObj.utxoDelay property, or request info on fewer
214
- * UTXOs at a
215
- * time. `null` indicates that the UTXO can *not* be safely spent, because
216
- * a judgement as to weather it is a token UTXO has not been made. Spending it
217
- * could burn tokens. It's safest to ignore UTXOs with a value of `null`.
218
- *
219
- *
220
- * @apiExample Example usage:
221
- *
222
- * (async () => {
223
- * try {
224
- * const utxos = await bchjs.Electrumx.utxo(`bitcoincash:qpcqs0n5xap26un2828n55gan2ylj7wavvzeuwdx05`)
225
- *
226
- * // Delay 100mS between processing UTXOs, to prevent rate-limit errors.
227
- * const utxoInfo = await bchjs.SLP.Utils.tokenUtxoDetails(utxos, { utxoDelay: 100 })
228
- *
229
- * console.log(`utxoInfo: ${JSON.stringify(utxoInfo, null, 2)}`)
230
- * } catch (error) {
231
- * console.error(error)
232
- * }
233
- * })()
234
- *
235
- * // returns
236
- * {
237
- * "txid": "fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb",
238
- * "vout": 1,
239
- * "amount": 0.00000546,
240
- * "satoshis": 546,
241
- * "height": 596089,
242
- * "confirmations": 748,
243
- * "utxoType": "token",
244
- * "tokenId": "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7",
245
- * "tokenTicker": "TOK-CH",
246
- * "tokenName": "TokyoCash",
247
- * "tokenDocumentUrl": "",
248
- * "tokenDocumentHash": "",
249
- * "decimals": 8,
250
- * "tokenQty": 2,
251
- * "isValid": true,
252
- * "tokenType": 1
253
- * }
254
- */
255
- async tokenUtxoDetails (utxos, usrObj = null) {
256
- try {
257
- // Throw error if input is not an array.
258
- if (!Array.isArray(utxos)) throw new Error('Input must be an array.')
259
-
260
- // console.log(`tokenUtxoDetails usrObj: ${JSON.stringify(usrObj, null, 2)}`)
261
-
262
- // Loop through each element in the array and validate the input before
263
- // further processing.
264
- for (let i = 0; i < utxos.length; i++) {
265
- const utxo = utxos[i]
266
-
267
- // Ensure the UTXO has a txid or tx_hash property.
268
- if (!utxo.txid) {
269
- // If Electrumx, convert the tx_hash property to txid.
270
- if (utxo.tx_hash) {
271
- utxo.txid = utxo.tx_hash
272
- } else {
273
- // If there is neither a txid or tx_hash property, throw an error.
274
- throw new Error(
275
- `utxo ${i} does not have a txid or tx_hash property.`
276
- )
277
- }
278
- }
279
-
280
- // Ensure the UTXO has a vout or tx_pos property.
281
- if (!Number.isInteger(utxo.vout)) {
282
- if (Number.isInteger(utxo.tx_pos)) {
283
- utxo.vout = utxo.tx_pos
284
- } else {
285
- throw new Error(
286
- `utxo ${i} does not have a vout or tx_pos property.`
287
- )
288
- }
289
- }
290
- }
291
-
292
- // Hydrate each UTXO with data from SLP OP_REUTRNs.
293
- const outAry = await this._hydrateUtxo(utxos, usrObj)
294
- // console.log(`outAry: ${JSON.stringify(outAry, null, 2)}`)
295
-
296
- // *After* each UTXO has been hydrated with SLP data,
297
- // validate the TXID with SLPDB.
298
- for (let i = 0; i < outAry.length; i++) {
299
- const utxo = outAry[i]
300
-
301
- // *After* the UTXO has been hydrated with SLP data,
302
- // validate the TXID with SLPDB.
303
- if (utxo.tokenType) {
304
- // Only execute this code-path if the current UTXO has a 'tokenType'
305
- // property. i.e. it has been successfully hydrated with SLP
306
- // information.
307
-
308
- // Validate using a 'waterfall' of validators.
309
- utxo.isValid = await this.waterfallValidateTxid(utxo.txid, usrObj)
310
- // console.log(`isValid: ${JSON.stringify(utxo.isValid, null, 2)}`)
311
- }
312
- }
313
-
314
- return outAry
315
- } catch (error) {
316
- // console.log('Error in tokenUtxoDetails()')
317
- if (error.response && error.response.data) throw error.response.data
318
- throw error
319
- }
320
- }
321
-
322
- // This is a private function that is called by tokenUtxoDetails().
323
- // It loops through an array of UTXOs and tries to hydrate them with SLP
324
- // token information from the OP_RETURN data.
325
- //
326
- // This function makes several calls to decodeOpReturn() to retrieve SLP
327
- // token data. If that call throws an error due to hitting rate limits, this
328
- // function will not throw an error. Instead, it will mark the `isValid`
329
- // property as `null`
330
- //
331
- // Exception to the above: It *will* throw an error if decodeOpReturn() throws
332
- // an error while trying to get the Genesis transaction for a Send or Mint
333
- // transaction. However, that is a rare occurence since the cache of
334
- // decodeOpReturn() will minimize API calls for this case. This behavior
335
- // could be changed, but right now it's a corner case of a corner case.
336
- //
337
- // If the usrObj has a utxoDelay property, then it will delay the loop for
338
- // each UTXO by that many milliseconds.
339
- async _hydrateUtxo (utxos, usrObj = null) {
340
- try {
341
- const decodeOpReturnCache = {}
342
-
343
- // console.log(`_hydrateUtxo usrObj: ${JSON.stringify(usrObj, null, 2)}`)
344
-
345
- // Output Array
346
- const outAry = []
347
-
348
- // Loop through each utxo
349
- for (let i = 0; i < utxos.length; i++) {
350
- const utxo = utxos[i]
351
-
352
- // If the user passes in a delay, then wait.
353
- if (usrObj && usrObj.utxoDelay && !isNaN(Number(usrObj.utxoDelay))) {
354
- const delayMs = Number(usrObj.utxoDelay)
355
- await this.util.sleep(delayMs)
356
- }
357
-
358
- // Get raw transaction data from the full node and attempt to decode
359
- // the OP_RETURN data.
360
- // If there is no OP_RETURN, mark the UTXO as false.
361
- let slpData = false
362
- try {
363
- slpData = await this.decodeOpReturn(
364
- utxo.txid,
365
- decodeOpReturnCache,
366
- usrObj // pass user data when making an internal call.
367
- )
368
- // console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`)
369
- } catch (err) {
370
- // console.log(
371
- // `error in _hydrateUtxo() from decodeOpReturn(${utxo.txid}): `,
372
- // err
373
- // )
374
-
375
- // An error will be thrown if the txid is not SLP.
376
- // If error is for some other reason, like a 429 error, mark utxo as 'null'
377
- // to display the unknown state.
378
- if (
379
- !err.message ||
380
- (err.message.indexOf('scriptpubkey not op_return') === -1 &&
381
- err.message.indexOf('lokad id') === -1 &&
382
- err.message.indexOf('trailing data') === -1)
383
- ) {
384
- // console.log(
385
- // "unknown error from decodeOpReturn(). Marking as 'null'",
386
- // err
387
- // )
388
-
389
- utxo.isValid = null
390
- outAry.push(utxo)
391
-
392
- // If error is thrown because there is no OP_RETURN, then it's not
393
- // an SLP UTXO.
394
- // Mark as false and continue the loop.
395
- } else {
396
- // console.log('marking as invalid')
397
- utxo.isValid = false
398
- outAry.push(utxo)
399
- }
400
-
401
- // Halt the execution of the loop and increase to the next index.
402
- continue
403
- }
404
- // console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`)
405
-
406
- const txType = slpData.txType.toLowerCase()
407
-
408
- // console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`)
409
-
410
- // If there is an OP_RETURN, attempt to decode it.
411
- // Handle Genesis SLP transactions.
412
- if (txType === 'genesis') {
413
- if (
414
- utxo.vout !== slpData.mintBatonVout && // UTXO is not a mint baton output.
415
- utxo.vout !== 1 // UTXO is not the reciever of the genesis or mint tokens.
416
- ) {
417
- // Can safely be marked as false.
418
- utxo.isValid = false
419
- outAry[i] = utxo
420
- } else {
421
- // If this is a valid SLP UTXO, then return the decoded OP_RETURN data.
422
- // Minting Baton
423
- if (utxo.vout === slpData.mintBatonVout) {
424
- utxo.utxoType = 'minting-baton'
425
- } else {
426
- // Tokens
427
-
428
- utxo.utxoType = 'token'
429
- utxo.tokenQty = new BigNumber(slpData.qty)
430
- .div(Math.pow(10, slpData.decimals))
431
- .toString()
432
- }
433
-
434
- utxo.tokenId = utxo.txid
435
- utxo.tokenTicker = slpData.ticker
436
- utxo.tokenName = slpData.name
437
- utxo.tokenDocumentUrl = slpData.documentUri
438
- utxo.tokenDocumentHash = slpData.documentHash
439
- utxo.decimals = slpData.decimals
440
- utxo.tokenType = slpData.tokenType
441
-
442
- // Initial value is null until UTXO can be validated and confirmed
443
- // to be valid (true) or not (false).
444
- utxo.isValid = null
445
-
446
- outAry[i] = utxo
447
- }
448
- }
449
-
450
- // Handle Mint SLP transactions.
451
- if (txType === 'mint') {
452
- if (
453
- utxo.vout !== slpData.mintBatonVout && // UTXO is not a mint baton output.
454
- utxo.vout !== 1 // UTXO is not the reciever of the genesis or mint tokens.
455
- ) {
456
- // Can safely be marked as false.
457
- utxo.isValid = false
458
-
459
- outAry[i] = utxo
460
- } else {
461
- // If UTXO passes validation, then return formatted token data.
462
-
463
- const genesisData = await this.decodeOpReturn(
464
- slpData.tokenId,
465
- decodeOpReturnCache,
466
- usrObj // pass user data when making an internal call.
467
- )
468
- // console.log(`genesisData: ${JSON.stringify(genesisData, null, 2)}`)
469
-
470
- // Minting Baton
471
- if (utxo.vout === slpData.mintBatonVout) {
472
- utxo.utxoType = 'minting-baton'
473
- } else {
474
- // Tokens
475
-
476
- utxo.utxoType = 'token'
477
- utxo.tokenQty = new BigNumber(slpData.qty)
478
- .div(Math.pow(10, genesisData.decimals))
479
- .toString()
480
- }
481
-
482
- // Hydrate the UTXO object with information about the SLP token.
483
- utxo.transactionType = 'mint'
484
- utxo.tokenId = slpData.tokenId
485
- utxo.tokenType = slpData.tokenType
486
-
487
- utxo.tokenTicker = genesisData.ticker
488
- utxo.tokenName = genesisData.name
489
- utxo.tokenDocumentUrl = genesisData.documentUri
490
- utxo.tokenDocumentHash = genesisData.documentHash
491
- utxo.decimals = genesisData.decimals
492
-
493
- utxo.mintBatonVout = slpData.mintBatonVout
494
-
495
- // Initial value is null until UTXO can be validated and confirmed
496
- // to be valid (true) or not (false).
497
- utxo.isValid = null
498
-
499
- outAry[i] = utxo
500
- }
501
- }
502
-
503
- // Handle Send SLP transactions.
504
- if (txType === 'send') {
505
- // Filter out any vouts that match.
506
- // const voutMatch = slpData.spendData.filter(x => utxo.vout === x.vout)
507
- // console.log(`voutMatch: ${JSON.stringify(voutMatch, null, 2)}`)
508
-
509
- // Figure out what token quantity is represented by this utxo.
510
- const tokenQty = slpData.amounts[utxo.vout - 1]
511
- // console.log('tokenQty: ', tokenQty)
512
-
513
- if (!tokenQty) {
514
- utxo.isValid = false
515
-
516
- outAry[i] = utxo
517
- } else {
518
- // If UTXO passes validation, then return formatted token data.
519
-
520
- const genesisData = await this.decodeOpReturn(
521
- slpData.tokenId,
522
- decodeOpReturnCache,
523
- usrObj // pass user data when making an internal call.
524
- )
525
- // console.log(`genesisData: ${JSON.stringify(genesisData, null, 2)}`)
526
-
527
- // console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`)
528
-
529
- // Hydrate the UTXO object with information about the SLP token.
530
- utxo.utxoType = 'token'
531
- utxo.transactionType = 'send'
532
- utxo.tokenId = slpData.tokenId
533
- utxo.tokenTicker = genesisData.ticker
534
- utxo.tokenName = genesisData.name
535
- utxo.tokenDocumentUrl = genesisData.documentUri
536
- utxo.tokenDocumentHash = genesisData.documentHash
537
- utxo.decimals = genesisData.decimals
538
- utxo.tokenType = slpData.tokenType
539
-
540
- // Initial value is null until UTXO can be validated and confirmed
541
- // to be valid (true) or not (false).
542
- utxo.isValid = null
543
-
544
- // Calculate the real token quantity.
545
-
546
- const tokenQtyBig = new BigNumber(tokenQty).div(
547
- Math.pow(10, genesisData.decimals)
548
- )
549
- // console.log(`tokenQtyBig`, tokenQtyBig.toString())
550
- utxo.tokenQty = tokenQtyBig.toString()
551
-
552
- // console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`)
553
-
554
- outAry[i] = utxo
555
- }
556
- }
557
- }
558
-
559
- return outAry
560
- } catch (error) {
561
- // console.log('_hydrateUtxo error: ', error)
562
- throw error
563
- }
564
- }
565
-
566
- /**
567
- * @api SLP.Utils.waterfallValidateTxid() waterfallValidateTxid()
568
- * @apiName waterfallValidateTxid
569
- * @apiGroup SLP Utils
570
- * @apiDescription Use multiple validators to validate an SLP TXID.
571
- *
572
- * This function aggregates all the available SLP token validation sources.
573
- * It starts with the fastest, most-efficient source first, and continues
574
- * to other validation sources until the txid is validated (true or false).
575
- * If the txid goes through all sources and can't be validated, it will
576
- * return null.
577
- *
578
- * Validation sources from most efficient to least efficient:
579
- * - SLPDB with whitelist filter
580
- * - SLPDB general purpose
581
- * - slp-api
582
- *
583
- * Currently only supports a single txid at a time.
584
- *
585
- * @apiExample Example usage:
586
- *
587
- * // validate single SLP txid
588
- * (async () => {
589
- * try {
590
- * let validated = await bchjs.SLP.Utils.waterfallValidateTxid(
591
- * "df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb"
592
- * );
593
- * console.log(validated);
594
- * } catch (error) {
595
- * console.error(error);
596
- * }
597
- * })();
598
- *
599
- * // returns
600
- * true
601
- */
602
- async waterfallValidateTxid (txid, usrObj = null) {
603
- try {
604
- // console.log('txid: ', txid)
605
-
606
- const cachedTxValidation = {}
607
-
608
- // If the value has been cached, use the cached version first.
609
- let isValid = cachedTxValidation[txid]
610
- if (!isValid && isValid !== false) {
611
- isValid = null
612
- } else {
613
- return isValid
614
- }
615
-
616
- // There are two possible responses from SLPDB. If SLPDB is functioning
617
- // correctly, then validateTxid() will return this:
618
- // isValid: [
619
- // {
620
- // "txid": "ff0c0354f8d3ddb34fa36f73494eb58ea24f8b8da6904aa8ed43b7a74886c583",
621
- // "valid": true
622
- // }
623
- // ]
624
- //
625
- // If SLPDB has fallen behind real-time processing, it will return this:
626
- // isValid: [
627
- // null
628
- // ]
629
- //
630
- // Note: validateTxid3() has the same output as validateTxid().
631
- // validateTxid2() uses slp-validate, which has a different output format.
632
-
633
- // Validate against the whitelist SLPDB first.
634
- const whitelistResult = await this.validateTxid3(txid, usrObj)
635
- // console.log(
636
- // `whitelist-SLPDB for ${txid}: ${JSON.stringify(
637
- // whitelistResult,
638
- // null,
639
- // 2
640
- // )}`
641
- // )
642
-
643
- // Safely retrieve the returned value.
644
- if (whitelistResult[0] !== null) isValid = whitelistResult[0].valid
645
-
646
- // Exit if isValid is not null.
647
- if (isValid !== null) {
648
- // Save to the cache.
649
- cachedTxValidation[txid] = isValid
650
-
651
- return isValid
652
- }
653
-
654
- // Try the general SLPDB, if the whitelist returned null.
655
- const generalResult = await this.validateTxid(txid, usrObj)
656
- // console.log(
657
- // `validateTxid() isValid: ${JSON.stringify(generalResult, null, 2)}`
658
- // )
659
-
660
- // Safely retrieve the returned value.
661
- if (generalResult[0] !== null) isValid = generalResult[0].valid
662
-
663
- // Exit if isValid is not null.
664
- if (isValid !== null) {
665
- // Save to the cache.
666
- cachedTxValidation[txid] = isValid
667
-
668
- return isValid
669
- }
670
-
671
- // If still null, as a last resort, check it against slp-validate
672
- let slpValidateResult = null
673
- try {
674
- slpValidateResult = await this.validateTxid2(txid)
675
- } catch (err) {
676
- /* exit quietly */
677
- }
678
- // console.log(
679
- // `slpValidateResult: ${JSON.stringify(slpValidateResult, null, 2)}`
680
- // )
681
-
682
- // Exit if isValid is not null.
683
- if (slpValidateResult !== null) {
684
- isValid = slpValidateResult.isValid
685
-
686
- // Save to the cache.
687
- cachedTxValidation[txid] = isValid
688
-
689
- return isValid
690
- }
691
-
692
- // If isValid is still null, return that value, signaling that the txid
693
- // could not be validated.
694
- return isValid
695
- } catch (error) {
696
- // This case handles rate limit errors.
697
- if (error.response && error.response.data && error.response.data.error) {
698
- throw new Error(error.response.data.error)
699
- }
700
-
701
- // console.log('Error in waterfallValidateTxid()')
702
- if (error.response && error.response.data) throw error.response.data
703
- throw error
704
- }
705
- }
706
185
  }
707
186
 
708
187
  module.exports = Utils