@neuraiproject/neurai-assets 1.5.1 → 1.5.3

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.
@@ -5999,66 +5999,116 @@ var NeuraiAssetsBundle = (function (exports) {
5999
5999
  }
6000
6000
 
6001
6001
  /**
6002
- * Bytes an asset payload adds on top of a plain destination output.
6003
- *
6004
- * An asset output is `<destination script> OP_XNA_ASSET <pushdata payload>
6005
- * OP_DROP`, so it costs the destination plus the wrapper (OP_XNA_ASSET,
6006
- * the pushdata prefix and OP_DROP) plus the payload itself:
6007
- *
6008
- * marker(3) + type(1) + nameLength(1) + name + kind-specific tail
6002
+ * Encoders that produce the exact scriptPubKey the node will see.
6009
6003
  *
6010
- * Sizing these as bare P2PKH outputs under-counts a transaction by tens to
6011
- * hundreds of bytes. That is invisible while the node's fee rate sits well
6012
- * above its minimum relay fee, and becomes `min relay fee not met` as soon as
6013
- * it does not which is exactly the failure this file's header warns about.
6004
+ * Sizing asset outputs from a hand-written byte formula drifted: the owner
6005
+ * token a reissue RETURNS is serialized as a transfer (it carries an amount),
6006
+ * not as the owner payload an issuance CREATES, and the null-asset-data
6007
+ * outputs of tag/freeze were not counted at all those outputs are not
6008
+ * P2PKH-plus-payload, they replace the destination script entirely. The result
6009
+ * was twelve of eighteen operations budgeting below what the node charges.
6014
6010
  *
6015
- * @param {object} descriptor - Output descriptor with `assetName` and `kind`
6016
- * @returns {number} Extra bytes, or 0 when the output carries no asset payload
6011
+ * Asking the serializer is the only way to keep this from drifting again: the
6012
+ * numbers below are not a model of the encoding, they ARE the encoding.
6017
6013
  */
6018
- function assetPayloadBytes(descriptor) {
6019
- if (!descriptor || typeof descriptor !== 'object' || !descriptor.assetName) {
6020
- return 0;
6021
- }
6014
+ const ct = requireDist_1();
6022
6015
 
6023
- // One byte per character, matching how the payload encodes the name
6024
- // (`serializeString` -> `asciiBytes`, which writes a single byte per char).
6025
- // Node's byte-length helper would be equivalent here, but it hangs off a
6026
- // global that browsers do not have: using it broke the extension bundle with
6027
- // "Buffer is not defined", and this library does much of its work there.
6028
- const nameLength = String(descriptor.assetName).length;
6029
- const kind = descriptor.kind || 'transfer';
6016
+ /** Bytes a CompactSize length prefix occupies for `n`. */
6017
+ function compactSizeBytes(n) {
6018
+ if (n < 253) return 1;
6019
+ if (n <= 0xffff) return 3;
6020
+ if (n <= 0xffffffff) return 5;
6021
+ return 9;
6022
+ }
6030
6023
 
6031
- // marker(3) + type(1) + CompactSize name length(1) + name
6032
- let payload = 5 + nameLength;
6024
+ /** An IPFS hash of the right LENGTH; only its size matters here. */
6025
+ const IPFS_PLACEHOLDER = 'Qm' + 'a'.repeat(44);
6026
+
6027
+ /**
6028
+ * The exact scriptPubKey for an asset-bearing output descriptor, or null when
6029
+ * the descriptor names no asset operation.
6030
+ *
6031
+ * Values are placeholders on purpose: every field the amount or the flag lands
6032
+ * in is fixed-width, so a zero costs the same bytes as the real number. The
6033
+ * name, the address and the presence of IPFS are the only things that move the
6034
+ * size, and those come from the descriptor.
6035
+ *
6036
+ * @param {object} descriptor - Output descriptor
6037
+ * @returns {Uint8Array|null} Encoded script, or null
6038
+ */
6039
+ function assetOutputScript(descriptor) {
6040
+ const { address, assetName, kind = 'transfer' } = descriptor;
6041
+ const ipfs = descriptor.hasIpfs ? (descriptor.ipfsHash || IPFS_PLACEHOLDER) : undefined;
6033
6042
 
6034
6043
  switch (kind) {
6035
6044
  case 'owner':
6036
- // The owner payload carries no amount: it is always exactly one unit.
6037
- break;
6045
+ return ct.encodeOwnerAssetScript(address, assetName);
6038
6046
  case 'issue':
6039
- // amount(8) + units(1) + reissuable(1) + has_ipfs(1)
6040
- payload += 11;
6041
- break;
6047
+ return ct.encodeNewAssetScript(address, assetName, 0n, 0, true, ipfs);
6042
6048
  case 'reissue':
6043
- // amount(8) + units(1) + reissuable(1)
6044
- payload += 10;
6045
- break;
6049
+ return ct.encodeReissueAssetScript(address, assetName, 0n, undefined, true, ipfs);
6050
+ case 'tag':
6051
+ return ct.encodeNullAssetTagScript(address, assetName, 'tag');
6052
+ case 'restriction':
6053
+ return ct.encodeNullAssetRestrictionScript(address, assetName, 1);
6054
+ case 'globalRestriction':
6055
+ return ct.encodeGlobalRestrictionScript(assetName, 1);
6056
+ case 'verifier':
6057
+ return ct.encodeVerifierStringScript(descriptor.verifierString || '');
6058
+ case 'transfer':
6059
+ return ct.encodeAssetTransferScript(address, assetName, 0n);
6046
6060
  default:
6047
- // transfer: amount(8)
6048
- payload += 8;
6049
- break;
6061
+ return null;
6050
6062
  }
6063
+ }
6051
6064
 
6052
- if (descriptor.hasIpfs) {
6053
- payload += 34;
6054
- }
6065
+ /**
6066
+ * Kinds whose script REPLACES the destination rather than extending it.
6067
+ *
6068
+ * A tag, a restriction or a verifier string is not "a payment with a payload
6069
+ * bolted on": there is no P2PKH to pay. Adding a destination's bytes to these
6070
+ * over-counts; treating them as plain destinations, which is what the builders
6071
+ * used to do, under-counts by far more.
6072
+ */
6073
+ const STANDALONE_KINDS = new Set(['tag', 'restriction', 'globalRestriction', 'verifier']);
6055
6074
 
6056
- // OP_XNA_ASSET(1) + pushdata prefix + OP_DROP(1). Payloads over 75 bytes
6057
- // need OP_PUSHDATA1, which is one byte wider reachable with the 121-char
6058
- // asset names testnet and regtest allow for DePIN.
6059
- const pushPrefix = payload > 75 ? 2 : 1;
6075
+ /**
6076
+ * Fallback used only when the encoders cannot express a descriptor an
6077
+ * address family they do not accept, say. Keeps the previous behaviour rather
6078
+ * than throwing in the middle of a fee estimate.
6079
+ */
6080
+ function assetPayloadBytesApprox(descriptor) {
6081
+ const nameLength = String(descriptor.assetName || '').length;
6082
+ const kind = descriptor.kind || 'transfer';
6083
+ let payload = 5 + nameLength;
6084
+ if (kind === 'issue') payload += 11;
6085
+ else if (kind === 'reissue') payload += 10;
6086
+ else if (kind !== 'owner') payload += 8;
6087
+ if (descriptor.hasIpfs) payload += 34;
6088
+ return 1 + (payload > 75 ? 2 : 1) + payload + 1;
6089
+ }
6060
6090
 
6061
- return 1 + pushPrefix + payload + 1;
6091
+ /**
6092
+ * Bytes an asset payload adds on top of a plain destination output.
6093
+ *
6094
+ * Kept for callers that only want the delta. Standalone kinds have no
6095
+ * destination to add to, so this is not meaningful for them.
6096
+ *
6097
+ * @param {object} descriptor - Output descriptor with `assetName` and `kind`
6098
+ * @returns {number} Extra bytes, or 0 when the output carries no asset payload
6099
+ */
6100
+ function assetPayloadBytes(descriptor) {
6101
+ if (!descriptor || typeof descriptor !== 'object' || !descriptor.assetName) {
6102
+ return 0;
6103
+ }
6104
+ try {
6105
+ const script = assetOutputScript(descriptor);
6106
+ if (!script) return 0;
6107
+ const base = isPQAddress(descriptor.address) ? 34 : 25;
6108
+ return script.length - base;
6109
+ } catch {
6110
+ return assetPayloadBytesApprox(descriptor);
6111
+ }
6062
6112
  }
6063
6113
 
6064
6114
  /**
@@ -6072,10 +6122,26 @@ var NeuraiAssetsBundle = (function (exports) {
6072
6122
  * @returns {number} Estimated bytes
6073
6123
  */
6074
6124
  function estimateOutputBytes(target) {
6125
+ if (typeof target !== 'string' && target && (target.assetName || target.kind === 'verifier')) {
6126
+ try {
6127
+ const script = assetOutputScript(target);
6128
+ if (script) {
6129
+ // value(8) + CompactSize(scriptLen) + script
6130
+ return 8 + compactSizeBytes(script.length) + script.length;
6131
+ }
6132
+ } catch {
6133
+ // fall through to the approximation
6134
+ }
6135
+ if (STANDALONE_KINDS.has(target.kind)) {
6136
+ return VBYTES.legacyOutputBytes;
6137
+ }
6138
+ const base = isPQAddress(target.address) ? VBYTES.pqOutputBytes : VBYTES.legacyOutputBytes;
6139
+ return base + assetPayloadBytesApprox(target);
6140
+ }
6141
+
6075
6142
  const address =
6076
6143
  typeof target === 'string' ? target : (target && target.address) || '';
6077
- const base = isPQAddress(address) ? VBYTES.pqOutputBytes : VBYTES.legacyOutputBytes;
6078
- return base + assetPayloadBytes(typeof target === 'string' ? null : target);
6144
+ return isPQAddress(address) ? VBYTES.pqOutputBytes : VBYTES.legacyOutputBytes;
6079
6145
  }
6080
6146
 
6081
6147
  /**
@@ -6105,6 +6171,8 @@ var NeuraiAssetsBundle = (function (exports) {
6105
6171
 
6106
6172
  feeSizing = {
6107
6173
  VBYTES,
6174
+ compactSizeBytes,
6175
+ assetOutputScript,
6108
6176
  isPQAddress,
6109
6177
  isPQScript,
6110
6178
  estimateInputVbytes,
@@ -6309,11 +6377,14 @@ var NeuraiAssetsBundle = (function (exports) {
6309
6377
  * @throws {InsufficientFundsError} If not enough funds
6310
6378
  */
6311
6379
  async selectBaseCurrencyUTXOs(addresses, requiredAmount, buffer = 0.1, options = {}) {
6312
- // Get all XNA UTXOs
6313
- const allUTXOs = await this.getUTXOs(addresses, null);
6314
-
6315
- // Get mempool and filter
6316
- const mempool = await this.getMempoolEntries(addresses);
6380
+ // Both reads describe the same addresses and neither feeds the other: the
6381
+ // mempool result only filters the UTXO result afterwards. Awaiting them in
6382
+ // sequence spent one extra network round trip per selection, which on a
6383
+ // remote RPC proxy is most of the time a wallet spends building anything.
6384
+ const [allUTXOs, mempool] = await Promise.all([
6385
+ this.getUTXOs(addresses, null),
6386
+ this.getMempoolEntries(addresses)
6387
+ ]);
6317
6388
  const unspentUTXOs = this.filterMempoolSpentUTXOs(allUTXOs, mempool);
6318
6389
 
6319
6390
  // Drop outpoints the caller already spends elsewhere in this transaction
@@ -6379,11 +6450,14 @@ var NeuraiAssetsBundle = (function (exports) {
6379
6450
  throw new Error('Asset name is required');
6380
6451
  }
6381
6452
 
6382
- // Get all asset UTXOs
6383
- const allUTXOs = await this.getUTXOs(addresses, assetName);
6384
-
6385
- // Get mempool and filter
6386
- const mempool = await this.getMempoolEntries(addresses);
6453
+ // Both reads describe the same addresses and neither feeds the other: the
6454
+ // mempool result only filters the UTXO result afterwards. Awaiting them in
6455
+ // sequence spent one extra network round trip per selection, which on a
6456
+ // remote RPC proxy is most of the time a wallet spends building anything.
6457
+ const [allUTXOs, mempool] = await Promise.all([
6458
+ this.getUTXOs(addresses, assetName),
6459
+ this.getMempoolEntries(addresses)
6460
+ ]);
6387
6461
  const unspentUTXOs = this.filterMempoolSpentUTXOs(allUTXOs, mempool);
6388
6462
 
6389
6463
  const excluded = toOutpointSet(options.exclude);
@@ -6505,8 +6579,10 @@ var NeuraiAssetsBundle = (function (exports) {
6505
6579
  * @returns {Promise<bigint>} Total balance in 10^8-scaled units
6506
6580
  */
6507
6581
  async getBalanceRaw(addresses, assetName = null) {
6508
- const utxos = await this.getUTXOs(addresses, assetName);
6509
- const mempool = await this.getMempoolEntries(addresses);
6582
+ const [utxos, mempool] = await Promise.all([
6583
+ this.getUTXOs(addresses, assetName),
6584
+ this.getMempoolEntries(addresses)
6585
+ ]);
6510
6586
  const availableUTXOs = this.filterMempoolSpentUTXOs(utxos, mempool);
6511
6587
 
6512
6588
  return sumProtocolIntegers(availableUTXOs, 'satoshis', `${assetName || 'XNA'} utxo.satoshis`);
@@ -7994,6 +8070,44 @@ var NeuraiAssetsBundle = (function (exports) {
7994
8070
  throw new Error('build must be implemented by subclass');
7995
8071
  }
7996
8072
 
8073
+ /**
8074
+ * Start the fee-rate lookup without waiting for it.
8075
+ *
8076
+ * Every build needs the fee rate, and it depends on nothing the build
8077
+ * computes, so there is no reason for it to wait its turn behind the reads
8078
+ * that come first. Kicking it off early lets it share a round trip with
8079
+ * them; `estimateFee`/`estimateFeeSats` await the same memoised promise, so
8080
+ * the call still happens exactly once and a failure still surfaces there.
8081
+ *
8082
+ * The trailing `catch` only marks the promise as handled while nothing is
8083
+ * awaiting it — it attaches to a derived promise, so the original still
8084
+ * rejects for whoever awaits it later.
8085
+ *
8086
+ * @returns {void}
8087
+ */
8088
+ warmFeeRate() {
8089
+ if (this._feeRatePromise) {
8090
+ return;
8091
+ }
8092
+ this._feeRatePromise = this.utxoSelector.getFeeRate();
8093
+ this._feeRatePromise.catch(() => {});
8094
+ }
8095
+
8096
+ /**
8097
+ * Start every read a build needs but that depends on nothing the build
8098
+ * computes: the fee rate and the NIP-040 asset marker.
8099
+ *
8100
+ * Both are memoised, so warming them costs no extra call — it only moves
8101
+ * them off the critical path. Every builder that reaches here goes on to
8102
+ * stamp a marker, so neither read is ever speculative.
8103
+ *
8104
+ * @returns {void}
8105
+ */
8106
+ warmChainReads() {
8107
+ this.warmFeeRate();
8108
+ this.resolveAssetMarker().catch(() => {});
8109
+ }
8110
+
7997
8111
  /**
7998
8112
  * Estimate transaction fee.
7999
8113
  *
@@ -8007,9 +8121,7 @@ var NeuraiAssetsBundle = (function (exports) {
8007
8121
  * @returns {Promise<number>} Estimated fee in XNA
8008
8122
  */
8009
8123
  async estimateFee(inputs, outputs) {
8010
- if (!this._feeRatePromise) {
8011
- this._feeRatePromise = this.utxoSelector.getFeeRate();
8012
- }
8124
+ this.warmFeeRate();
8013
8125
  const feeRate = await this._feeRatePromise;
8014
8126
  return this.utxoSelector.estimateFee(inputs, outputs, feeRate);
8015
8127
  }
@@ -8040,9 +8152,7 @@ var NeuraiAssetsBundle = (function (exports) {
8040
8152
  * @returns {Promise<bigint>} Estimated fee in satoshis
8041
8153
  */
8042
8154
  async estimateFeeSats(inputs, outputs) {
8043
- if (!this._feeRatePromise) {
8044
- this._feeRatePromise = this.utxoSelector.getFeeRate();
8045
- }
8155
+ this.warmFeeRate();
8046
8156
  const feeRate = await this._feeRatePromise;
8047
8157
  return this.utxoSelector.estimateFeeSats(inputs, outputs, feeRate);
8048
8158
  }
@@ -8085,6 +8195,10 @@ var NeuraiAssetsBundle = (function (exports) {
8085
8195
  initialInputHint = 1
8086
8196
  } = options;
8087
8197
 
8198
+ // Covers the builders that never call assetExists, whose first read is
8199
+ // this one.
8200
+ this.warmChainReads();
8201
+
8088
8202
  const addresses = await this._getAddresses();
8089
8203
  const excluded = UTXOSelector.toOutpointSet(exclude);
8090
8204
  // toOutpointSet returns the caller's Set untouched when it already is one;
@@ -8714,6 +8828,9 @@ var NeuraiAssetsBundle = (function (exports) {
8714
8828
  * @returns {Promise<boolean>} True if exists
8715
8829
  */
8716
8830
  async assetExists(assetName) {
8831
+ // This is the first read a build performs and its answer gates nothing but
8832
+ // the guard below, so let the build's other chain reads travel alongside it.
8833
+ this.warmChainReads();
8717
8834
  try {
8718
8835
  const assetData = await this.rpc('getassetdata', [assetName]);
8719
8836
  return assetData !== null && assetData !== undefined;
@@ -8733,6 +8850,9 @@ var NeuraiAssetsBundle = (function (exports) {
8733
8850
  * @returns {Promise<object|null>} Asset data or null if not found
8734
8851
  */
8735
8852
  async getAssetData(assetName) {
8853
+ // This is the first read a build performs and its answer gates nothing but
8854
+ // the guard below, so let the build's other chain reads travel alongside it.
8855
+ this.warmChainReads();
8736
8856
  try {
8737
8857
  return await this.rpc('getassetdata', [assetName]);
8738
8858
  } catch (error) {
@@ -9031,8 +9151,24 @@ var NeuraiAssetsBundle = (function (exports) {
9031
9151
  throw new Error('Cannot parse parent asset from SUB asset name');
9032
9152
  }
9033
9153
 
9034
- // 3. Check if parent asset exists
9035
- const parentExists = await this.assetExists(parentAssetName);
9154
+ // El token owner no depende de nada de lo que sigue: pedirlo a la vez que
9155
+ // las lecturas del asset ahorra una ida y vuelta completa. Se ESPERA en su
9156
+ // sitio de siempre, así que el orden de los errores no cambia.
9157
+ const ownerTokenName = AssetNameParser.getOwnerTokenName(parentAssetName);
9158
+ const addresses = await this._getAddresses();
9159
+ const ownerTokenLookup = this.ownerTokenManager.findOwnerTokenUTXO(
9160
+ ownerTokenName,
9161
+ addresses
9162
+ );
9163
+ ownerTokenLookup.catch(() => {});
9164
+
9165
+ // 3-4. Existencia del padre y del sub: independientes entre sí, así que
9166
+ // se preguntan a la vez. Se comprueban en el mismo orden de antes, luego
9167
+ // «no existe el padre» sigue ganando a «el sub ya existe».
9168
+ const [parentExists, subExists] = await Promise.all([
9169
+ this.assetExists(parentAssetName),
9170
+ this.assetExists(assetName)
9171
+ ]);
9036
9172
  if (!parentExists) {
9037
9173
  throw new ParentAssetNotFoundError(
9038
9174
  `Parent asset ${parentAssetName} does not exist. You must create the ROOT asset first.`,
@@ -9040,8 +9176,6 @@ var NeuraiAssetsBundle = (function (exports) {
9040
9176
  );
9041
9177
  }
9042
9178
 
9043
- // 4. Check if SUB asset already exists
9044
- const subExists = await this.assetExists(assetName);
9045
9179
  if (subExists) {
9046
9180
  throw new AssetExistsError(
9047
9181
  `Asset ${assetName} already exists on the blockchain`,
@@ -9050,18 +9184,13 @@ var NeuraiAssetsBundle = (function (exports) {
9050
9184
  }
9051
9185
 
9052
9186
  // 5. Get addresses
9053
- const addresses = await this._getAddresses();
9054
9187
  const toAddress = await this.getToAddress();
9055
9188
  const changeAddress = await this.getChangeAddress();
9056
9189
 
9057
9190
  // 6. Find parent's owner token (CRITICAL: must have this)
9058
- const ownerTokenName = AssetNameParser.getOwnerTokenName(parentAssetName);
9059
9191
  let ownerTokenUTXO;
9060
9192
  try {
9061
- ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
9062
- ownerTokenName,
9063
- addresses
9064
- );
9193
+ ownerTokenUTXO = await ownerTokenLookup;
9065
9194
  } catch (error) {
9066
9195
  if (error instanceof OwnerTokenNotFoundError) {
9067
9196
  throw new OwnerTokenNotFoundError(
@@ -9082,7 +9211,11 @@ var NeuraiAssetsBundle = (function (exports) {
9082
9211
  const outputAddresses = [
9083
9212
  burnInfo.address,
9084
9213
  changeAddress,
9085
- { address: changeAddress, assetName: ownerTokenName, kind: 'owner' },
9214
+ // El token owner que la operación GASTA y devuelve viaja como
9215
+ // transferencia (lleva importe), no con el payload 'owner', que sólo
9216
+ // describe el token que una emisión CREA. Estimarlo como 'owner' dejaba
9217
+ // la transacción 8 bytes por debajo de lo que el nodo cobra.
9218
+ { address: changeAddress, assetName: ownerTokenName },
9086
9219
  { address: toAddress, assetName, kind: 'issue', hasIpfs },
9087
9220
  { address: changeAddress, assetName: `${assetName}!`, kind: 'owner' },
9088
9221
  ];
@@ -9466,6 +9599,19 @@ var NeuraiAssetsBundle = (function (exports) {
9466
9599
  newIpfs
9467
9600
  } = this.params;
9468
9601
 
9602
+ // El token owner no depende de nada de lo que sigue: pedirlo a la vez que
9603
+ // los datos del asset ahorra una ida y vuelta completa. Se ESPERA en su
9604
+ // sitio de siempre (paso 6), así que el orden de los errores no cambia:
9605
+ // «el asset no existe» y «no es reemitible» se siguen lanzando antes que
9606
+ // «no tienes el token owner».
9607
+ const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
9608
+ const addresses = await this._getAddresses();
9609
+ const ownerTokenLookup = this.ownerTokenManager.findOwnerTokenUTXO(
9610
+ ownerTokenName,
9611
+ addresses
9612
+ );
9613
+ ownerTokenLookup.catch(() => {});
9614
+
9469
9615
  // 2. Get asset data to verify it exists and is reissuable
9470
9616
  const assetData = await this.getAssetData(assetName);
9471
9617
  if (!assetData) {
@@ -9501,19 +9647,14 @@ var NeuraiAssetsBundle = (function (exports) {
9501
9647
  }
9502
9648
 
9503
9649
  // 5. Get addresses
9504
- const addresses = await this._getAddresses();
9505
9650
  const toAddress = await this.getToAddress();
9506
9651
  const changeAddress = await this.getChangeAddress();
9507
9652
  const isDepinAsset = AssetNameParser.isDepin(assetName);
9508
9653
 
9509
9654
  // 6. Find owner token (CRITICAL: must have this)
9510
- const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
9511
9655
  let ownerTokenUTXO;
9512
9656
  try {
9513
- ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
9514
- ownerTokenName,
9515
- addresses
9516
- );
9657
+ ownerTokenUTXO = await ownerTokenLookup;
9517
9658
  } catch (error) {
9518
9659
  if (error instanceof OwnerTokenNotFoundError) {
9519
9660
  throw new OwnerTokenNotFoundError(
@@ -9534,7 +9675,11 @@ var NeuraiAssetsBundle = (function (exports) {
9534
9675
  const outputAddresses = [
9535
9676
  burnInfo.address,
9536
9677
  changeAddress,
9537
- { address: changeAddress, assetName: ownerTokenName, kind: 'owner' },
9678
+ // El token owner que la operación GASTA y devuelve viaja como
9679
+ // transferencia (lleva importe), no con el payload 'owner', que sólo
9680
+ // describe el token que una emisión CREA. Estimarlo como 'owner' dejaba
9681
+ // la transacción 8 bytes por debajo de lo que el nodo cobra.
9682
+ { address: changeAddress, assetName: ownerTokenName },
9538
9683
  { address: toAddress, assetName, kind: 'reissue', hasIpfs: Boolean(newIpfs) },
9539
9684
  ];
9540
9685
  // Fund the XNA side. The owner-token input counts towards the size
@@ -9802,7 +9947,8 @@ var NeuraiAssetsBundle = (function (exports) {
9802
9947
  ...recipients.map(r => ({ address: r.address, assetName })),
9803
9948
  { address: changeAddress, assetName }, // asset change (harmless over-count if absent)
9804
9949
  ...(isDepin
9805
- ? [{ address: changeAddress, assetName: ownerTokenName, kind: 'owner' }]
9950
+ // Escolta: se gasta y se devuelve, luego es una transferencia.
9951
+ ? [{ address: changeAddress, assetName: ownerTokenName }]
9806
9952
  : []),
9807
9953
  ];
9808
9954
 
@@ -10086,6 +10232,17 @@ var NeuraiAssetsBundle = (function (exports) {
10086
10232
  ipfsHashes = []
10087
10233
  } = this.params;
10088
10234
 
10235
+ // El token owner no depende de nada de lo que sigue: pedirlo a la vez que
10236
+ // las lecturas del asset ahorra una ida y vuelta completa. Se ESPERA en su
10237
+ // sitio de siempre, así que el orden de los errores no cambia.
10238
+ const ownerTokenName = AssetNameParser.getOwnerTokenName(rootName);
10239
+ const addresses = await this._getAddresses();
10240
+ const ownerTokenLookup = this.ownerTokenManager.findOwnerTokenUTXO(
10241
+ ownerTokenName,
10242
+ addresses
10243
+ );
10244
+ ownerTokenLookup.catch(() => {});
10245
+
10089
10246
  // 2. Check if parent asset exists
10090
10247
  const parentExists = await this.assetExists(rootName);
10091
10248
  if (!parentExists) {
@@ -10095,31 +10252,31 @@ var NeuraiAssetsBundle = (function (exports) {
10095
10252
  );
10096
10253
  }
10097
10254
 
10098
- // 3. Check if any of the unique assets already exist
10099
- for (const tag of assetTags) {
10100
- const fullName = `${rootName}#${tag}`;
10101
- const exists = await this.assetExists(fullName);
10102
- if (exists) {
10103
- throw new AssetExistsError(
10104
- `Unique asset ${fullName} already exists on the blockchain`,
10105
- fullName
10106
- );
10107
- }
10255
+ // 3. Check if any of the unique assets already exist.
10256
+ // En fila, emitir diez únicos costaba diez idas y vueltas antes de
10257
+ // empezar a construir. Ninguna depende de la anterior. El resultado se
10258
+ // recorre en orden, así que el error sigue nombrando el primer nombre
10259
+ // repetido de la lista, no el que el nodo conteste primero.
10260
+ const uniqueNames = assetTags.map(tag => `${rootName}#${tag}`);
10261
+ const uniqueExists = await Promise.all(
10262
+ uniqueNames.map(fullName => this.assetExists(fullName))
10263
+ );
10264
+ const takenIndex = uniqueExists.findIndex(Boolean);
10265
+ if (takenIndex !== -1) {
10266
+ throw new AssetExistsError(
10267
+ `Unique asset ${uniqueNames[takenIndex]} already exists on the blockchain`,
10268
+ uniqueNames[takenIndex]
10269
+ );
10108
10270
  }
10109
10271
 
10110
10272
  // 4. Get addresses
10111
- const addresses = await this._getAddresses();
10112
10273
  const toAddress = await this.getToAddress();
10113
10274
  const changeAddress = await this.getChangeAddress();
10114
10275
 
10115
10276
  // 5. Find parent's owner token (CRITICAL: must have this)
10116
- const ownerTokenName = AssetNameParser.getOwnerTokenName(rootName);
10117
10277
  let ownerTokenUTXO;
10118
10278
  try {
10119
- ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
10120
- ownerTokenName,
10121
- addresses
10122
- );
10279
+ ownerTokenUTXO = await ownerTokenLookup;
10123
10280
  } catch (error) {
10124
10281
  if (error instanceof OwnerTokenNotFoundError) {
10125
10282
  throw new OwnerTokenNotFoundError(
@@ -10141,7 +10298,11 @@ var NeuraiAssetsBundle = (function (exports) {
10141
10298
  const outputAddresses = [
10142
10299
  burnInfo.address,
10143
10300
  changeAddress,
10144
- { address: changeAddress, assetName: ownerTokenName, kind: 'owner' },
10301
+ // El token owner que la operación GASTA y devuelve viaja como
10302
+ // transferencia (lleva importe), no con el payload 'owner', que sólo
10303
+ // describe el token que una emisión CREA. Estimarlo como 'owner' dejaba
10304
+ // la transacción 8 bytes por debajo de lo que el nodo cobra.
10305
+ { address: changeAddress, assetName: ownerTokenName },
10145
10306
  ...assetTags.map(tag => ({
10146
10307
  address: toAddress,
10147
10308
  assetName: `${rootName}#${tag}`,
@@ -10625,6 +10786,17 @@ var NeuraiAssetsBundle = (function (exports) {
10625
10786
  ipfsHash = ''
10626
10787
  } = this.params;
10627
10788
 
10789
+ // El token owner no depende de nada de lo que sigue: pedirlo a la vez que
10790
+ // las lecturas del asset ahorra una ida y vuelta completa. Se ESPERA en su
10791
+ // sitio de siempre, así que el orden de los errores no cambia.
10792
+ const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
10793
+ const addresses = await this._getAddresses();
10794
+ const ownerTokenLookup = this.ownerTokenManager.findOwnerTokenUTXO(
10795
+ ownerTokenName,
10796
+ addresses
10797
+ );
10798
+ ownerTokenLookup.catch(() => {});
10799
+
10628
10800
  // 2. Check if asset already exists
10629
10801
  const exists = await this.assetExists(assetName);
10630
10802
  if (exists) {
@@ -10641,18 +10813,13 @@ var NeuraiAssetsBundle = (function (exports) {
10641
10813
  const burnInfo = this.burnManager.getIssueRestrictedBurn();
10642
10814
 
10643
10815
  // 5. Get addresses
10644
- const addresses = await this._getAddresses();
10645
10816
  const toAddress = await this.getToAddress();
10646
10817
  const changeAddress = await this.getChangeAddress();
10647
10818
 
10648
10819
  // 6. Find owner token UTXO (CRITICAL: node requires it as input)
10649
- const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
10650
10820
  let ownerTokenUTXO;
10651
10821
  try {
10652
- ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
10653
- ownerTokenName,
10654
- addresses
10655
- );
10822
+ ownerTokenUTXO = await ownerTokenLookup;
10656
10823
  } catch (error) {
10657
10824
  if (error instanceof OwnerTokenNotFoundError) {
10658
10825
  throw new OwnerTokenNotFoundError(
@@ -10664,11 +10831,16 @@ var NeuraiAssetsBundle = (function (exports) {
10664
10831
  }
10665
10832
 
10666
10833
  // 7. Estimate fee (+1 for owner token input)
10834
+ // Las salidas de asset deben describirse como lo que el nodo serializa.
10835
+ // Como direcciones desnudas se contaban 34 bytes por salida y el payload
10836
+ // entero quedaba sin pagar; las de datos nulos (tag, congelación,
10837
+ // verificador) ni siquiera llevan destino: su script SUSTITUYE al P2PKH.
10667
10838
  const outputAddresses = [
10668
10839
  burnInfo.address,
10669
10840
  changeAddress,
10670
- changeAddress, // owner token return goes to change address
10671
- toAddress,
10841
+ { kind: 'verifier', assetName, verifierString },
10842
+ { address: changeAddress, assetName: ownerTokenName },
10843
+ { address: toAddress, assetName, kind: 'issue', hasIpfs: Boolean(this.params.ipfsHash) },
10672
10844
  ];
10673
10845
  // Fund the XNA side. The owner-token input counts towards the size
10674
10846
  // estimate from the first round and is excluded from XNA selection.
@@ -10878,6 +11050,17 @@ var NeuraiAssetsBundle = (function (exports) {
10878
11050
  newIpfs
10879
11051
  } = this.params;
10880
11052
 
11053
+ // El token owner no depende de nada de lo que sigue: pedirlo a la vez que
11054
+ // las lecturas del asset ahorra una ida y vuelta completa. Se ESPERA en su
11055
+ // sitio de siempre, así que el orden de los errores no cambia.
11056
+ const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
11057
+ const addresses = await this._getAddresses();
11058
+ const ownerTokenLookup = this.ownerTokenManager.findOwnerTokenUTXO(
11059
+ ownerTokenName,
11060
+ addresses
11061
+ );
11062
+ ownerTokenLookup.catch(() => {});
11063
+
10881
11064
  // 2. Get asset data to verify it exists and is reissuable
10882
11065
  const assetData = await this.getAssetData(assetName);
10883
11066
  if (!assetData) {
@@ -10913,18 +11096,13 @@ var NeuraiAssetsBundle = (function (exports) {
10913
11096
  }
10914
11097
 
10915
11098
  // 5. Get addresses
10916
- const addresses = await this._getAddresses();
10917
11099
  const toAddress = await this.getToAddress();
10918
11100
  const changeAddress = await this.getChangeAddress();
10919
11101
 
10920
11102
  // 6. Find owner token (CRITICAL: must have this)
10921
- const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
10922
11103
  let ownerTokenUTXO;
10923
11104
  try {
10924
- ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
10925
- ownerTokenName,
10926
- addresses
10927
- );
11105
+ ownerTokenUTXO = await ownerTokenLookup;
10928
11106
  } catch (error) {
10929
11107
  if (error instanceof OwnerTokenNotFoundError) {
10930
11108
  throw new OwnerTokenNotFoundError(
@@ -10940,11 +11118,18 @@ var NeuraiAssetsBundle = (function (exports) {
10940
11118
  const burnInfo = this.burnManager.getReissueBurn();
10941
11119
 
10942
11120
  // 8. Estimate fee
11121
+ // Las salidas de asset deben describirse como lo que el nodo serializa.
11122
+ // Como direcciones desnudas se contaban 34 bytes por salida y el payload
11123
+ // entero quedaba sin pagar; las de datos nulos (tag, congelación,
11124
+ // verificador) ni siquiera llevan destino: su script SUSTITUYE al P2PKH.
10943
11125
  const outputAddresses = [
10944
11126
  burnInfo.address,
10945
11127
  changeAddress,
10946
- changeAddress, // owner token return goes to change address
10947
- toAddress,
11128
+ ...(changeVerifier && newVerifier
11129
+ ? [{ kind: 'verifier', assetName, verifierString: newVerifier }]
11130
+ : []),
11131
+ { address: changeAddress, assetName: ownerTokenName },
11132
+ { address: toAddress, assetName, kind: 'reissue', hasIpfs: Boolean(this.params.ipfsHash) },
10948
11133
  ];
10949
11134
  // Fund the XNA side. The owner-token input counts towards the size
10950
11135
  // estimate from the first round and is excluded from XNA selection.
@@ -11204,7 +11389,20 @@ var NeuraiAssetsBundle = (function (exports) {
11204
11389
 
11205
11390
  // 6. Estimate fee
11206
11391
  // Outputs: burn + XNA change + tag/untag operation (sent to changeAddress)
11207
- const outputAddresses = [burnInfo.address, changeAddress, changeAddress];
11392
+ // Las salidas de asset deben describirse como lo que el nodo serializa.
11393
+ // Como direcciones desnudas se contaban 34 bytes por salida y el payload
11394
+ // entero quedaba sin pagar; las de datos nulos (tag, congelación,
11395
+ // verificador) ni siquiera llevan destino: su script SUSTITUYE al P2PKH.
11396
+ const outputAddresses = [
11397
+ burnInfo.address,
11398
+ changeAddress,
11399
+ // El nodo devuelve el resto del qualifier a la dirección de cambio.
11400
+ { address: changeAddress, assetName: qualifierName },
11401
+ // Una salida de datos nulos por dirección etiquetada.
11402
+ ...targetAddresses.map(target => ({
11403
+ address: target, assetName: qualifierName, kind: 'tag'
11404
+ }))
11405
+ ];
11208
11406
  // 7-11. Fund the XNA side. The qualifier inputs count towards the size
11209
11407
  // estimate from the first round and are excluded from XNA selection.
11210
11408
  const burnSats = this.xnaAmountToSats(burnInfo.amount, { label: 'burn amount' });
@@ -11418,6 +11616,17 @@ var NeuraiAssetsBundle = (function (exports) {
11418
11616
 
11419
11617
  const { assetName } = this.params;
11420
11618
 
11619
+ // El token owner no depende de nada de lo que sigue: pedirlo a la vez que
11620
+ // las lecturas del asset ahorra una ida y vuelta completa. Se ESPERA en su
11621
+ // sitio de siempre, así que el orden de los errores no cambia.
11622
+ const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
11623
+ const addresses = await this._getAddresses();
11624
+ const ownerTokenLookup = this.ownerTokenManager.findOwnerTokenUTXO(
11625
+ ownerTokenName,
11626
+ addresses
11627
+ );
11628
+ ownerTokenLookup.catch(() => {});
11629
+
11421
11630
  // 2. Check if asset exists and is restricted
11422
11631
  const assetData = await this.getAssetData(assetName);
11423
11632
  if (!assetData) {
@@ -11428,17 +11637,12 @@ var NeuraiAssetsBundle = (function (exports) {
11428
11637
  }
11429
11638
 
11430
11639
  // 3. Get wallet addresses
11431
- const addresses = await this._getAddresses();
11432
11640
  const changeAddress = await this.getChangeAddress();
11433
11641
 
11434
11642
  // 4. Find owner token (CRITICAL: must have this)
11435
- const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
11436
11643
  let ownerTokenUTXO;
11437
11644
  try {
11438
- ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
11439
- ownerTokenName,
11440
- addresses
11441
- );
11645
+ ownerTokenUTXO = await ownerTokenLookup;
11442
11646
  } catch (error) {
11443
11647
  if (error instanceof OwnerTokenNotFoundError) {
11444
11648
  throw new OwnerTokenNotFoundError(
@@ -11454,7 +11658,22 @@ var NeuraiAssetsBundle = (function (exports) {
11454
11658
 
11455
11659
  // 6. Estimate fee
11456
11660
  // Outputs: XNA change + freeze/unfreeze operation (sent to changeAddress)
11457
- const outputAddresses = [changeAddress, changeAddress];
11661
+ // Las salidas de asset deben describirse como lo que el nodo serializa.
11662
+ // Como direcciones desnudas se contaban 34 bytes por salida y el payload
11663
+ // entero quedaba sin pagar; las de datos nulos (tag, congelación,
11664
+ // verificador) ni siquiera llevan destino: su script SUSTITUYE al P2PKH.
11665
+ const isGlobal = operationType === 'FREEZE_ASSET' || operationType === 'UNFREEZE_ASSET';
11666
+ const frozenAddresses = isGlobal ? [] : (this.params.addresses || []);
11667
+ const outputAddresses = [
11668
+ changeAddress,
11669
+ // El token owner se gasta y se devuelve: transferencia.
11670
+ { address: changeAddress, assetName: ownerTokenName },
11671
+ ...(isGlobal
11672
+ ? [{ assetName, kind: 'globalRestriction' }]
11673
+ : frozenAddresses.map(target => ({
11674
+ address: target, assetName, kind: 'restriction'
11675
+ })))
11676
+ ];
11458
11677
  // 7-10. Fund the XNA side (fee only, this operation does not burn). The
11459
11678
  // owner-token input counts towards the size estimate from the first
11460
11679
  // round and is excluded from XNA selection.