@neuraiproject/neurai-assets 1.3.2 → 1.4.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.
@@ -7,6 +7,55 @@ var NeuraiAssetsBundle = (function (exports) {
7
7
 
8
8
  var src = {exports: {}};
9
9
 
10
+ /**
11
+ * Extract a human-readable message from an RPC rejection.
12
+ *
13
+ * @neuraiproject/neurai-rpc >= 0.5 never rejects with a plain Error, so
14
+ * `error.message` is undefined for every node failure. It uses three shapes:
15
+ *
16
+ * 1. {error: {code, message}, description} JSON-RPC error (also on HTTP 200)
17
+ * 2. {statusText, status, description, error} HTTP response other than 200
18
+ * 3. {originalError, type: 'ServerUnreachable', error, description}
19
+ *
20
+ * In shape 3 `error` is a string, in shapes 1 and 2 it is an object (or
21
+ * null). Plain Errors (thrown by this library or by mocks) keep working
22
+ * through the `error.message` candidate.
23
+ */
24
+
25
+ var rpcErrorMessage_1;
26
+ var hasRequiredRpcErrorMessage;
27
+
28
+ function requireRpcErrorMessage () {
29
+ if (hasRequiredRpcErrorMessage) return rpcErrorMessage_1;
30
+ hasRequiredRpcErrorMessage = 1;
31
+ function rpcErrorMessage(error) {
32
+ if (!error) {
33
+ return '';
34
+ }
35
+ if (typeof error === 'string') {
36
+ return error;
37
+ }
38
+
39
+ const candidates = [
40
+ error.error && error.error.message,
41
+ typeof error.error === 'string' ? error.error : null,
42
+ error.description,
43
+ error.message,
44
+ error.statusText
45
+ ];
46
+
47
+ for (const candidate of candidates) {
48
+ if (typeof candidate === 'string' && candidate.length > 0) {
49
+ return candidate;
50
+ }
51
+ }
52
+ return '';
53
+ }
54
+
55
+ rpcErrorMessage_1 = { rpcErrorMessage };
56
+ return rpcErrorMessage_1;
57
+ }
58
+
10
59
  /**
11
60
  * Validation Error Classes
12
61
  * Errors thrown during parameter validation
@@ -332,6 +381,7 @@ var NeuraiAssetsBundle = (function (exports) {
332
381
  function requireAssetQueries () {
333
382
  if (hasRequiredAssetQueries) return AssetQueries_1;
334
383
  hasRequiredAssetQueries = 1;
384
+ const { rpcErrorMessage } = requireRpcErrorMessage();
335
385
  const { AssetNotFoundError, InvalidAddressError } = requireErrors();
336
386
 
337
387
  class AssetQueries {
@@ -373,14 +423,14 @@ var NeuraiAssetsBundle = (function (exports) {
373
423
  }
374
424
 
375
425
  // RPC error - likely asset doesn't exist
376
- if (error.message && error.message.includes('not found')) {
426
+ if (rpcErrorMessage(error).includes('not found')) {
377
427
  throw new AssetNotFoundError(
378
428
  `Asset ${assetName} not found on blockchain`,
379
429
  assetName
380
430
  );
381
431
  }
382
432
 
383
- throw new Error(`Failed to get asset data: ${error.message}`);
433
+ throw new Error(`Failed to get asset data: ${rpcErrorMessage(error)}`);
384
434
  }
385
435
  }
386
436
 
@@ -397,7 +447,7 @@ var NeuraiAssetsBundle = (function (exports) {
397
447
  const assets = await this.rpc('listassets', [filter, verbose, count, start]);
398
448
  return assets || [];
399
449
  } catch (error) {
400
- throw new Error(`Failed to list assets: ${error.message}`);
450
+ throw new Error(`Failed to list assets: ${rpcErrorMessage(error)}`);
401
451
  }
402
452
  }
403
453
 
@@ -415,7 +465,7 @@ var NeuraiAssetsBundle = (function (exports) {
415
465
  const myAssets = await this.rpc('listmyassets', [assetName, verbose, count, start, confs]);
416
466
  return myAssets || {};
417
467
  } catch (error) {
418
- throw new Error(`Failed to list my assets: ${error.message}`);
468
+ throw new Error(`Failed to list my assets: ${rpcErrorMessage(error)}`);
419
469
  }
420
470
  }
421
471
 
@@ -436,13 +486,13 @@ var NeuraiAssetsBundle = (function (exports) {
436
486
  const result = await this.rpc('listaddressesbyasset', [assetName, onlyCount, count, start]);
437
487
  return result || (onlyCount ? 0 : []);
438
488
  } catch (error) {
439
- if (error.message && error.message.includes('not found')) {
489
+ if (rpcErrorMessage(error).includes('not found')) {
440
490
  throw new AssetNotFoundError(
441
491
  `Asset ${assetName} not found on blockchain`,
442
492
  assetName
443
493
  );
444
494
  }
445
- throw new Error(`Failed to list addresses by asset: ${error.message}`);
495
+ throw new Error(`Failed to list addresses by asset: ${rpcErrorMessage(error)}`);
446
496
  }
447
497
  }
448
498
 
@@ -463,7 +513,7 @@ var NeuraiAssetsBundle = (function (exports) {
463
513
  const result = await this.rpc('listassetbalancesbyaddress', [address, onlyTotal, count, start]);
464
514
  return result || (onlyTotal ? 0 : []);
465
515
  } catch (error) {
466
- throw new Error(`Failed to list asset balances by address: ${error.message}`);
516
+ throw new Error(`Failed to list asset balances by address: ${rpcErrorMessage(error)}`);
467
517
  }
468
518
  }
469
519
 
@@ -487,10 +537,10 @@ var NeuraiAssetsBundle = (function (exports) {
487
537
  return result === true || result === 1;
488
538
  } catch (error) {
489
539
  // If tag doesn't exist or address doesn't have it, return false
490
- if (error.message && (error.message.includes('not found') || error.message.includes('does not have'))) {
540
+ if (rpcErrorMessage(error).includes('not found') || rpcErrorMessage(error).includes('does not have')) {
491
541
  return false;
492
542
  }
493
- throw new Error(`Failed to check address tag: ${error.message}`);
543
+ throw new Error(`Failed to check address tag: ${rpcErrorMessage(error)}`);
494
544
  }
495
545
  }
496
546
 
@@ -509,10 +559,10 @@ var NeuraiAssetsBundle = (function (exports) {
509
559
  return tags || [];
510
560
  } catch (error) {
511
561
  // If no tags found, return empty array
512
- if (error.message && error.message.includes('not found')) {
562
+ if (rpcErrorMessage(error).includes('not found')) {
513
563
  return [];
514
564
  }
515
- throw new Error(`Failed to list tags for address: ${error.message}`);
565
+ throw new Error(`Failed to list tags for address: ${rpcErrorMessage(error)}`);
516
566
  }
517
567
  }
518
568
 
@@ -530,13 +580,13 @@ var NeuraiAssetsBundle = (function (exports) {
530
580
  const addresses = await this.rpc('listaddressesfortag', [qualifierName]);
531
581
  return addresses || [];
532
582
  } catch (error) {
533
- if (error.message && error.message.includes('not found')) {
583
+ if (rpcErrorMessage(error).includes('not found')) {
534
584
  throw new AssetNotFoundError(
535
585
  `Qualifier ${qualifierName} not found on blockchain`,
536
586
  qualifierName
537
587
  );
538
588
  }
539
- throw new Error(`Failed to list addresses for tag: ${error.message}`);
589
+ throw new Error(`Failed to list addresses for tag: ${rpcErrorMessage(error)}`);
540
590
  }
541
591
  }
542
592
 
@@ -560,10 +610,10 @@ var NeuraiAssetsBundle = (function (exports) {
560
610
  return result === true || result === 1;
561
611
  } catch (error) {
562
612
  // If address doesn't meet requirements, return false
563
- if (error.message && (error.message.includes('not found') || error.message.includes('does not meet'))) {
613
+ if (rpcErrorMessage(error).includes('not found') || rpcErrorMessage(error).includes('does not meet')) {
564
614
  return false;
565
615
  }
566
- throw new Error(`Failed to check address restriction: ${error.message}`);
616
+ throw new Error(`Failed to check address restriction: ${rpcErrorMessage(error)}`);
567
617
  }
568
618
  }
569
619
 
@@ -590,7 +640,7 @@ var NeuraiAssetsBundle = (function (exports) {
590
640
  }
591
641
  return false;
592
642
  } catch (error) {
593
- throw new Error(`Failed to check if address is frozen: ${error.message}`);
643
+ throw new Error(`Failed to check if address is frozen: ${rpcErrorMessage(error)}`);
594
644
  }
595
645
  }
596
646
 
@@ -608,13 +658,13 @@ var NeuraiAssetsBundle = (function (exports) {
608
658
  const result = await this.rpc('checkglobalrestriction', [restrictedAssetName]);
609
659
  return result === true || result === 1;
610
660
  } catch (error) {
611
- if (error.message && error.message.includes('not found')) {
661
+ if (rpcErrorMessage(error).includes('not found')) {
612
662
  throw new AssetNotFoundError(
613
663
  `Restricted asset ${restrictedAssetName} not found on blockchain`,
614
664
  restrictedAssetName
615
665
  );
616
666
  }
617
- throw new Error(`Failed to check global restriction: ${error.message}`);
667
+ throw new Error(`Failed to check global restriction: ${rpcErrorMessage(error)}`);
618
668
  }
619
669
  }
620
670
 
@@ -632,13 +682,13 @@ var NeuraiAssetsBundle = (function (exports) {
632
682
  const result = await this.rpc('getverifierstring', [restrictedAssetName]);
633
683
  return result || '';
634
684
  } catch (error) {
635
- if (error.message && error.message.includes('not found')) {
685
+ if (rpcErrorMessage(error).includes('not found')) {
636
686
  throw new AssetNotFoundError(
637
687
  `Restricted asset ${restrictedAssetName} not found on blockchain`,
638
688
  restrictedAssetName
639
689
  );
640
690
  }
641
- throw new Error(`Failed to get verifier string: ${error.message}`);
691
+ throw new Error(`Failed to get verifier string: ${rpcErrorMessage(error)}`);
642
692
  }
643
693
  }
644
694
 
@@ -680,7 +730,7 @@ var NeuraiAssetsBundle = (function (exports) {
680
730
  const result = await this.rpc('getsnapshotrequest', [assetName, blockHeight]);
681
731
  return result;
682
732
  } catch (error) {
683
- throw new Error(`Failed to get snapshot request: ${error.message}`);
733
+ throw new Error(`Failed to get snapshot request: ${rpcErrorMessage(error)}`);
684
734
  }
685
735
  }
686
736
 
@@ -703,7 +753,7 @@ var NeuraiAssetsBundle = (function (exports) {
703
753
  const result = await this.rpc('cancelsnapshotrequest', [assetName, blockHeight]);
704
754
  return result === true || result === 1;
705
755
  } catch (error) {
706
- throw new Error(`Failed to cancel snapshot request: ${error.message}`);
756
+ throw new Error(`Failed to cancel snapshot request: ${rpcErrorMessage(error)}`);
707
757
  }
708
758
  }
709
759
 
@@ -721,13 +771,13 @@ var NeuraiAssetsBundle = (function (exports) {
721
771
  const result = await this.rpc('listdepinholders', [assetName]);
722
772
  return result || [];
723
773
  } catch (error) {
724
- if (error.message && error.message.includes('not found')) {
774
+ if (rpcErrorMessage(error).includes('not found')) {
725
775
  throw new AssetNotFoundError(
726
776
  `DEPIN asset ${assetName} not found on blockchain`,
727
777
  assetName
728
778
  );
729
779
  }
730
- throw new Error(`Failed to list DEPIN holders: ${error.message}`);
780
+ throw new Error(`Failed to list DEPIN holders: ${rpcErrorMessage(error)}`);
731
781
  }
732
782
  }
733
783
 
@@ -750,7 +800,7 @@ var NeuraiAssetsBundle = (function (exports) {
750
800
  const result = await this.rpc('checkdepinvalidity', [assetName, address]);
751
801
  return result || { has_asset: false };
752
802
  } catch (error) {
753
- throw new Error(`Failed to check DEPIN validity: ${error.message}`);
803
+ throw new Error(`Failed to check DEPIN validity: ${rpcErrorMessage(error)}`);
754
804
  }
755
805
  }
756
806
 
@@ -764,7 +814,7 @@ var NeuraiAssetsBundle = (function (exports) {
764
814
  const assets = await this.listAssets('*', false, 1, 0);
765
815
  return Array.isArray(assets) ? assets.length : 0;
766
816
  } catch (error) {
767
- throw new Error(`Failed to get asset count: ${error.message}`);
817
+ throw new Error(`Failed to get asset count: ${rpcErrorMessage(error)}`);
768
818
  }
769
819
  }
770
820
 
@@ -1050,7 +1100,7 @@ var NeuraiAssetsBundle = (function (exports) {
1050
1100
  addressPrefix: 'N',
1051
1101
  authScriptAddressPrefix: 'nq1',
1052
1102
  pqAddressPrefix: 'nq1',
1053
- assetNameMaxLength: 32,
1103
+ assetNameMaxLength: 31,
1054
1104
  defaultRPCPort: 19001,
1055
1105
  coin: 'XNA',
1056
1106
  baseNetwork: 'xna'
@@ -1061,7 +1111,7 @@ var NeuraiAssetsBundle = (function (exports) {
1061
1111
  addressPrefix: 't',
1062
1112
  authScriptAddressPrefix: 'tnq1',
1063
1113
  pqAddressPrefix: 'tnq1',
1064
- assetNameMaxLength: 32, // Same as mainnet
1114
+ assetNameMaxLength: 121, // DePIN networks (testnet/regtest) extend the cap
1065
1115
  defaultRPCPort: 19101,
1066
1116
  coin: 'TXNA',
1067
1117
  baseNetwork: 'xna-test'
@@ -1072,7 +1122,7 @@ var NeuraiAssetsBundle = (function (exports) {
1072
1122
  addressPrefix: 'N',
1073
1123
  authScriptAddressPrefix: 'nq1',
1074
1124
  pqAddressPrefix: 'nq1',
1075
- assetNameMaxLength: 32,
1125
+ assetNameMaxLength: 31,
1076
1126
  defaultRPCPort: 19001,
1077
1127
  coin: 'XNA',
1078
1128
  baseNetwork: 'xna'
@@ -1083,7 +1133,7 @@ var NeuraiAssetsBundle = (function (exports) {
1083
1133
  addressPrefix: 't',
1084
1134
  authScriptAddressPrefix: 'tnq1',
1085
1135
  pqAddressPrefix: 'tnq1',
1086
- assetNameMaxLength: 32,
1136
+ assetNameMaxLength: 121,
1087
1137
  defaultRPCPort: 19101,
1088
1138
  coin: 'TXNA',
1089
1139
  baseNetwork: 'xna-test'
@@ -1911,6 +1961,8 @@ var NeuraiAssetsBundle = (function (exports) {
1911
1961
  function requireNetworkDetector () {
1912
1962
  if (hasRequiredNetworkDetector) return networkDetector;
1913
1963
  hasRequiredNetworkDetector = 1;
1964
+ const { rpcErrorMessage } = requireRpcErrorMessage();
1965
+
1914
1966
  const {
1915
1967
  NETWORKS,
1916
1968
  areAddressNetworksCompatible,
@@ -1945,7 +1997,7 @@ var NeuraiAssetsBundle = (function (exports) {
1945
1997
  // Default to mainnet
1946
1998
  return 'xna';
1947
1999
  } catch (error) {
1948
- throw new Error(`Failed to detect network from RPC: ${error.message}`);
2000
+ throw new Error(`Failed to detect network from RPC: ${rpcErrorMessage(error)}`);
1949
2001
  }
1950
2002
  }
1951
2003
 
@@ -2465,6 +2517,7 @@ var NeuraiAssetsBundle = (function (exports) {
2465
2517
  function requireOwnerTokenManager () {
2466
2518
  if (hasRequiredOwnerTokenManager) return OwnerTokenManager_1;
2467
2519
  hasRequiredOwnerTokenManager = 1;
2520
+ const { rpcErrorMessage } = requireRpcErrorMessage();
2468
2521
  const { AssetNameParser } = requireUtils();
2469
2522
  const {
2470
2523
  OwnerTokenNotFoundError,
@@ -2523,7 +2576,7 @@ var NeuraiAssetsBundle = (function (exports) {
2523
2576
  }
2524
2577
 
2525
2578
  throw new AssetError(
2526
- `Failed to find owner token ${ownerTokenName}: ${error.message}`
2579
+ `Failed to find owner token ${ownerTokenName}: ${rpcErrorMessage(error)}`
2527
2580
  );
2528
2581
  }
2529
2582
  }
@@ -2661,7 +2714,7 @@ var NeuraiAssetsBundle = (function (exports) {
2661
2714
  return ownerTokenUTXOs;
2662
2715
  } catch (error) {
2663
2716
  throw new AssetError(
2664
- `Failed to get owner tokens: ${error.message}`
2717
+ `Failed to get owner tokens: ${rpcErrorMessage(error)}`
2665
2718
  );
2666
2719
  }
2667
2720
  }
@@ -2853,6 +2906,7 @@ var NeuraiAssetsBundle = (function (exports) {
2853
2906
  function requireUTXOSelector () {
2854
2907
  if (hasRequiredUTXOSelector) return UTXOSelector_1;
2855
2908
  hasRequiredUTXOSelector = 1;
2909
+ const { rpcErrorMessage } = requireRpcErrorMessage();
2856
2910
  const { InsufficientFundsError } = requireErrors();
2857
2911
  const { estimateTransactionVbytes } = requireFeeSizing();
2858
2912
 
@@ -2891,7 +2945,7 @@ var NeuraiAssetsBundle = (function (exports) {
2891
2945
  return utxos.filter(utxo => !utxo.assetName || utxo.assetName === 'XNA');
2892
2946
  }
2893
2947
  } catch (error) {
2894
- throw new Error(`Failed to get UTXOs: ${error.message}`);
2948
+ throw new Error(`Failed to get UTXOs: ${rpcErrorMessage(error)}`);
2895
2949
  }
2896
2950
  }
2897
2951
 
@@ -3493,7 +3547,10 @@ var NeuraiAssetsBundle = (function (exports) {
3493
3547
  const { InvalidAssetNameError } = requireErrors();
3494
3548
 
3495
3549
  const MIN_ASSET_LENGTH = 3;
3496
- const MAINNET_MAX_NAME_LENGTH = 32;
3550
+ // Full-name caps, mirror of the node (assets_fromscript.cpp:31-47): the limit
3551
+ // applies to the COMPLETE name, owner '!' and tags included, so a mainnet
3552
+ // root is effectively capped at 30 (its owner token "ROOT!" must fit in 31).
3553
+ const MAINNET_MAX_NAME_LENGTH = 31;
3497
3554
  const TESTNET_MAX_NAME_LENGTH = 121;
3498
3555
 
3499
3556
  const ROOT_NAME_CHARACTERS = /^[A-Z0-9._]{3,}$/;
@@ -4363,6 +4420,7 @@ var NeuraiAssetsBundle = (function (exports) {
4363
4420
  function requireBaseAssetTransactionBuilder () {
4364
4421
  if (hasRequiredBaseAssetTransactionBuilder) return BaseAssetTransactionBuilder_1;
4365
4422
  hasRequiredBaseAssetTransactionBuilder = 1;
4423
+ const { rpcErrorMessage } = requireRpcErrorMessage();
4366
4424
  const { BurnManager, OwnerTokenManager, UTXOSelector, OutputOrderer } = requireManagers();
4367
4425
  const { AssetNameValidator, AmountValidator } = requireValidators();
4368
4426
 
@@ -4423,6 +4481,9 @@ var NeuraiAssetsBundle = (function (exports) {
4423
4481
  // post-selection recompute). The fee rate is stable for the duration of
4424
4482
  // a single build, so cache the first lookup and reuse it.
4425
4483
  this._feeRatePromise = null;
4484
+
4485
+ // NIP-040 marker for the localRawBuild metadata; resolved once per build.
4486
+ this._assetMarkerPromise = null;
4426
4487
  }
4427
4488
 
4428
4489
  /**
@@ -4515,7 +4576,7 @@ var NeuraiAssetsBundle = (function (exports) {
4515
4576
 
4516
4577
  return rawTx;
4517
4578
  } catch (error) {
4518
- throw new Error(`Failed to create raw transaction: ${error.message}`);
4579
+ throw new Error(`Failed to create raw transaction: ${rpcErrorMessage(error)}`);
4519
4580
  }
4520
4581
  }
4521
4582
 
@@ -4677,19 +4738,73 @@ var NeuraiAssetsBundle = (function (exports) {
4677
4738
  return Math.round(amount * 100000000);
4678
4739
  }
4679
4740
 
4741
+ /**
4742
+ * NIP-040 marker for every asset output of the localRawBuild metadata.
4743
+ *
4744
+ * The chain decides which marker ("rvn" or "xna") new asset outputs must
4745
+ * carry, per network and height, and the node reports the one required for
4746
+ * the next block as `getblockchaininfo.asset_marker` (node commit 347362b).
4747
+ * Resolution order:
4748
+ * 1. `params.assetMarker` (explicit caller override — offline builds,
4749
+ * tests, or a node this library should not ask);
4750
+ * 2. `getblockchaininfo.asset_marker` from the connected node;
4751
+ * 3. `'rvn'` when the node predates the field or the call fails
4752
+ * (matches what such a node enforces; documented in the README).
4753
+ * The RPC-built transaction path never needs this: the node stamps the
4754
+ * marker itself in `createrawtransaction`.
4755
+ *
4756
+ * @returns {Promise<'rvn'|'xna'>} Marker for locally built asset outputs
4757
+ */
4758
+ resolveAssetMarker() {
4759
+ if (!this._assetMarkerPromise) {
4760
+ this._assetMarkerPromise = this._fetchAssetMarker();
4761
+ }
4762
+ return this._assetMarkerPromise;
4763
+ }
4764
+
4765
+ async _fetchAssetMarker() {
4766
+ const override = this.params.assetMarker;
4767
+ if (override !== undefined && override !== null) {
4768
+ if (override !== 'rvn' && override !== 'xna') {
4769
+ throw new Error(
4770
+ `Invalid assetMarker: ${override} (expected 'rvn' or 'xna', the value of getblockchaininfo.asset_marker)`
4771
+ );
4772
+ }
4773
+ return override;
4774
+ }
4775
+
4776
+ let info = null;
4777
+ try {
4778
+ info = await this.rpc('getblockchaininfo', []);
4779
+ } catch (error) {
4780
+ return 'rvn';
4781
+ }
4782
+ const marker = info ? info.asset_marker : undefined;
4783
+ if (marker === undefined || marker === null) {
4784
+ return 'rvn';
4785
+ }
4786
+ if (marker !== 'rvn' && marker !== 'xna') {
4787
+ throw new Error(`Node reported an unknown asset_marker: ${marker}`);
4788
+ }
4789
+ return marker;
4790
+ }
4791
+
4680
4792
  /**
4681
4793
  * Build a typed local raw build payload compatible with
4682
4794
  * @neuraiproject/neurai-create-transaction createFromOperation(...)
4683
4795
  *
4796
+ * Stamps the NIP-040 `assetMarker` (see resolveAssetMarker) so
4797
+ * createFromOperation >= 0.7.0 emits the marker the chain requires.
4798
+ *
4684
4799
  * @param {string} operationType - Operation type
4685
4800
  * @param {Array} inputs - Builder inputs
4686
4801
  * @param {object|null} burnInfo - Burn metadata
4687
4802
  * @param {string|null} changeAddress - XNA change address
4688
4803
  * @param {number|null} changeAmount - XNA change amount in XNA
4689
4804
  * @param {object} operationParams - Operation-specific params
4690
- * @returns {{ operationType: string, params: object }} Local raw build
4805
+ * @returns {Promise<{ operationType: string, params: object }>} Local raw build
4691
4806
  */
4692
- buildLocalRawBuild(
4807
+ async buildLocalRawBuild(
4693
4808
  operationType,
4694
4809
  inputs,
4695
4810
  burnInfo = null,
@@ -4699,6 +4814,7 @@ var NeuraiAssetsBundle = (function (exports) {
4699
4814
  ) {
4700
4815
  const params = {
4701
4816
  inputs: this.toRawTxInputs(inputs),
4817
+ assetMarker: await this.resolveAssetMarker(),
4702
4818
  ...operationParams
4703
4819
  };
4704
4820
 
@@ -4835,7 +4951,7 @@ var NeuraiAssetsBundle = (function (exports) {
4835
4951
  return assetData !== null && assetData !== undefined;
4836
4952
  } catch (error) {
4837
4953
  // If asset doesn't exist, RPC will throw error
4838
- if (error.message && error.message.includes('not found')) {
4954
+ if (rpcErrorMessage(error).includes('not found')) {
4839
4955
  return false;
4840
4956
  }
4841
4957
  // Re-throw other errors
@@ -4852,7 +4968,7 @@ var NeuraiAssetsBundle = (function (exports) {
4852
4968
  try {
4853
4969
  return await this.rpc('getassetdata', [assetName]);
4854
4970
  } catch (error) {
4855
- if (error.message && error.message.includes('not found')) {
4971
+ if (rpcErrorMessage(error).includes('not found')) {
4856
4972
  return null;
4857
4973
  }
4858
4974
  throw error;
@@ -5038,7 +5154,7 @@ var NeuraiAssetsBundle = (function (exports) {
5038
5154
  assetName,
5039
5155
  ownerTokenName: assetName + '!',
5040
5156
  operationType: 'ISSUE_ROOT',
5041
- localRawBuild: this.buildLocalRawBuild(
5157
+ localRawBuild: await this.buildLocalRawBuild(
5042
5158
  'ISSUE_ROOT',
5043
5159
  inputs,
5044
5160
  burnInfo,
@@ -5306,7 +5422,7 @@ var NeuraiAssetsBundle = (function (exports) {
5306
5422
  ownerTokenName: assetName + '!',
5307
5423
  parentOwnerTokenUsed: ownerTokenName,
5308
5424
  operationType: 'ISSUE_SUB',
5309
- localRawBuild: this.buildLocalRawBuild(
5425
+ localRawBuild: await this.buildLocalRawBuild(
5310
5426
  'ISSUE_SUB',
5311
5427
  inputs,
5312
5428
  burnInfo,
@@ -5476,7 +5592,7 @@ var NeuraiAssetsBundle = (function (exports) {
5476
5592
  assetName,
5477
5593
  ownerTokenName: `${assetName}!`,
5478
5594
  operationType: 'ISSUE_DEPIN',
5479
- localRawBuild: this.buildLocalRawBuild(
5595
+ localRawBuild: await this.buildLocalRawBuild(
5480
5596
  'ISSUE_DEPIN',
5481
5597
  inputs,
5482
5598
  burnInfo,
@@ -5744,7 +5860,7 @@ var NeuraiAssetsBundle = (function (exports) {
5744
5860
  previousSupply: currentSupply,
5745
5861
  reissuableLocked: reissuable === false,
5746
5862
  operationType: 'REISSUE',
5747
- localRawBuild: this.buildLocalRawBuild(
5863
+ localRawBuild: await this.buildLocalRawBuild(
5748
5864
  'REISSUE',
5749
5865
  inputs,
5750
5866
  burnInfo,
@@ -5769,6 +5885,274 @@ var NeuraiAssetsBundle = (function (exports) {
5769
5885
  return ReissueBuilder_1;
5770
5886
  }
5771
5887
 
5888
+ /**
5889
+ * Transfer Builder
5890
+ * Builds transactions that transfer an existing asset to one or more recipients.
5891
+ *
5892
+ * Works for any asset type (regular, sub, restricted, DePIN). The only
5893
+ * type-specific rule lives in Neurai consensus for DePIN (`&`) assets, which are
5894
+ * soulbound: a DePIN transfer is only valid if the same transaction
5895
+ * 1. SPENDS the asset's owner token `&NAME!` as an input, and
5896
+ * 2. re-creates (transfers) that owner token in an output.
5897
+ * See Neurai-DePIN/src/consensus/tx_verify.cpp (bad-txns-depin-transfer-not-by-owner).
5898
+ * For non-DePIN assets no owner token is required for a plain transfer.
5899
+ *
5900
+ * Owner-token destination: the owner token is returned to the sender's change
5901
+ * address — the asset moves to the recipient but authority stays with the owner
5902
+ * (soulbound semantics). Transferring ownership itself is out of scope here.
5903
+ *
5904
+ * This builder mirrors ReissueBuilder (which also spends + returns an owner
5905
+ * token) but, since a transfer has no reissue entry, it adds the owner-token
5906
+ * return output explicitly via OwnerTokenManager.
5907
+ */
5908
+
5909
+ var TransferBuilder_1;
5910
+ var hasRequiredTransferBuilder;
5911
+
5912
+ function requireTransferBuilder () {
5913
+ if (hasRequiredTransferBuilder) return TransferBuilder_1;
5914
+ hasRequiredTransferBuilder = 1;
5915
+ const BaseAssetTransactionBuilder = requireBaseAssetTransactionBuilder();
5916
+ const { OutputFormatter, AssetNameParser } = requireUtils();
5917
+ const { OwnerTokenNotFoundError } = requireErrors();
5918
+
5919
+ class TransferBuilder extends BaseAssetTransactionBuilder {
5920
+ /**
5921
+ * Validate transfer parameters
5922
+ * @param {object} params - Transfer parameters
5923
+ * @throws {Error} If validation fails
5924
+ */
5925
+ validateParams(params) {
5926
+ if (!params.assetName) {
5927
+ throw new Error('assetName is required');
5928
+ }
5929
+
5930
+ if (!Array.isArray(params.recipients) || params.recipients.length === 0) {
5931
+ throw new Error('recipients is required (non-empty array of { address, amount })');
5932
+ }
5933
+
5934
+ params.recipients.forEach((recipient, index) => {
5935
+ if (!recipient || !recipient.address) {
5936
+ throw new Error(`recipients[${index}].address is required`);
5937
+ }
5938
+ if (recipient.amount === undefined || recipient.amount === null) {
5939
+ throw new Error(`recipients[${index}].amount is required`);
5940
+ }
5941
+ if (recipient.amount <= 0) {
5942
+ throw new Error(`recipients[${index}].amount must be greater than 0`);
5943
+ }
5944
+ });
5945
+
5946
+ return true;
5947
+ }
5948
+
5949
+ /**
5950
+ * Build transfer transaction
5951
+ * @returns {Promise<object>} Transaction result
5952
+ */
5953
+ async build() {
5954
+ // 1. Validate parameters
5955
+ this.validateParams(this.params);
5956
+
5957
+ const { assetName, recipients } = this.params;
5958
+
5959
+ // Total amount to send, in user-facing asset units (NOT raw 10^8 sats).
5960
+ // selectAssetUTXOs / the createrawtransaction transfer output both expect
5961
+ // display units and scale by 10^8 themselves — pre-multiplying would
5962
+ // double-scale (see UTXOSelector.selectAssetUTXOs / BaseBuilder.toSatoshis).
5963
+ const totalAssetUnits = recipients.reduce((sum, r) => sum + r.amount, 0);
5964
+
5965
+ // 2. Addresses
5966
+ const addresses = await this._getAddresses();
5967
+ const changeAddress = await this.getChangeAddress();
5968
+
5969
+ // 3. DePIN detection + owner token lookup (soulbound rule)
5970
+ const isDepin = AssetNameParser.isDepin(assetName);
5971
+ let ownerTokenName = null;
5972
+ let ownerTokenUTXO = null;
5973
+ if (isDepin) {
5974
+ ownerTokenName = AssetNameParser.getOwnerTokenName(assetName); // &NAME -> &NAME!
5975
+ try {
5976
+ ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
5977
+ ownerTokenName,
5978
+ addresses
5979
+ );
5980
+ } catch (error) {
5981
+ if (error instanceof OwnerTokenNotFoundError) {
5982
+ throw new OwnerTokenNotFoundError(
5983
+ `You must own the asset's owner token (${ownerTokenName}) to transfer ` +
5984
+ `this DePIN asset. DePIN assets are soulbound: the transfer must be ` +
5985
+ `authorized by the owner.`,
5986
+ ownerTokenName
5987
+ );
5988
+ }
5989
+ throw error;
5990
+ }
5991
+ }
5992
+
5993
+ // 4. Output addresses used only for the fee (vsize) estimate. Include every
5994
+ // potential output so the fee is never under-estimated.
5995
+ const outputAddresses = [
5996
+ changeAddress, // XNA change
5997
+ ...recipients.map(r => r.address), // one transfer per recipient
5998
+ changeAddress, // asset change (harmless over-count if absent)
5999
+ ...(isDepin ? [changeAddress] : []), // owner token return
6000
+ ];
6001
+
6002
+ // 5. First (rough) fee estimate, then select asset + XNA UTXOs.
6003
+ const estimatedFee = await this.estimateFee(isDepin ? 3 : 2, outputAddresses);
6004
+ const utxoSelection = await this.selectUTXOs(estimatedFee, assetName, totalAssetUnits);
6005
+ const assetUTXOs = utxoSelection.assetUTXOs;
6006
+ const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
6007
+ let totalXNAInput = utxoSelection.totalXNA;
6008
+
6009
+ // Asset change computed in raw 10^8-sats to avoid float drift, then back to units.
6010
+ const assetInputRawSats = assetUTXOs.reduce((sum, u) => sum + u.satoshis, 0);
6011
+ const totalAssetRawSats = Math.round(totalAssetUnits * 100000000);
6012
+ const assetChangeRawSats = assetInputRawSats - totalAssetRawSats;
6013
+ const assetChangeUnits = assetChangeRawSats / 100000000;
6014
+
6015
+ // 6. Recompute the fee with the real inputs (PQ-aware), including the owner
6016
+ // token when DePIN, then top up XNA if the rough estimate fell short.
6017
+ const actualFeeInputs = [
6018
+ ...baseCurrencyUTXOs,
6019
+ ...assetUTXOs,
6020
+ ...(isDepin ? [ownerTokenUTXO] : []),
6021
+ ];
6022
+ const actualFee = await this.estimateFee(actualFeeInputs, outputAddresses);
6023
+
6024
+ if (totalXNAInput < actualFee) {
6025
+ const additionalNeeded = actualFee - totalXNAInput + 0.001;
6026
+ const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
6027
+ baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
6028
+ totalXNAInput += additionalSelection.totalXNA;
6029
+ }
6030
+
6031
+ // 7. XNA change (no burn for a transfer)
6032
+ const finalXNAInput = baseCurrencyUTXOs.reduce(
6033
+ (sum, utxo) => sum + utxo.satoshis / 100000000,
6034
+ 0
6035
+ );
6036
+ const xnaChange = finalXNAInput - actualFee;
6037
+
6038
+ // 8. Build inputs: asset UTXOs + [owner token] + XNA UTXOs
6039
+ const inputs = [];
6040
+
6041
+ assetUTXOs.forEach(utxo => {
6042
+ inputs.push({
6043
+ txid: utxo.txid,
6044
+ vout: utxo.outputIndex,
6045
+ address: utxo.address,
6046
+ assetName: utxo.assetName,
6047
+ satoshis: utxo.satoshis,
6048
+ });
6049
+ });
6050
+
6051
+ if (isDepin) {
6052
+ inputs.push({
6053
+ txid: ownerTokenUTXO.txid,
6054
+ vout: ownerTokenUTXO.outputIndex,
6055
+ address: ownerTokenUTXO.address,
6056
+ assetName: ownerTokenUTXO.assetName,
6057
+ satoshis: ownerTokenUTXO.satoshis,
6058
+ });
6059
+ }
6060
+
6061
+ baseCurrencyUTXOs.forEach(utxo => {
6062
+ inputs.push({
6063
+ txid: utxo.txid,
6064
+ vout: utxo.outputIndex,
6065
+ address: utxo.address,
6066
+ satoshis: utxo.satoshis,
6067
+ });
6068
+ });
6069
+
6070
+ // 9. Build outputs (unordered — outputOrderer enforces protocol order)
6071
+ const outputs = [];
6072
+
6073
+ // XNA change
6074
+ if (xnaChange > 0.00000001) {
6075
+ outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
6076
+ }
6077
+
6078
+ // One transfer per recipient (display units; the daemon scales by 10^8)
6079
+ recipients.forEach(r => {
6080
+ outputs.push({ [r.address]: OutputFormatter.formatTransferOutput(assetName, r.amount) });
6081
+ });
6082
+
6083
+ // Asset change back to the sender
6084
+ if (assetChangeRawSats > 0) {
6085
+ outputs.push({
6086
+ [changeAddress]: OutputFormatter.formatTransferOutput(assetName, assetChangeUnits),
6087
+ });
6088
+ }
6089
+
6090
+ // DePIN: return the owner token (required so the tx contains a transfer of
6091
+ // &NAME! — satisfies the consensus `transfersOwnerToken` check).
6092
+ if (isDepin) {
6093
+ outputs.push(
6094
+ this.ownerTokenManager.createOwnerTokenReturnOutput(ownerTokenName, changeAddress)
6095
+ );
6096
+ }
6097
+
6098
+ // 10. Order outputs (protocol requirement)
6099
+ const orderedOutputs = this.outputOrderer.order(outputs);
6100
+
6101
+ // 11. Create raw transaction
6102
+ const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
6103
+
6104
+ // 12. Format and return result
6105
+ const allUTXOs = [
6106
+ ...assetUTXOs,
6107
+ ...(isDepin ? [ownerTokenUTXO] : []),
6108
+ ...baseCurrencyUTXOs,
6109
+ ];
6110
+ const xnaChangeOut = xnaChange > 0.00000001 ? parseFloat(xnaChange.toFixed(8)) : null;
6111
+
6112
+ return this.formatResult(
6113
+ rawTx,
6114
+ allUTXOs,
6115
+ inputs,
6116
+ orderedOutputs,
6117
+ actualFee,
6118
+ 0, // burnAmount — transfers don't burn
6119
+ {
6120
+ assetName,
6121
+ recipients: recipients.map(r => ({ address: r.address, amount: r.amount })),
6122
+ assetChange: assetChangeRawSats > 0 ? assetChangeUnits : 0,
6123
+ isDepin,
6124
+ ownerTokenUsed: isDepin ? ownerTokenName : null,
6125
+ operationType: 'TRANSFER',
6126
+ localRawBuild: await this.buildLocalRawBuild(
6127
+ 'TRANSFER',
6128
+ inputs,
6129
+ null, // no burn
6130
+ changeAddress,
6131
+ xnaChangeOut,
6132
+ {
6133
+ assetName,
6134
+ transfers: recipients.map(r => ({
6135
+ address: r.address,
6136
+ assetName,
6137
+ amount: r.amount,
6138
+ })),
6139
+ assetChange: assetChangeRawSats > 0
6140
+ ? { address: changeAddress, assetName, amount: assetChangeUnits }
6141
+ : null,
6142
+ ownerReturn: isDepin
6143
+ ? { address: changeAddress, assetName: ownerTokenName, amount: 1 }
6144
+ : null,
6145
+ }
6146
+ ),
6147
+ }
6148
+ );
6149
+ }
6150
+ }
6151
+
6152
+ TransferBuilder_1 = TransferBuilder;
6153
+ return TransferBuilder_1;
6154
+ }
6155
+
5772
6156
  /**
5773
6157
  * Issue Unique Builder
5774
6158
  * Builds transactions for creating UNIQUE assets (NFTs)
@@ -6023,7 +6407,7 @@ var NeuraiAssetsBundle = (function (exports) {
6023
6407
  nftCount,
6024
6408
  ownerTokenUsed: ownerTokenName,
6025
6409
  operationType: 'ISSUE_UNIQUE',
6026
- localRawBuild: this.buildLocalRawBuild(
6410
+ localRawBuild: await this.buildLocalRawBuild(
6027
6411
  'ISSUE_UNIQUE',
6028
6412
  inputs,
6029
6413
  burnInfo,
@@ -6284,7 +6668,7 @@ var NeuraiAssetsBundle = (function (exports) {
6284
6668
  parentQualifier: isSub ? parsed.parent : null,
6285
6669
  parentQualifierUsed: parentQualifierName,
6286
6670
  operationType: isSub ? 'ISSUE_SUB_QUALIFIER' : 'ISSUE_QUALIFIER',
6287
- localRawBuild: this.buildLocalRawBuild(
6671
+ localRawBuild: await this.buildLocalRawBuild(
6288
6672
  isSub ? 'ISSUE_SUB_QUALIFIER' : 'ISSUE_QUALIFIER',
6289
6673
  inputs,
6290
6674
  burnInfo,
@@ -6538,7 +6922,7 @@ var NeuraiAssetsBundle = (function (exports) {
6538
6922
  verifierString,
6539
6923
  requiredQualifiers,
6540
6924
  operationType: 'ISSUE_RESTRICTED',
6541
- localRawBuild: this.buildLocalRawBuild(
6925
+ localRawBuild: await this.buildLocalRawBuild(
6542
6926
  'ISSUE_RESTRICTED',
6543
6927
  inputs,
6544
6928
  burnInfo,
@@ -6824,7 +7208,7 @@ var NeuraiAssetsBundle = (function (exports) {
6824
7208
  requiredQualifiers,
6825
7209
  reissuableLocked: reissuable === false,
6826
7210
  operationType: 'REISSUE_RESTRICTED',
6827
- localRawBuild: this.buildLocalRawBuild(
7211
+ localRawBuild: await this.buildLocalRawBuild(
6828
7212
  'REISSUE_RESTRICTED',
6829
7213
  inputs,
6830
7214
  burnInfo,
@@ -7066,7 +7450,7 @@ var NeuraiAssetsBundle = (function (exports) {
7066
7450
  targetAddresses,
7067
7451
  addressCount,
7068
7452
  operationType: isUntag ? 'UNTAG_ADDRESSES' : 'TAG_ADDRESSES',
7069
- localRawBuild: this.buildLocalRawBuild(
7453
+ localRawBuild: await this.buildLocalRawBuild(
7070
7454
  isUntag ? 'UNTAG_ADDRESSES' : 'TAG_ADDRESSES',
7071
7455
  inputs,
7072
7456
  burnInfo,
@@ -7328,7 +7712,7 @@ var NeuraiAssetsBundle = (function (exports) {
7328
7712
  targetAddresses: targetAddresses.length > 0 ? targetAddresses : null,
7329
7713
  addressCount: targetAddresses.length,
7330
7714
  operationType,
7331
- localRawBuild: this.buildLocalRawBuild(
7715
+ localRawBuild: await this.buildLocalRawBuild(
7332
7716
  operationType,
7333
7717
  inputs,
7334
7718
  null,
@@ -7405,6 +7789,7 @@ var NeuraiAssetsBundle = (function (exports) {
7405
7789
  const IssueSubBuilder = requireIssueSubBuilder();
7406
7790
  const IssueDepinBuilder = requireIssueDepinBuilder();
7407
7791
  const ReissueBuilder = requireReissueBuilder();
7792
+ const TransferBuilder = requireTransferBuilder();
7408
7793
 
7409
7794
  // Advanced Builders
7410
7795
  const IssueUniqueBuilder = requireIssueUniqueBuilder();
@@ -7423,6 +7808,7 @@ var NeuraiAssetsBundle = (function (exports) {
7423
7808
  IssueSubBuilder,
7424
7809
  IssueDepinBuilder,
7425
7810
  ReissueBuilder,
7811
+ TransferBuilder,
7426
7812
 
7427
7813
  // Advanced Builders
7428
7814
  IssueUniqueBuilder,
@@ -7475,7 +7861,8 @@ var NeuraiAssetsBundle = (function (exports) {
7475
7861
  ReissueBuilder,
7476
7862
  ReissueRestrictedBuilder,
7477
7863
  TagAddressBuilder,
7478
- FreezeAddressBuilder
7864
+ FreezeAddressBuilder,
7865
+ TransferBuilder
7479
7866
  } = requireBuilders();
7480
7867
 
7481
7868
  class NeuraiAssets {
@@ -7486,6 +7873,10 @@ var NeuraiAssetsBundle = (function (exports) {
7486
7873
  * @param {Array<string>} config.addresses - Wallet addresses
7487
7874
  * @param {string} config.changeAddress - Default change address
7488
7875
  * @param {string} config.toAddress - Default receiving address
7876
+ * @param {('rvn'|'xna')} [config.assetMarker] - NIP-040 marker for locally
7877
+ * built raw transactions. Omit to use the node's
7878
+ * getblockchaininfo.asset_marker (falls back to 'rvn' on nodes that do
7879
+ * not report it). Per-operation params.assetMarker overrides this.
7489
7880
  */
7490
7881
  constructor(rpc, config = {}) {
7491
7882
  if (!rpc || typeof rpc !== 'function') {
@@ -7523,7 +7914,10 @@ var NeuraiAssetsBundle = (function (exports) {
7523
7914
  network: this.config.network,
7524
7915
  walletAddresses: this.config.addresses,
7525
7916
  changeAddress: params.changeAddress || this.config.changeAddress,
7526
- toAddress: params.toAddress || this.config.toAddress
7917
+ toAddress: params.toAddress || this.config.toAddress,
7918
+ // NIP-040: marker for the localRawBuild metadata. Undefined lets the
7919
+ // builder ask the node (getblockchaininfo.asset_marker).
7920
+ assetMarker: params.assetMarker !== undefined ? params.assetMarker : this.config.assetMarker
7527
7921
  };
7528
7922
  }
7529
7923
 
@@ -7592,6 +7986,32 @@ var NeuraiAssetsBundle = (function (exports) {
7592
7986
  return await builder.build();
7593
7987
  }
7594
7988
 
7989
+ // ========================================
7990
+ // TRANSFER OPERATIONS
7991
+ // ========================================
7992
+
7993
+ /**
7994
+ * Transfer an existing asset to one or more recipients.
7995
+ *
7996
+ * Works for any asset type. DePIN (`&`) assets are soulbound: this method
7997
+ * automatically spends and returns the asset's owner token (`&NAME!`) so the
7998
+ * transfer satisfies Neurai consensus (bad-txns-depin-transfer-not-by-owner).
7999
+ * The owner token is returned to the change address (authority stays with the
8000
+ * sender). For non-DePIN assets no owner token is involved.
8001
+ *
8002
+ * @param {object} params - Transfer parameters
8003
+ * @param {string} params.assetName - Asset to transfer (e.g. 'TOKEN', '$SEC', '&DEVICE')
8004
+ * @param {Array<object>} params.recipients - Recipients
8005
+ * @param {string} params.recipients[].address - Destination address
8006
+ * @param {number} params.recipients[].amount - Amount in asset display units (> 0)
8007
+ * @param {string} [params.changeAddress] - Override change/owner-return address
8008
+ * @returns {Promise<object>} Transaction data
8009
+ */
8010
+ async transferAsset(params) {
8011
+ const builder = new TransferBuilder(this.rpc, this._buildParams(params));
8012
+ return await builder.build();
8013
+ }
8014
+
7595
8015
  // ========================================
7596
8016
  // UNIQUE ASSET (NFT) OPERATIONS
7597
8017
  // ========================================