@neuraiproject/neurai-assets 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1091,45 +1091,45 @@ var NeuraiAssetsBundle = (function (exports) {
1091
1091
  };
1092
1092
 
1093
1093
  /**
1094
- * Asset naming rules (same for all networks)
1094
+ * Asset naming helpers.
1095
+ * Network-specific maximum lengths are enforced in AssetNameValidator.
1095
1096
  */
1096
1097
  const ASSET_NAME_RULES = {
1097
1098
  ROOT: {
1098
1099
  minLength: 3,
1099
- maxLength: 30,
1100
+ maxLength: 31,
1100
1101
  pattern: /^[A-Z0-9_.]+$/,
1101
- cannotStartWith: ['.', 'A', 'Z'],
1102
- reserved: ['XNA', 'NEURAI']
1102
+ reserved: ['XNA', 'NEURAI', 'NEURAICOIN']
1103
1103
  },
1104
1104
  SUB: {
1105
1105
  minLength: 1,
1106
- maxLength: 30,
1106
+ maxLength: 31,
1107
1107
  pattern: /^[A-Z0-9_.]+$/,
1108
1108
  separator: '/',
1109
- maxDepth: 1 // Only one level of sub-assets
1109
+ maxDepth: null
1110
1110
  },
1111
1111
  UNIQUE: {
1112
1112
  minLength: 1,
1113
- maxLength: 30,
1114
- pattern: /^[A-Z0-9_.]+$/,
1113
+ maxLength: 32,
1114
+ pattern: /^[-A-Za-z0-9@$%&*()[\]{}_.?:]+$/,
1115
1115
  separator: '#'
1116
1116
  },
1117
1117
  QUALIFIER: {
1118
1118
  minLength: 3,
1119
- maxLength: 30,
1120
- pattern: /^[A-Z0-9_]+$/,
1119
+ maxLength: 32,
1120
+ pattern: /^[A-Z0-9_.]+$/,
1121
1121
  prefix: '#',
1122
1122
  separator: '/'
1123
1123
  },
1124
1124
  RESTRICTED: {
1125
1125
  minLength: 3,
1126
- maxLength: 30,
1126
+ maxLength: 32,
1127
1127
  pattern: /^[A-Z0-9_.]+$/,
1128
1128
  prefix: '$'
1129
1129
  },
1130
1130
  DEPIN: {
1131
1131
  minLength: 3,
1132
- maxLength: 120,
1132
+ maxLength: 121,
1133
1133
  pattern: /^[A-Z0-9_.]+$/,
1134
1134
  prefix: '&',
1135
1135
  separator: '/'
@@ -2725,6 +2725,118 @@ var NeuraiAssetsBundle = (function (exports) {
2725
2725
  return OwnerTokenManager_1;
2726
2726
  }
2727
2727
 
2728
+ /**
2729
+ * Fee / size helpers for Neurai transactions.
2730
+ *
2731
+ * These constants and classifiers are the same ones exposed by
2732
+ * `@neuraiproject/neurai-sign-transaction` (`VBYTES`, `isPQAddress`,
2733
+ * `isPQScript`, `estimateInputVbytes`, `estimateOutputBytes`,
2734
+ * `estimateTransactionVbytes`). They are inlined here to keep the assets
2735
+ * package light: depending on the full signer would pull `bitcoinjs-lib`
2736
+ * and `@noble/post-quantum` into the IIFE / browser bundles, which is far
2737
+ * more weight than the few constants we actually need for fee estimation.
2738
+ *
2739
+ * SOURCE OF TRUTH: `@neuraiproject/neurai-sign-transaction` `src/estimate.ts`.
2740
+ * Keep these values in sync with the signer's `VBYTES`. Mismatches surface
2741
+ * immediately as `min relay fee not met` failures from the node.
2742
+ */
2743
+
2744
+ var feeSizing;
2745
+ var hasRequiredFeeSizing;
2746
+
2747
+ function requireFeeSizing () {
2748
+ if (hasRequiredFeeSizing) return feeSizing;
2749
+ hasRequiredFeeSizing = 1;
2750
+ /** Per-component byte sizes used across the Neurai stack for fee estimation. */
2751
+ const VBYTES = Object.freeze({
2752
+ /** Raw transaction overhead: version (4) + in-count varint (1) + out-count varint (1) + locktime (4). */
2753
+ baseTxOverheadBytes: 10,
2754
+ /** Extra weight contributed by the segwit marker + flag bytes when any input is PQ. */
2755
+ segwitMarkerVbytes: 1,
2756
+ /** vbytes for a typical legacy P2PKH input (worst-case scriptSig). */
2757
+ legacyInputVbytes: 148,
2758
+ /** vbytes for a typical PQ AuthScript input with the default OP_TRUE witnessScript. */
2759
+ pqInputVbytes: 977,
2760
+ /** Bytes of a legacy P2PKH output (8-byte value + 1-byte script length + 25-byte scriptPubKey). */
2761
+ legacyOutputBytes: 34,
2762
+ /** Bytes of an AuthScript-v1 output (8-byte value + 1-byte script length + 34-byte scriptPubKey). */
2763
+ pqOutputBytes: 43,
2764
+ });
2765
+
2766
+ /** True for Neurai PQ AuthScript bech32 destinations (`nq1…` mainnet, `tnq1…` testnet). */
2767
+ function isPQAddress(address) {
2768
+ return (
2769
+ typeof address === 'string' &&
2770
+ (address.startsWith('nq1') || address.startsWith('tnq1'))
2771
+ );
2772
+ }
2773
+
2774
+ /** True for AuthScript-v1 scriptPubKey hex (witness v1, 32-byte program — `5120…`). */
2775
+ function isPQScript(scriptHex) {
2776
+ if (typeof scriptHex !== 'string' || scriptHex.length < 4) return false;
2777
+ return scriptHex.toLowerCase().startsWith('5120');
2778
+ }
2779
+
2780
+ /**
2781
+ * Estimate the vbytes contributed by spending one UTXO. Uses the UTXO's
2782
+ * `script` if available, otherwise falls back to its `address`. Unknown
2783
+ * prevouts are treated as legacy.
2784
+ */
2785
+ function estimateInputVbytes(utxo) {
2786
+ const script = utxo && utxo.script;
2787
+ if (typeof script === 'string' && script.length > 0) {
2788
+ return isPQScript(script) ? VBYTES.pqInputVbytes : VBYTES.legacyInputVbytes;
2789
+ }
2790
+ const address = utxo && utxo.address;
2791
+ if (typeof address === 'string' && isPQAddress(address)) {
2792
+ return VBYTES.pqInputVbytes;
2793
+ }
2794
+ return VBYTES.legacyInputVbytes;
2795
+ }
2796
+
2797
+ /** Estimate the bytes contributed by an output (address string or `{address}`). */
2798
+ function estimateOutputBytes(target) {
2799
+ const address =
2800
+ typeof target === 'string' ? target : (target && target.address) || '';
2801
+ return isPQAddress(address) ? VBYTES.pqOutputBytes : VBYTES.legacyOutputBytes;
2802
+ }
2803
+
2804
+ /**
2805
+ * Sum the per-input/per-output contributions, plus base overhead and segwit
2806
+ * marker (added once when any input is PQ). Inputs may be partial UTXO-like
2807
+ * objects with `script` and/or `address`. Outputs may be address strings or
2808
+ * `{ address }` descriptors.
2809
+ */
2810
+ function estimateTransactionVbytes(inputs, outputs) {
2811
+ let vbytes = VBYTES.baseTxOverheadBytes;
2812
+ let hasPQInput = false;
2813
+
2814
+ for (const inp of inputs) {
2815
+ const v = estimateInputVbytes(inp);
2816
+ vbytes += v;
2817
+ if (v === VBYTES.pqInputVbytes) hasPQInput = true;
2818
+ }
2819
+
2820
+ for (const out of outputs) {
2821
+ vbytes += estimateOutputBytes(out);
2822
+ }
2823
+
2824
+ if (hasPQInput) vbytes += VBYTES.segwitMarkerVbytes;
2825
+
2826
+ return vbytes;
2827
+ }
2828
+
2829
+ feeSizing = {
2830
+ VBYTES,
2831
+ isPQAddress,
2832
+ isPQScript,
2833
+ estimateInputVbytes,
2834
+ estimateOutputBytes,
2835
+ estimateTransactionVbytes,
2836
+ };
2837
+ return feeSizing;
2838
+ }
2839
+
2728
2840
  /**
2729
2841
  * UTXO Selector
2730
2842
  * Selects appropriate UTXOs for asset transactions
@@ -2742,6 +2854,7 @@ var NeuraiAssetsBundle = (function (exports) {
2742
2854
  if (hasRequiredUTXOSelector) return UTXOSelector_1;
2743
2855
  hasRequiredUTXOSelector = 1;
2744
2856
  const { InsufficientFundsError } = requireErrors();
2857
+ const { estimateTransactionVbytes } = requireFeeSizing();
2745
2858
 
2746
2859
  class UTXOSelector {
2747
2860
  /**
@@ -2980,35 +3093,44 @@ var NeuraiAssetsBundle = (function (exports) {
2980
3093
  }
2981
3094
 
2982
3095
  /**
2983
- * Estimate transaction size in bytes
2984
- * Used for fee calculation
3096
+ * Estimate transaction size in vbytes for fee calculation.
3097
+ *
3098
+ * Both arguments accept either a count (legacy callers) or an array of
3099
+ * descriptors that allow the estimator to distinguish PQ AuthScript
3100
+ * inputs/outputs from legacy P2PKH ones — PQ inputs are roughly six
3101
+ * times larger than legacy inputs and would otherwise underflow the
3102
+ * node's `min relay fee`.
2985
3103
  *
2986
- * @param {number} inputCount - Number of inputs
2987
- * @param {number} outputCount - Number of outputs
2988
- * @returns {number} Estimated size in bytes
3104
+ * Input descriptors may be UTXO-like objects with `script` and/or
3105
+ * `address`. Output descriptors may be address strings or `{ address }`.
3106
+ * When a count is provided instead of an array, every input/output is
3107
+ * treated as legacy.
3108
+ *
3109
+ * @param {number|Array} inputs - Input count or array of UTXO-like descriptors
3110
+ * @param {number|Array} outputs - Output count or array of address-like descriptors
3111
+ * @returns {number} Estimated vbytes
2989
3112
  */
2990
- estimateTransactionSize(inputCount, outputCount) {
2991
- // Rough estimation:
2992
- // - Each input: ~180 bytes
2993
- // - Each output: ~34 bytes
2994
- // - Transaction overhead: ~10 bytes
2995
- const inputSize = inputCount * 180;
2996
- const outputSize = outputCount * 34;
2997
- const overhead = 10;
2998
-
2999
- return inputSize + outputSize + overhead;
3113
+ estimateTransactionSize(inputs, outputs) {
3114
+ const inputDescriptors = Array.isArray(inputs)
3115
+ ? inputs
3116
+ : new Array(inputs).fill({});
3117
+ const outputDescriptors = Array.isArray(outputs)
3118
+ ? outputs
3119
+ : new Array(outputs).fill({});
3120
+ return estimateTransactionVbytes(inputDescriptors, outputDescriptors);
3000
3121
  }
3001
3122
 
3002
3123
  /**
3003
- * Estimate fee for a transaction
3004
- * @param {number} inputCount - Number of inputs
3005
- * @param {number} outputCount - Number of outputs
3124
+ * Estimate fee for a transaction.
3125
+ *
3126
+ * @param {number|Array} inputs - Input count or array of UTXO-like descriptors
3127
+ * @param {number|Array} outputs - Output count or array of address-like descriptors
3006
3128
  * @param {number} feeRate - Fee rate in XNA per KB (default: 0.015)
3007
3129
  * @returns {number} Estimated fee in XNA
3008
3130
  */
3009
- estimateFee(inputCount, outputCount, feeRate = 0.015) {
3010
- const sizeBytes = this.estimateTransactionSize(inputCount, outputCount);
3011
- const sizeKB = sizeBytes / 1000;
3131
+ estimateFee(inputs, outputs, feeRate = 0.015) {
3132
+ const sizeVbytes = this.estimateTransactionSize(inputs, outputs);
3133
+ const sizeKB = sizeVbytes / 1000;
3012
3134
  const fee = sizeKB * feeRate;
3013
3135
 
3014
3136
  // Round up to 8 decimals
@@ -3370,52 +3492,164 @@ var NeuraiAssetsBundle = (function (exports) {
3370
3492
  const { ASSET_NAME_RULES } = requireConstants();
3371
3493
  const { InvalidAssetNameError } = requireErrors();
3372
3494
 
3495
+ const MIN_ASSET_LENGTH = 3;
3496
+ const MAINNET_MAX_NAME_LENGTH = 32;
3497
+ const TESTNET_MAX_NAME_LENGTH = 121;
3498
+
3499
+ const ROOT_NAME_CHARACTERS = /^[A-Z0-9._]{3,}$/;
3500
+ const SUB_NAME_CHARACTERS = /^[A-Z0-9._]+$/;
3501
+ const UNIQUE_TAG_CHARACTERS = /^[-A-Za-z0-9@$%&*()[\]{}_.?:]+$/;
3502
+ const QUALIFIER_NAME_CHARACTERS = /^#[A-Z0-9._]{3,}$/;
3503
+ const SUB_QUALIFIER_NAME_CHARACTERS = /^#[A-Z0-9._]+$/;
3504
+ const RESTRICTED_NAME_CHARACTERS = /^\$[A-Z0-9._]{3,}$/;
3505
+ const DEPIN_NAME_CHARACTERS = /^&[A-Z0-9._]{3,}$/;
3506
+ const SUB_DEPIN_NAME_CHARACTERS = /^&[A-Z0-9._]+\/[A-Z0-9._/]+$/;
3507
+ const DOUBLE_PUNCTUATION = /^.*[._]{2,}.*$/;
3508
+ const LEADING_PUNCTUATION = /^[._].*$/;
3509
+ const TRAILING_PUNCTUATION = /^.*[._]$/;
3510
+ const QUALIFIER_LEADING_PUNCTUATION = /^[#$][._].*$/;
3511
+ const NEURAI_NAMES = /^XNA$|^NEURAI$|^NEURAICOIN$|^#XNA$|^#NEURAI$|^#NEURAICOIN$/;
3512
+
3513
+ function isTestnet(network) {
3514
+ return typeof network === 'string' && network.toLowerCase().includes('test');
3515
+ }
3516
+
3517
+ function getMaxAssetNameLength(network) {
3518
+ return isTestnet(network) ? TESTNET_MAX_NAME_LENGTH : MAINNET_MAX_NAME_LENGTH;
3519
+ }
3520
+
3521
+ function getRootOrSubMaxLength(network) {
3522
+ return getMaxAssetNameLength(network) - 1;
3523
+ }
3524
+
3525
+ function ensureName(name, label) {
3526
+ if (!name || typeof name !== 'string') {
3527
+ throw new InvalidAssetNameError(`${label} must be a non-empty string`, name);
3528
+ }
3529
+ }
3530
+
3531
+ function ensureMaxLength(name, maxLength, label) {
3532
+ if (name.length > maxLength) {
3533
+ throw new InvalidAssetNameError(`${label} cannot exceed ${maxLength} characters`, name);
3534
+ }
3535
+ }
3536
+
3537
+ function ensureUppercase(name, message) {
3538
+ if (name !== name.toUpperCase()) {
3539
+ throw new InvalidAssetNameError(message, name);
3540
+ }
3541
+ }
3542
+
3543
+ function isRootNameValid(name) {
3544
+ return ROOT_NAME_CHARACTERS.test(name)
3545
+ && !DOUBLE_PUNCTUATION.test(name)
3546
+ && !LEADING_PUNCTUATION.test(name)
3547
+ && !TRAILING_PUNCTUATION.test(name)
3548
+ && !NEURAI_NAMES.test(name);
3549
+ }
3550
+
3551
+ function isSubNameValid(name) {
3552
+ return SUB_NAME_CHARACTERS.test(name)
3553
+ && !DOUBLE_PUNCTUATION.test(name)
3554
+ && !LEADING_PUNCTUATION.test(name)
3555
+ && !TRAILING_PUNCTUATION.test(name);
3556
+ }
3557
+
3558
+ function isQualifierNameValid(name) {
3559
+ return QUALIFIER_NAME_CHARACTERS.test(name)
3560
+ && !DOUBLE_PUNCTUATION.test(name)
3561
+ && !QUALIFIER_LEADING_PUNCTUATION.test(name)
3562
+ && !TRAILING_PUNCTUATION.test(name)
3563
+ && !NEURAI_NAMES.test(name);
3564
+ }
3565
+
3566
+ function isSubQualifierNameValid(name) {
3567
+ return SUB_QUALIFIER_NAME_CHARACTERS.test(name)
3568
+ && !DOUBLE_PUNCTUATION.test(name)
3569
+ && !LEADING_PUNCTUATION.test(name)
3570
+ && !TRAILING_PUNCTUATION.test(name);
3571
+ }
3572
+
3573
+ function isRestrictedNameValid(name) {
3574
+ return RESTRICTED_NAME_CHARACTERS.test(name)
3575
+ && !DOUBLE_PUNCTUATION.test(name)
3576
+ && !LEADING_PUNCTUATION.test(name)
3577
+ && !TRAILING_PUNCTUATION.test(name)
3578
+ && !NEURAI_NAMES.test(name);
3579
+ }
3580
+
3581
+ function isNameValidBeforeTag(name) {
3582
+ const parts = name.split('/');
3583
+
3584
+ if (!isRootNameValid(parts[0])) {
3585
+ return false;
3586
+ }
3587
+
3588
+ for (let index = 1; index < parts.length; index += 1) {
3589
+ if (!isSubNameValid(parts[index])) {
3590
+ return false;
3591
+ }
3592
+ }
3593
+
3594
+ return true;
3595
+ }
3596
+
3597
+ function isQualifierNameValidBeforeTag(name) {
3598
+ const parts = name.split('/');
3599
+
3600
+ if (!isQualifierNameValid(parts[0])) {
3601
+ return false;
3602
+ }
3603
+
3604
+ if (parts.length > 2) {
3605
+ return false;
3606
+ }
3607
+
3608
+ for (let index = 1; index < parts.length; index += 1) {
3609
+ if (!isSubQualifierNameValid(parts[index])) {
3610
+ return false;
3611
+ }
3612
+ }
3613
+
3614
+ return true;
3615
+ }
3616
+
3617
+ function isDepinIndicator(name) {
3618
+ return DEPIN_NAME_CHARACTERS.test(name) || SUB_DEPIN_NAME_CHARACTERS.test(name);
3619
+ }
3620
+
3373
3621
  class AssetNameValidator {
3374
3622
  /**
3375
3623
  * Validate ROOT asset name
3376
- * Rules: 3-30 uppercase characters, A-Z, 0-9, underscore, period
3377
- * Cannot start with period, A, or Z
3624
+ * Rules: 3-31 visible characters on mainnet, A-Z, 0-9, underscore, period
3625
+ * Cannot start or end with period/underscore, cannot use repeated punctuation
3378
3626
  * Cannot be reserved names
3379
3627
  */
3380
- static validateRoot(name) {
3381
- if (!name || typeof name !== 'string') {
3382
- throw new InvalidAssetNameError('Asset name must be a non-empty string', name);
3383
- }
3628
+ static validateRoot(name, network = 'xna') {
3629
+ ensureName(name, 'Asset name');
3384
3630
 
3385
- // Length check
3386
- if (name.length < ASSET_NAME_RULES.ROOT.minLength || name.length > ASSET_NAME_RULES.ROOT.maxLength) {
3631
+ const maxLength = getRootOrSubMaxLength(network);
3632
+
3633
+ if (name.length < MIN_ASSET_LENGTH || name.length > maxLength) {
3387
3634
  throw new InvalidAssetNameError(
3388
- `ROOT asset name must be ${ASSET_NAME_RULES.ROOT.minLength}-${ASSET_NAME_RULES.ROOT.maxLength} characters`,
3635
+ `ROOT asset name must be ${MIN_ASSET_LENGTH}-${maxLength} characters`,
3389
3636
  name
3390
3637
  );
3391
3638
  }
3392
3639
 
3393
- // Uppercase check
3394
- if (name !== name.toUpperCase()) {
3395
- throw new InvalidAssetNameError('Asset name must be uppercase', name);
3396
- }
3640
+ ensureUppercase(name, 'Asset name must be uppercase');
3397
3641
 
3398
- // Starting character check
3399
- if (ASSET_NAME_RULES.ROOT.cannotStartWith.some(char => name.startsWith(char))) {
3400
- throw new InvalidAssetNameError(
3401
- `Asset name cannot start with: ${ASSET_NAME_RULES.ROOT.cannotStartWith.join(', ')}`,
3402
- name
3403
- );
3642
+ if (NEURAI_NAMES.test(name)) {
3643
+ throw new InvalidAssetNameError(`${name} is a reserved asset name`, name);
3404
3644
  }
3405
3645
 
3406
- // Pattern check
3407
- if (!ASSET_NAME_RULES.ROOT.pattern.test(name)) {
3646
+ if (!isRootNameValid(name)) {
3408
3647
  throw new InvalidAssetNameError(
3409
- 'Asset name can only contain A-Z, 0-9, underscore, and period',
3648
+ 'Name contains invalid characters (Valid characters are: A-Z 0-9 _ .) (special characters can\'t be the first or last characters)',
3410
3649
  name
3411
3650
  );
3412
3651
  }
3413
3652
 
3414
- // Reserved names check
3415
- if (ASSET_NAME_RULES.ROOT.reserved.includes(name)) {
3416
- throw new InvalidAssetNameError(`${name} is a reserved asset name`, name);
3417
- }
3418
-
3419
3653
  return true;
3420
3654
  }
3421
3655
 
@@ -3423,39 +3657,25 @@ var NeuraiAssetsBundle = (function (exports) {
3423
3657
  * Validate SUB asset name
3424
3658
  * Format: ROOT/SUBNAME
3425
3659
  */
3426
- static validateSub(name) {
3427
- if (!name || typeof name !== 'string') {
3428
- throw new InvalidAssetNameError('SUB asset name must be a non-empty string', name);
3429
- }
3660
+ static validateSub(name, network = 'xna') {
3661
+ ensureName(name, 'SUB asset name');
3430
3662
 
3431
- const parts = name.split(ASSET_NAME_RULES.SUB.separator);
3432
- if (parts.length !== 2) {
3663
+ if (!name.includes(ASSET_NAME_RULES.SUB.separator)) {
3433
3664
  throw new InvalidAssetNameError(
3434
3665
  `SUB asset must be in ${ASSET_NAME_RULES.SUB.separator} format (ROOT/SUBNAME)`,
3435
3666
  name
3436
3667
  );
3437
3668
  }
3438
3669
 
3439
- const [rootName, subName] = parts;
3440
-
3441
- // Validate root part
3442
- this.validateRoot(rootName);
3443
-
3444
- // Validate sub part
3445
- if (subName.length < ASSET_NAME_RULES.SUB.minLength || subName.length > ASSET_NAME_RULES.SUB.maxLength) {
3446
- throw new InvalidAssetNameError(
3447
- `SUB asset name must be ${ASSET_NAME_RULES.SUB.minLength}-${ASSET_NAME_RULES.SUB.maxLength} characters`,
3448
- name
3449
- );
3450
- }
3670
+ ensureMaxLength(name, getRootOrSubMaxLength(network), 'SUB asset name');
3451
3671
 
3452
- if (subName !== subName.toUpperCase()) {
3672
+ if (name !== name.toUpperCase()) {
3453
3673
  throw new InvalidAssetNameError('SUB asset name must be uppercase', name);
3454
3674
  }
3455
3675
 
3456
- if (!ASSET_NAME_RULES.SUB.pattern.test(subName)) {
3676
+ if (!isNameValidBeforeTag(name)) {
3457
3677
  throw new InvalidAssetNameError(
3458
- 'SUB asset name can only contain A-Z, 0-9, underscore, and period',
3678
+ 'Name contains invalid characters (Valid characters are: A-Z 0-9 _ .) (special characters can\'t be the first or last characters)',
3459
3679
  name
3460
3680
  );
3461
3681
  }
@@ -3467,10 +3687,10 @@ var NeuraiAssetsBundle = (function (exports) {
3467
3687
  * Validate UNIQUE asset name
3468
3688
  * Format: ROOT#TAG
3469
3689
  */
3470
- static validateUnique(name) {
3471
- if (!name || typeof name !== 'string') {
3472
- throw new InvalidAssetNameError('UNIQUE asset name must be a non-empty string', name);
3473
- }
3690
+ static validateUnique(name, network = 'xna') {
3691
+ ensureName(name, 'UNIQUE asset name');
3692
+
3693
+ ensureMaxLength(name, getMaxAssetNameLength(network), 'UNIQUE asset name');
3474
3694
 
3475
3695
  const parts = name.split(ASSET_NAME_RULES.UNIQUE.separator);
3476
3696
  if (parts.length !== 2) {
@@ -3482,24 +3702,9 @@ var NeuraiAssetsBundle = (function (exports) {
3482
3702
 
3483
3703
  const [rootName, tag] = parts;
3484
3704
 
3485
- // Validate root part
3486
- this.validateRoot(rootName);
3487
-
3488
- // Validate tag
3489
- if (tag.length < ASSET_NAME_RULES.UNIQUE.minLength || tag.length > ASSET_NAME_RULES.UNIQUE.maxLength) {
3490
- throw new InvalidAssetNameError(
3491
- `UNIQUE tag must be ${ASSET_NAME_RULES.UNIQUE.minLength}-${ASSET_NAME_RULES.UNIQUE.maxLength} characters`,
3492
- name
3493
- );
3494
- }
3495
-
3496
- if (tag !== tag.toUpperCase()) {
3497
- throw new InvalidAssetNameError('UNIQUE tag must be uppercase', name);
3498
- }
3499
-
3500
- if (!ASSET_NAME_RULES.UNIQUE.pattern.test(tag)) {
3705
+ if (!isNameValidBeforeTag(rootName) || !UNIQUE_TAG_CHARACTERS.test(tag)) {
3501
3706
  throw new InvalidAssetNameError(
3502
- 'UNIQUE tag can only contain A-Z, 0-9, underscore, and period',
3707
+ 'Unique name contains invalid characters (Valid characters are: A-Z a-z 0-9 @ $ % & * ( ) [ ] { } _ . ? : -)',
3503
3708
  name
3504
3709
  );
3505
3710
  }
@@ -3511,10 +3716,8 @@ var NeuraiAssetsBundle = (function (exports) {
3511
3716
  * Validate QUALIFIER asset name
3512
3717
  * Format: #NAME or #ROOT/SUB
3513
3718
  */
3514
- static validateQualifier(name) {
3515
- if (!name || typeof name !== 'string') {
3516
- throw new InvalidAssetNameError('QUALIFIER asset name must be a non-empty string', name);
3517
- }
3719
+ static validateQualifier(name, network = 'xna') {
3720
+ ensureName(name, 'QUALIFIER asset name');
3518
3721
 
3519
3722
  if (!name.startsWith(ASSET_NAME_RULES.QUALIFIER.prefix)) {
3520
3723
  throw new InvalidAssetNameError(
@@ -3523,58 +3726,17 @@ var NeuraiAssetsBundle = (function (exports) {
3523
3726
  );
3524
3727
  }
3525
3728
 
3526
- const withoutPrefix = name.substring(1);
3527
-
3528
- if (withoutPrefix.includes(ASSET_NAME_RULES.QUALIFIER.separator)) {
3529
- // Sub-qualifier: #ROOT/SUB
3530
- const parts = withoutPrefix.split(ASSET_NAME_RULES.QUALIFIER.separator);
3531
- if (parts.length !== 2) {
3532
- throw new InvalidAssetNameError(
3533
- 'SUB_QUALIFIER must be in #ROOT/SUB format',
3534
- name
3535
- );
3536
- }
3537
-
3538
- // Validate each part as a qualifier name (without the #)
3539
- parts.forEach(part => {
3540
- if (part.length < ASSET_NAME_RULES.QUALIFIER.minLength || part.length > ASSET_NAME_RULES.QUALIFIER.maxLength) {
3541
- throw new InvalidAssetNameError(
3542
- `QUALIFIER name must be ${ASSET_NAME_RULES.QUALIFIER.minLength}-${ASSET_NAME_RULES.QUALIFIER.maxLength} characters`,
3543
- name
3544
- );
3545
- }
3729
+ ensureMaxLength(name, getMaxAssetNameLength(network), 'QUALIFIER asset name');
3546
3730
 
3547
- if (part !== part.toUpperCase()) {
3548
- throw new InvalidAssetNameError('QUALIFIER name must be uppercase', name);
3549
- }
3550
-
3551
- if (!ASSET_NAME_RULES.QUALIFIER.pattern.test(part)) {
3552
- throw new InvalidAssetNameError(
3553
- 'QUALIFIER name can only contain A-Z, 0-9, and underscore',
3554
- name
3555
- );
3556
- }
3557
- });
3558
- } else {
3559
- // Root qualifier: #NAME
3560
- if (withoutPrefix.length < ASSET_NAME_RULES.QUALIFIER.minLength ||
3561
- withoutPrefix.length > ASSET_NAME_RULES.QUALIFIER.maxLength) {
3562
- throw new InvalidAssetNameError(
3563
- `QUALIFIER name must be ${ASSET_NAME_RULES.QUALIFIER.minLength}-${ASSET_NAME_RULES.QUALIFIER.maxLength} characters`,
3564
- name
3565
- );
3566
- }
3567
-
3568
- if (withoutPrefix !== withoutPrefix.toUpperCase()) {
3569
- throw new InvalidAssetNameError('QUALIFIER name must be uppercase', name);
3570
- }
3731
+ if (name !== name.toUpperCase()) {
3732
+ throw new InvalidAssetNameError('QUALIFIER name must be uppercase', name);
3733
+ }
3571
3734
 
3572
- if (!ASSET_NAME_RULES.QUALIFIER.pattern.test(withoutPrefix)) {
3573
- throw new InvalidAssetNameError(
3574
- 'QUALIFIER name can only contain A-Z, 0-9, and underscore',
3575
- name
3576
- );
3577
- }
3735
+ if (!isQualifierNameValidBeforeTag(name)) {
3736
+ throw new InvalidAssetNameError(
3737
+ 'Qualifier name contains invalid characters (Valid characters are: A-Z 0-9 _ .) (# must be the first character, _ . special characters can\'t be the first or last characters)',
3738
+ name
3739
+ );
3578
3740
  }
3579
3741
 
3580
3742
  return true;
@@ -3584,10 +3746,8 @@ var NeuraiAssetsBundle = (function (exports) {
3584
3746
  * Validate RESTRICTED asset name
3585
3747
  * Format: $NAME
3586
3748
  */
3587
- static validateRestricted(name) {
3588
- if (!name || typeof name !== 'string') {
3589
- throw new InvalidAssetNameError('RESTRICTED asset name must be a non-empty string', name);
3590
- }
3749
+ static validateRestricted(name, network = 'xna') {
3750
+ ensureName(name, 'RESTRICTED asset name');
3591
3751
 
3592
3752
  if (!name.startsWith(ASSET_NAME_RULES.RESTRICTED.prefix)) {
3593
3753
  throw new InvalidAssetNameError(
@@ -3596,10 +3756,18 @@ var NeuraiAssetsBundle = (function (exports) {
3596
3756
  );
3597
3757
  }
3598
3758
 
3599
- const withoutPrefix = name.substring(1);
3759
+ ensureMaxLength(name, getMaxAssetNameLength(network), 'RESTRICTED asset name');
3760
+
3761
+ if (name !== name.toUpperCase()) {
3762
+ throw new InvalidAssetNameError('RESTRICTED asset name must be uppercase', name);
3763
+ }
3600
3764
 
3601
- // Validate the part after $ as a ROOT asset name
3602
- this.validateRoot(withoutPrefix);
3765
+ if (!isRestrictedNameValid(name)) {
3766
+ throw new InvalidAssetNameError(
3767
+ 'Restricted name contains invalid characters (Valid characters are: A-Z 0-9 _ .) ($ must be the first character, _ . special characters can\'t be the first or last characters)',
3768
+ name
3769
+ );
3770
+ }
3603
3771
 
3604
3772
  return true;
3605
3773
  }
@@ -3608,10 +3776,8 @@ var NeuraiAssetsBundle = (function (exports) {
3608
3776
  * Validate DEPIN asset name
3609
3777
  * Format: &NAME or &NAME/SUB[/...]
3610
3778
  */
3611
- static validateDepin(name) {
3612
- if (!name || typeof name !== 'string') {
3613
- throw new InvalidAssetNameError('DEPIN asset name must be a non-empty string', name);
3614
- }
3779
+ static validateDepin(name, network) {
3780
+ ensureName(name, 'DEPIN asset name');
3615
3781
 
3616
3782
  if (!name.startsWith(ASSET_NAME_RULES.DEPIN.prefix)) {
3617
3783
  throw new InvalidAssetNameError(
@@ -3620,58 +3786,41 @@ var NeuraiAssetsBundle = (function (exports) {
3620
3786
  );
3621
3787
  }
3622
3788
 
3623
- if (name.length > ASSET_NAME_RULES.DEPIN.maxLength) {
3624
- throw new InvalidAssetNameError(
3625
- `DEPIN asset name cannot exceed ${ASSET_NAME_RULES.DEPIN.maxLength} characters`,
3626
- name
3627
- );
3789
+ if (network && !isTestnet(network)) {
3790
+ throw new InvalidAssetNameError('DEPIN assets are only available in testnet', name);
3628
3791
  }
3629
3792
 
3630
- const parts = name.split(ASSET_NAME_RULES.DEPIN.separator);
3631
- if (parts.length === 0) {
3632
- throw new InvalidAssetNameError('DEPIN asset name is invalid', name);
3793
+ ensureMaxLength(name, getMaxAssetNameLength(network || 'xna-test'), 'DEPIN asset name');
3794
+
3795
+ if (name !== name.toUpperCase()) {
3796
+ throw new InvalidAssetNameError('DEPIN asset name must be uppercase', name);
3633
3797
  }
3634
3798
 
3635
- const rootPart = parts[0].substring(1);
3636
- if (rootPart.length < ASSET_NAME_RULES.DEPIN.minLength) {
3799
+ if (!isDepinIndicator(name)) {
3637
3800
  throw new InvalidAssetNameError(
3638
- `DEPIN root name must be at least ${ASSET_NAME_RULES.DEPIN.minLength} characters`,
3801
+ 'DEPIN asset name can only contain A-Z, 0-9, underscore, period, and separator /',
3639
3802
  name
3640
3803
  );
3641
3804
  }
3642
3805
 
3643
- if (rootPart !== rootPart.toUpperCase()) {
3644
- throw new InvalidAssetNameError('DEPIN asset name must be uppercase', name);
3645
- }
3806
+ const parts = name.split(ASSET_NAME_RULES.DEPIN.separator);
3646
3807
 
3647
- if (!ASSET_NAME_RULES.DEPIN.pattern.test(rootPart)) {
3808
+ if (parts.length > 1) {
3809
+ parts.forEach(part => {
3810
+ if (part.length < MIN_ASSET_LENGTH) {
3811
+ throw new InvalidAssetNameError(
3812
+ `Each DEPIN sub-part must be at least ${MIN_ASSET_LENGTH} characters`,
3813
+ name
3814
+ );
3815
+ }
3816
+ });
3817
+ } else if (name.length < MIN_ASSET_LENGTH + 1) {
3648
3818
  throw new InvalidAssetNameError(
3649
- 'DEPIN asset name can only contain A-Z, 0-9, underscore, and period',
3819
+ `DEPIN name must be at least ${MIN_ASSET_LENGTH} characters (excluding &)`,
3650
3820
  name
3651
3821
  );
3652
3822
  }
3653
3823
 
3654
- const subParts = parts.slice(1);
3655
- subParts.forEach(part => {
3656
- if (part.length < ASSET_NAME_RULES.DEPIN.minLength) {
3657
- throw new InvalidAssetNameError(
3658
- `Each DEPIN sub-part must be at least ${ASSET_NAME_RULES.DEPIN.minLength} characters`,
3659
- name
3660
- );
3661
- }
3662
-
3663
- if (part !== part.toUpperCase()) {
3664
- throw new InvalidAssetNameError('DEPIN asset name must be uppercase', name);
3665
- }
3666
-
3667
- if (!ASSET_NAME_RULES.DEPIN.pattern.test(part)) {
3668
- throw new InvalidAssetNameError(
3669
- 'DEPIN asset name can only contain A-Z, 0-9, underscore, and period',
3670
- name
3671
- );
3672
- }
3673
- });
3674
-
3675
3824
  return true;
3676
3825
  }
3677
3826
 
@@ -3679,24 +3828,31 @@ var NeuraiAssetsBundle = (function (exports) {
3679
3828
  * Validate owner token name
3680
3829
  * Format: ASSETNAME!
3681
3830
  */
3682
- static validateOwnerToken(name) {
3683
- if (!name || typeof name !== 'string') {
3684
- throw new InvalidAssetNameError('Owner token name must be a non-empty string', name);
3685
- }
3831
+ static validateOwnerToken(name, network) {
3832
+ ensureName(name, 'Owner token name');
3833
+
3834
+ ensureMaxLength(name, getMaxAssetNameLength(network), 'Owner token name');
3686
3835
 
3687
3836
  if (!name.endsWith('!')) {
3688
3837
  throw new InvalidAssetNameError('Owner token must end with !', name);
3689
3838
  }
3690
3839
 
3691
3840
  const assetName = name.substring(0, name.length - 1);
3841
+ const validBaseName = isNameValidBeforeTag(assetName)
3842
+ || (assetName.startsWith('&') && (() => {
3843
+ try {
3844
+ this.validateDepin(assetName, network);
3845
+ return true;
3846
+ } catch (error) {
3847
+ return false;
3848
+ }
3849
+ })());
3692
3850
 
3693
- // Validate the asset name part (could be ROOT, RESTRICTED, or DEPIN)
3694
- if (assetName.startsWith('$')) {
3695
- this.validateRestricted(assetName);
3696
- } else if (assetName.startsWith('&')) {
3697
- this.validateDepin(assetName);
3698
- } else {
3699
- this.validateRoot(assetName);
3851
+ if (!validBaseName) {
3852
+ throw new InvalidAssetNameError(
3853
+ 'Owner name contains invalid characters (Valid characters are: A-Z 0-9 _ .) (special characters can\'t be the first or last characters)',
3854
+ name
3855
+ );
3700
3856
  }
3701
3857
 
3702
3858
  return true;
@@ -3707,27 +3863,27 @@ var NeuraiAssetsBundle = (function (exports) {
3707
3863
  * @param {string} name - Asset name
3708
3864
  * @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'DEPIN', 'OWNER')
3709
3865
  */
3710
- static validateAndDetectType(name) {
3866
+ static validateAndDetectType(name, network) {
3711
3867
  if (name.endsWith('!')) {
3712
- this.validateOwnerToken(name);
3868
+ this.validateOwnerToken(name, network);
3713
3869
  return 'OWNER';
3714
3870
  } else if (name.startsWith('#')) {
3715
- this.validateQualifier(name);
3871
+ this.validateQualifier(name, network);
3716
3872
  return name.includes('/') ? 'SUB_QUALIFIER' : 'QUALIFIER';
3717
3873
  } else if (name.startsWith('$')) {
3718
- this.validateRestricted(name);
3874
+ this.validateRestricted(name, network);
3719
3875
  return 'RESTRICTED';
3720
3876
  } else if (name.startsWith('&')) {
3721
- this.validateDepin(name);
3877
+ this.validateDepin(name, network);
3722
3878
  return 'DEPIN';
3723
3879
  } else if (name.includes('#')) {
3724
- this.validateUnique(name);
3880
+ this.validateUnique(name, network);
3725
3881
  return 'UNIQUE';
3726
3882
  } else if (name.includes('/')) {
3727
- this.validateSub(name);
3883
+ this.validateSub(name, network);
3728
3884
  return 'SUB';
3729
3885
  } else {
3730
- this.validateRoot(name);
3886
+ this.validateRoot(name, network);
3731
3887
  return 'ROOT';
3732
3888
  }
3733
3889
  }
@@ -4296,14 +4452,20 @@ var NeuraiAssetsBundle = (function (exports) {
4296
4452
  }
4297
4453
 
4298
4454
  /**
4299
- * Estimate transaction fee
4300
- * @param {number} inputCount - Number of inputs
4301
- * @param {number} outputCount - Number of outputs
4455
+ * Estimate transaction fee.
4456
+ *
4457
+ * Both arguments accept either a count (legacy) or an array of descriptors
4458
+ * that lets the underlying estimator distinguish PQ AuthScript inputs/outputs
4459
+ * from legacy P2PKH ones. Pass arrays whenever you have actual UTXOs and
4460
+ * output addresses on hand — counts produce a legacy-only estimate.
4461
+ *
4462
+ * @param {number|Array} inputs - Input count or array of UTXO-like descriptors
4463
+ * @param {number|Array} outputs - Output count or array of address-like descriptors
4302
4464
  * @returns {Promise<number>} Estimated fee in XNA
4303
4465
  */
4304
- async estimateFee(inputCount, outputCount) {
4466
+ async estimateFee(inputs, outputs) {
4305
4467
  const feeRate = await this.utxoSelector.getFeeRate();
4306
- return this.utxoSelector.estimateFee(inputCount, outputCount, feeRate);
4468
+ return this.utxoSelector.estimateFee(inputs, outputs, feeRate);
4307
4469
  }
4308
4470
 
4309
4471
  /**
@@ -4404,22 +4566,22 @@ var NeuraiAssetsBundle = (function (exports) {
4404
4566
 
4405
4567
  switch (type) {
4406
4568
  case 'ROOT':
4407
- AssetNameValidator.validateRoot(assetName);
4569
+ AssetNameValidator.validateRoot(assetName, this.network);
4408
4570
  break;
4409
4571
  case 'SUB':
4410
- AssetNameValidator.validateSub(assetName);
4572
+ AssetNameValidator.validateSub(assetName, this.network);
4411
4573
  break;
4412
4574
  case 'UNIQUE':
4413
- AssetNameValidator.validateUnique(assetName);
4575
+ AssetNameValidator.validateUnique(assetName, this.network);
4414
4576
  break;
4415
4577
  case 'QUALIFIER':
4416
- AssetNameValidator.validateQualifier(assetName);
4578
+ AssetNameValidator.validateQualifier(assetName, this.network);
4417
4579
  break;
4418
4580
  case 'RESTRICTED':
4419
- AssetNameValidator.validateRestricted(assetName);
4581
+ AssetNameValidator.validateRestricted(assetName, this.network);
4420
4582
  break;
4421
4583
  case 'DEPIN':
4422
- AssetNameValidator.validateDepin(assetName);
4584
+ AssetNameValidator.validateDepin(assetName, this.network);
4423
4585
  break;
4424
4586
  default:
4425
4587
  throw new Error(`Unknown asset type: ${type}`);
@@ -4765,7 +4927,8 @@ var NeuraiAssetsBundle = (function (exports) {
4765
4927
  const changeAddress = await this.getChangeAddress();
4766
4928
 
4767
4929
  // 5. Estimate fee (rough estimate for initial UTXO selection)
4768
- const estimatedFee = await this.estimateFee(1, 3);
4930
+ const outputAddresses = [burnInfo.address, changeAddress, toAddress];
4931
+ const estimatedFee = await this.estimateFee(1, outputAddresses);
4769
4932
 
4770
4933
  // 6. Calculate total XNA needed
4771
4934
  const totalXNANeeded = burnInfo.amount + estimatedFee;
@@ -4775,8 +4938,8 @@ var NeuraiAssetsBundle = (function (exports) {
4775
4938
  const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
4776
4939
  const totalXNAInput = utxoSelection.totalXNA;
4777
4940
 
4778
- // 8. Recalculate fee with actual input count
4779
- const actualFee = await this.estimateFee(baseCurrencyUTXOs.length, 3);
4941
+ // 8. Recalculate fee with actual inputs (PQ-aware)
4942
+ const actualFee = await this.estimateFee(baseCurrencyUTXOs, outputAddresses);
4780
4943
 
4781
4944
  // 9. Verify we still have enough after fee recalculation
4782
4945
  const totalRequired = burnInfo.amount + actualFee;
@@ -5000,7 +5163,13 @@ var NeuraiAssetsBundle = (function (exports) {
5000
5163
  // 8. Estimate fee
5001
5164
  // Inputs: XNA UTXOs + owner token UTXO
5002
5165
  // Outputs: burn + change + owner token return + issue operation
5003
- const estimatedFee = await this.estimateFee(2, 4);
5166
+ const outputAddresses = [
5167
+ burnInfo.address,
5168
+ changeAddress,
5169
+ changeAddress, // owner token return goes to change address
5170
+ toAddress,
5171
+ ];
5172
+ const estimatedFee = await this.estimateFee(2, outputAddresses);
5004
5173
 
5005
5174
  // 9. Calculate total XNA needed
5006
5175
  const totalXNANeeded = burnInfo.amount + estimatedFee;
@@ -5010,9 +5179,9 @@ var NeuraiAssetsBundle = (function (exports) {
5010
5179
  const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
5011
5180
  const totalXNAInput = utxoSelection.totalXNA;
5012
5181
 
5013
- // 11. Recalculate fee with actual input count
5014
- const actualInputCount = baseCurrencyUTXOs.length + 1; // +1 for owner token
5015
- const actualFee = await this.estimateFee(actualInputCount, 4);
5182
+ // 11. Recalculate fee with actual inputs (PQ-aware), including owner token UTXO
5183
+ const actualFeeInputs = [...baseCurrencyUTXOs, ownerTokenUTXO];
5184
+ const actualFee = await this.estimateFee(actualFeeInputs, outputAddresses);
5016
5185
 
5017
5186
  // 12. Verify we have enough XNA
5018
5187
  const totalRequired = burnInfo.amount + actualFee;
@@ -5214,14 +5383,15 @@ var NeuraiAssetsBundle = (function (exports) {
5214
5383
  const toAddress = await this.getToAddress();
5215
5384
  const changeAddress = await this.getChangeAddress();
5216
5385
 
5217
- const estimatedFee = await this.estimateFee(1, 3);
5386
+ const outputAddresses = [burnInfo.address, changeAddress, toAddress];
5387
+ const estimatedFee = await this.estimateFee(1, outputAddresses);
5218
5388
  const totalXNANeeded = burnInfo.amount + estimatedFee;
5219
5389
 
5220
5390
  const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
5221
5391
  const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
5222
5392
  const totalXNAInput = utxoSelection.totalXNA;
5223
5393
 
5224
- const actualFee = await this.estimateFee(baseCurrencyUTXOs.length, 3);
5394
+ const actualFee = await this.estimateFee(baseCurrencyUTXOs, outputAddresses);
5225
5395
  const totalRequired = burnInfo.amount + actualFee;
5226
5396
 
5227
5397
  if (totalXNAInput < totalRequired) {
@@ -5436,9 +5606,14 @@ var NeuraiAssetsBundle = (function (exports) {
5436
5606
 
5437
5607
  // 8. Estimate fee
5438
5608
  // Inputs: XNA UTXOs + owner token UTXO
5439
- // Outputs: burn + change + reissue operation
5440
- // (node auto-generates owner token return from the reissue entry, total = 4 physical outputs)
5441
- const estimatedFee = await this.estimateFee(2, 4);
5609
+ // Outputs: burn + change + owner token return + reissue operation
5610
+ const outputAddresses = [
5611
+ burnInfo.address,
5612
+ changeAddress,
5613
+ changeAddress, // owner token return goes to change address
5614
+ toAddress,
5615
+ ];
5616
+ const estimatedFee = await this.estimateFee(2, outputAddresses);
5442
5617
 
5443
5618
  // 9. Calculate total XNA needed
5444
5619
  const totalXNANeeded = burnInfo.amount + estimatedFee;
@@ -5448,9 +5623,9 @@ var NeuraiAssetsBundle = (function (exports) {
5448
5623
  const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
5449
5624
  const totalXNAInput = utxoSelection.totalXNA;
5450
5625
 
5451
- // 11. Recalculate fee with actual input count
5452
- const actualInputCount = baseCurrencyUTXOs.length + 1; // +1 for owner token
5453
- const actualFee = await this.estimateFee(actualInputCount, 4);
5626
+ // 11. Recalculate fee with actual inputs (PQ-aware), including owner token UTXO
5627
+ const actualFeeInputs = [...baseCurrencyUTXOs, ownerTokenUTXO];
5628
+ const actualFee = await this.estimateFee(actualFeeInputs, outputAddresses);
5454
5629
 
5455
5630
  // 12. Verify we have enough XNA
5456
5631
  const totalRequired = burnInfo.amount + actualFee;
@@ -5711,7 +5886,13 @@ var NeuraiAssetsBundle = (function (exports) {
5711
5886
  // 7. Estimate fee
5712
5887
  // Inputs: XNA UTXOs + owner token UTXO
5713
5888
  // Outputs: burn + change + owner token return + issue_unique operation
5714
- const estimatedFee = await this.estimateFee(2, 4);
5889
+ const outputAddresses = [
5890
+ burnInfo.address,
5891
+ changeAddress,
5892
+ changeAddress, // owner token return goes to change address
5893
+ toAddress,
5894
+ ];
5895
+ const estimatedFee = await this.estimateFee(2, outputAddresses);
5715
5896
 
5716
5897
  // 8. Calculate total XNA needed
5717
5898
  const totalXNANeeded = burnInfo.amount + estimatedFee;
@@ -5721,9 +5902,9 @@ var NeuraiAssetsBundle = (function (exports) {
5721
5902
  const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
5722
5903
  const totalXNAInput = utxoSelection.totalXNA;
5723
5904
 
5724
- // 10. Recalculate fee with actual input count
5725
- const actualInputCount = baseCurrencyUTXOs.length + 1; // +1 for owner token
5726
- const actualFee = await this.estimateFee(actualInputCount, 4);
5905
+ // 10. Recalculate fee with actual inputs (PQ-aware), including owner token UTXO
5906
+ const actualFeeInputs = [...baseCurrencyUTXOs, ownerTokenUTXO];
5907
+ const actualFee = await this.estimateFee(actualFeeInputs, outputAddresses);
5727
5908
 
5728
5909
  // 11. Verify we have enough XNA
5729
5910
  const totalRequired = burnInfo.amount + actualFee;
@@ -5840,7 +6021,7 @@ var NeuraiAssetsBundle = (function (exports) {
5840
6021
  *
5841
6022
  * QUALIFIER assets:
5842
6023
  * - KYC/compliance tags (e.g., #KYC_VERIFIED, #ACCREDITED)
5843
- * - Format: #NAME or #ROOT/SUB
6024
+ * - Format: #NAME or #ROOT/#SUB
5844
6025
  * - Cost: 2000 XNA (root) or 200 XNA (sub-qualifier)
5845
6026
  * - Quantity: 1-10 units only
5846
6027
  * - Units: Always 0 (non-divisible)
@@ -5971,8 +6152,8 @@ var NeuraiAssetsBundle = (function (exports) {
5971
6152
  const changeAddress = await this.getChangeAddress();
5972
6153
 
5973
6154
  // 7. Estimate fee
5974
- const outputCount = 3;
5975
- const estimatedFee = await this.estimateFee(2, outputCount);
6155
+ const outputAddresses = [burnInfo.address, changeAddress, toAddress];
6156
+ const estimatedFee = await this.estimateFee(2, outputAddresses);
5976
6157
 
5977
6158
  // 8. Calculate total XNA needed
5978
6159
  const totalXNANeeded = burnInfo.amount + estimatedFee;
@@ -5982,9 +6163,9 @@ var NeuraiAssetsBundle = (function (exports) {
5982
6163
  const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
5983
6164
  const totalXNAInput = utxoSelection.totalXNA;
5984
6165
 
5985
- // 10. Recalculate fee with actual input count
5986
- const actualInputCount = baseCurrencyUTXOs.length + parentQualifierUTXOs.length;
5987
- const actualFee = await this.estimateFee(actualInputCount, outputCount);
6166
+ // 10. Recalculate fee with actual inputs (PQ-aware), including parent qualifier UTXOs
6167
+ const actualFeeInputs = [...baseCurrencyUTXOs, ...parentQualifierUTXOs];
6168
+ const actualFee = await this.estimateFee(actualFeeInputs, outputAddresses);
5988
6169
 
5989
6170
  // 11. Verify we have enough XNA
5990
6171
  const totalRequired = burnInfo.amount + actualFee;
@@ -6223,7 +6404,13 @@ var NeuraiAssetsBundle = (function (exports) {
6223
6404
  }
6224
6405
 
6225
6406
  // 7. Estimate fee (+1 for owner token input)
6226
- const estimatedFee = await this.estimateFee(2, 4);
6407
+ const outputAddresses = [
6408
+ burnInfo.address,
6409
+ changeAddress,
6410
+ changeAddress, // owner token return goes to change address
6411
+ toAddress,
6412
+ ];
6413
+ const estimatedFee = await this.estimateFee(2, outputAddresses);
6227
6414
 
6228
6415
  // 8. Calculate total XNA needed
6229
6416
  const totalXNANeeded = burnInfo.amount + estimatedFee;
@@ -6233,8 +6420,9 @@ var NeuraiAssetsBundle = (function (exports) {
6233
6420
  const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
6234
6421
  const totalXNAInput = utxoSelection.totalXNA;
6235
6422
 
6236
- // 10. Recalculate fee with actual input count (+1 for owner token)
6237
- const actualFee = await this.estimateFee(baseCurrencyUTXOs.length + 1, 4);
6423
+ // 10. Recalculate fee with actual inputs (PQ-aware), including owner token UTXO
6424
+ const actualFeeInputs = [...baseCurrencyUTXOs, ownerTokenUTXO];
6425
+ const actualFee = await this.estimateFee(actualFeeInputs, outputAddresses);
6238
6426
 
6239
6427
  // 11. Verify we have enough XNA
6240
6428
  const totalRequired = burnInfo.amount + actualFee;
@@ -6491,7 +6679,13 @@ var NeuraiAssetsBundle = (function (exports) {
6491
6679
  const burnInfo = this.burnManager.getReissueBurn();
6492
6680
 
6493
6681
  // 8. Estimate fee
6494
- const estimatedFee = await this.estimateFee(2, 4);
6682
+ const outputAddresses = [
6683
+ burnInfo.address,
6684
+ changeAddress,
6685
+ changeAddress, // owner token return goes to change address
6686
+ toAddress,
6687
+ ];
6688
+ const estimatedFee = await this.estimateFee(2, outputAddresses);
6495
6689
 
6496
6690
  // 9. Calculate total XNA needed
6497
6691
  const totalXNANeeded = burnInfo.amount + estimatedFee;
@@ -6501,9 +6695,9 @@ var NeuraiAssetsBundle = (function (exports) {
6501
6695
  const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
6502
6696
  const totalXNAInput = utxoSelection.totalXNA;
6503
6697
 
6504
- // 11. Recalculate fee with actual input count
6505
- const actualInputCount = baseCurrencyUTXOs.length + 1; // +1 for owner token
6506
- const actualFee = await this.estimateFee(actualInputCount, 4);
6698
+ // 11. Recalculate fee with actual inputs (PQ-aware), including owner token UTXO
6699
+ const actualFeeInputs = [...baseCurrencyUTXOs, ownerTokenUTXO];
6700
+ const actualFee = await this.estimateFee(actualFeeInputs, outputAddresses);
6507
6701
 
6508
6702
  // 12. Verify we have enough XNA
6509
6703
  const totalRequired = burnInfo.amount + actualFee;
@@ -6738,7 +6932,9 @@ var NeuraiAssetsBundle = (function (exports) {
6738
6932
  : this.burnManager.getTagAddressBurn(addressCount);
6739
6933
 
6740
6934
  // 6. Estimate fee
6741
- const estimatedFee = await this.estimateFee(2, 3);
6935
+ // Outputs: burn + XNA change + tag/untag operation (sent to changeAddress)
6936
+ const outputAddresses = [burnInfo.address, changeAddress, changeAddress];
6937
+ const estimatedFee = await this.estimateFee(2, outputAddresses);
6742
6938
 
6743
6939
  // 7. Calculate total XNA needed
6744
6940
  const totalXNANeeded = burnInfo.amount + estimatedFee;
@@ -6748,9 +6944,9 @@ var NeuraiAssetsBundle = (function (exports) {
6748
6944
  const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
6749
6945
  const totalXNAInput = utxoSelection.totalXNA;
6750
6946
 
6751
- // 9. Recalculate fee with actual input count
6752
- const actualInputCount = baseCurrencyUTXOs.length + qualifierUTXOs.length;
6753
- const actualFee = await this.estimateFee(actualInputCount, 3);
6947
+ // 9. Recalculate fee with actual inputs (PQ-aware), including qualifier UTXOs
6948
+ const actualFeeInputs = [...baseCurrencyUTXOs, ...qualifierUTXOs];
6949
+ const actualFee = await this.estimateFee(actualFeeInputs, outputAddresses);
6754
6950
 
6755
6951
  // 10. Verify we have enough XNA
6756
6952
  const totalRequired = burnInfo.amount + actualFee;
@@ -6985,16 +7181,18 @@ var NeuraiAssetsBundle = (function (exports) {
6985
7181
  const burnAmount = 0;
6986
7182
 
6987
7183
  // 6. Estimate fee
6988
- const estimatedFee = await this.estimateFee(2, 3);
7184
+ // Outputs: XNA change + freeze/unfreeze operation (sent to changeAddress)
7185
+ const outputAddresses = [changeAddress, changeAddress];
7186
+ const estimatedFee = await this.estimateFee(2, outputAddresses);
6989
7187
 
6990
7188
  // 7. Select XNA UTXOs (only for fee, no burn)
6991
7189
  const utxoSelection = await this.selectUTXOs(estimatedFee, null, 0);
6992
7190
  const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
6993
7191
  const totalXNAInput = utxoSelection.totalXNA;
6994
7192
 
6995
- // 8. Recalculate fee with actual input count
6996
- const actualInputCount = baseCurrencyUTXOs.length + 1; // +1 for owner token
6997
- const actualFee = await this.estimateFee(actualInputCount, 3);
7193
+ // 8. Recalculate fee with actual inputs (PQ-aware), including owner token UTXO
7194
+ const actualFeeInputs = [...baseCurrencyUTXOs, ownerTokenUTXO];
7195
+ const actualFee = await this.estimateFee(actualFeeInputs, outputAddresses);
6998
7196
 
6999
7197
  // 9. Verify we have enough XNA for fee
7000
7198
  if (totalXNAInput < actualFee) {
@@ -7305,7 +7503,7 @@ var NeuraiAssetsBundle = (function (exports) {
7305
7503
  /**
7306
7504
  * Create a ROOT asset
7307
7505
  * @param {object} params - Asset creation parameters
7308
- * @param {string} params.assetName - Asset name (3-30 chars, A-Z 0-9 _ .)
7506
+ * @param {string} params.assetName - Asset name (3-31 visible chars on mainnet, A-Z 0-9 _ .)
7309
7507
  * @param {number} params.quantity - Total supply
7310
7508
  * @param {number} [params.units=0] - Decimal places (0-8)
7311
7509
  * @param {boolean} [params.reissuable=true] - Can mint more later