@neuraiproject/neurai-assets 1.3.2 → 1.3.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.
package/dist/index.cjs CHANGED
@@ -5770,6 +5770,274 @@ function requireReissueBuilder () {
5770
5770
  return ReissueBuilder_1;
5771
5771
  }
5772
5772
 
5773
+ /**
5774
+ * Transfer Builder
5775
+ * Builds transactions that transfer an existing asset to one or more recipients.
5776
+ *
5777
+ * Works for any asset type (regular, sub, restricted, DePIN). The only
5778
+ * type-specific rule lives in Neurai consensus for DePIN (`&`) assets, which are
5779
+ * soulbound: a DePIN transfer is only valid if the same transaction
5780
+ * 1. SPENDS the asset's owner token `&NAME!` as an input, and
5781
+ * 2. re-creates (transfers) that owner token in an output.
5782
+ * See Neurai-DePIN/src/consensus/tx_verify.cpp (bad-txns-depin-transfer-not-by-owner).
5783
+ * For non-DePIN assets no owner token is required for a plain transfer.
5784
+ *
5785
+ * Owner-token destination: the owner token is returned to the sender's change
5786
+ * address — the asset moves to the recipient but authority stays with the owner
5787
+ * (soulbound semantics). Transferring ownership itself is out of scope here.
5788
+ *
5789
+ * This builder mirrors ReissueBuilder (which also spends + returns an owner
5790
+ * token) but, since a transfer has no reissue entry, it adds the owner-token
5791
+ * return output explicitly via OwnerTokenManager.
5792
+ */
5793
+
5794
+ var TransferBuilder_1;
5795
+ var hasRequiredTransferBuilder;
5796
+
5797
+ function requireTransferBuilder () {
5798
+ if (hasRequiredTransferBuilder) return TransferBuilder_1;
5799
+ hasRequiredTransferBuilder = 1;
5800
+ const BaseAssetTransactionBuilder = requireBaseAssetTransactionBuilder();
5801
+ const { OutputFormatter, AssetNameParser } = requireUtils();
5802
+ const { OwnerTokenNotFoundError } = requireErrors();
5803
+
5804
+ class TransferBuilder extends BaseAssetTransactionBuilder {
5805
+ /**
5806
+ * Validate transfer parameters
5807
+ * @param {object} params - Transfer parameters
5808
+ * @throws {Error} If validation fails
5809
+ */
5810
+ validateParams(params) {
5811
+ if (!params.assetName) {
5812
+ throw new Error('assetName is required');
5813
+ }
5814
+
5815
+ if (!Array.isArray(params.recipients) || params.recipients.length === 0) {
5816
+ throw new Error('recipients is required (non-empty array of { address, amount })');
5817
+ }
5818
+
5819
+ params.recipients.forEach((recipient, index) => {
5820
+ if (!recipient || !recipient.address) {
5821
+ throw new Error(`recipients[${index}].address is required`);
5822
+ }
5823
+ if (recipient.amount === undefined || recipient.amount === null) {
5824
+ throw new Error(`recipients[${index}].amount is required`);
5825
+ }
5826
+ if (recipient.amount <= 0) {
5827
+ throw new Error(`recipients[${index}].amount must be greater than 0`);
5828
+ }
5829
+ });
5830
+
5831
+ return true;
5832
+ }
5833
+
5834
+ /**
5835
+ * Build transfer transaction
5836
+ * @returns {Promise<object>} Transaction result
5837
+ */
5838
+ async build() {
5839
+ // 1. Validate parameters
5840
+ this.validateParams(this.params);
5841
+
5842
+ const { assetName, recipients } = this.params;
5843
+
5844
+ // Total amount to send, in user-facing asset units (NOT raw 10^8 sats).
5845
+ // selectAssetUTXOs / the createrawtransaction transfer output both expect
5846
+ // display units and scale by 10^8 themselves — pre-multiplying would
5847
+ // double-scale (see UTXOSelector.selectAssetUTXOs / BaseBuilder.toSatoshis).
5848
+ const totalAssetUnits = recipients.reduce((sum, r) => sum + r.amount, 0);
5849
+
5850
+ // 2. Addresses
5851
+ const addresses = await this._getAddresses();
5852
+ const changeAddress = await this.getChangeAddress();
5853
+
5854
+ // 3. DePIN detection + owner token lookup (soulbound rule)
5855
+ const isDepin = AssetNameParser.isDepin(assetName);
5856
+ let ownerTokenName = null;
5857
+ let ownerTokenUTXO = null;
5858
+ if (isDepin) {
5859
+ ownerTokenName = AssetNameParser.getOwnerTokenName(assetName); // &NAME -> &NAME!
5860
+ try {
5861
+ ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
5862
+ ownerTokenName,
5863
+ addresses
5864
+ );
5865
+ } catch (error) {
5866
+ if (error instanceof OwnerTokenNotFoundError) {
5867
+ throw new OwnerTokenNotFoundError(
5868
+ `You must own the asset's owner token (${ownerTokenName}) to transfer ` +
5869
+ `this DePIN asset. DePIN assets are soulbound: the transfer must be ` +
5870
+ `authorized by the owner.`,
5871
+ ownerTokenName
5872
+ );
5873
+ }
5874
+ throw error;
5875
+ }
5876
+ }
5877
+
5878
+ // 4. Output addresses used only for the fee (vsize) estimate. Include every
5879
+ // potential output so the fee is never under-estimated.
5880
+ const outputAddresses = [
5881
+ changeAddress, // XNA change
5882
+ ...recipients.map(r => r.address), // one transfer per recipient
5883
+ changeAddress, // asset change (harmless over-count if absent)
5884
+ ...(isDepin ? [changeAddress] : []), // owner token return
5885
+ ];
5886
+
5887
+ // 5. First (rough) fee estimate, then select asset + XNA UTXOs.
5888
+ const estimatedFee = await this.estimateFee(isDepin ? 3 : 2, outputAddresses);
5889
+ const utxoSelection = await this.selectUTXOs(estimatedFee, assetName, totalAssetUnits);
5890
+ const assetUTXOs = utxoSelection.assetUTXOs;
5891
+ const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
5892
+ let totalXNAInput = utxoSelection.totalXNA;
5893
+
5894
+ // Asset change computed in raw 10^8-sats to avoid float drift, then back to units.
5895
+ const assetInputRawSats = assetUTXOs.reduce((sum, u) => sum + u.satoshis, 0);
5896
+ const totalAssetRawSats = Math.round(totalAssetUnits * 100000000);
5897
+ const assetChangeRawSats = assetInputRawSats - totalAssetRawSats;
5898
+ const assetChangeUnits = assetChangeRawSats / 100000000;
5899
+
5900
+ // 6. Recompute the fee with the real inputs (PQ-aware), including the owner
5901
+ // token when DePIN, then top up XNA if the rough estimate fell short.
5902
+ const actualFeeInputs = [
5903
+ ...baseCurrencyUTXOs,
5904
+ ...assetUTXOs,
5905
+ ...(isDepin ? [ownerTokenUTXO] : []),
5906
+ ];
5907
+ const actualFee = await this.estimateFee(actualFeeInputs, outputAddresses);
5908
+
5909
+ if (totalXNAInput < actualFee) {
5910
+ const additionalNeeded = actualFee - totalXNAInput + 0.001;
5911
+ const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
5912
+ baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
5913
+ totalXNAInput += additionalSelection.totalXNA;
5914
+ }
5915
+
5916
+ // 7. XNA change (no burn for a transfer)
5917
+ const finalXNAInput = baseCurrencyUTXOs.reduce(
5918
+ (sum, utxo) => sum + utxo.satoshis / 100000000,
5919
+ 0
5920
+ );
5921
+ const xnaChange = finalXNAInput - actualFee;
5922
+
5923
+ // 8. Build inputs: asset UTXOs + [owner token] + XNA UTXOs
5924
+ const inputs = [];
5925
+
5926
+ assetUTXOs.forEach(utxo => {
5927
+ inputs.push({
5928
+ txid: utxo.txid,
5929
+ vout: utxo.outputIndex,
5930
+ address: utxo.address,
5931
+ assetName: utxo.assetName,
5932
+ satoshis: utxo.satoshis,
5933
+ });
5934
+ });
5935
+
5936
+ if (isDepin) {
5937
+ inputs.push({
5938
+ txid: ownerTokenUTXO.txid,
5939
+ vout: ownerTokenUTXO.outputIndex,
5940
+ address: ownerTokenUTXO.address,
5941
+ assetName: ownerTokenUTXO.assetName,
5942
+ satoshis: ownerTokenUTXO.satoshis,
5943
+ });
5944
+ }
5945
+
5946
+ baseCurrencyUTXOs.forEach(utxo => {
5947
+ inputs.push({
5948
+ txid: utxo.txid,
5949
+ vout: utxo.outputIndex,
5950
+ address: utxo.address,
5951
+ satoshis: utxo.satoshis,
5952
+ });
5953
+ });
5954
+
5955
+ // 9. Build outputs (unordered — outputOrderer enforces protocol order)
5956
+ const outputs = [];
5957
+
5958
+ // XNA change
5959
+ if (xnaChange > 0.00000001) {
5960
+ outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
5961
+ }
5962
+
5963
+ // One transfer per recipient (display units; the daemon scales by 10^8)
5964
+ recipients.forEach(r => {
5965
+ outputs.push({ [r.address]: OutputFormatter.formatTransferOutput(assetName, r.amount) });
5966
+ });
5967
+
5968
+ // Asset change back to the sender
5969
+ if (assetChangeRawSats > 0) {
5970
+ outputs.push({
5971
+ [changeAddress]: OutputFormatter.formatTransferOutput(assetName, assetChangeUnits),
5972
+ });
5973
+ }
5974
+
5975
+ // DePIN: return the owner token (required so the tx contains a transfer of
5976
+ // &NAME! — satisfies the consensus `transfersOwnerToken` check).
5977
+ if (isDepin) {
5978
+ outputs.push(
5979
+ this.ownerTokenManager.createOwnerTokenReturnOutput(ownerTokenName, changeAddress)
5980
+ );
5981
+ }
5982
+
5983
+ // 10. Order outputs (protocol requirement)
5984
+ const orderedOutputs = this.outputOrderer.order(outputs);
5985
+
5986
+ // 11. Create raw transaction
5987
+ const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
5988
+
5989
+ // 12. Format and return result
5990
+ const allUTXOs = [
5991
+ ...assetUTXOs,
5992
+ ...(isDepin ? [ownerTokenUTXO] : []),
5993
+ ...baseCurrencyUTXOs,
5994
+ ];
5995
+ const xnaChangeOut = xnaChange > 0.00000001 ? parseFloat(xnaChange.toFixed(8)) : null;
5996
+
5997
+ return this.formatResult(
5998
+ rawTx,
5999
+ allUTXOs,
6000
+ inputs,
6001
+ orderedOutputs,
6002
+ actualFee,
6003
+ 0, // burnAmount — transfers don't burn
6004
+ {
6005
+ assetName,
6006
+ recipients: recipients.map(r => ({ address: r.address, amount: r.amount })),
6007
+ assetChange: assetChangeRawSats > 0 ? assetChangeUnits : 0,
6008
+ isDepin,
6009
+ ownerTokenUsed: isDepin ? ownerTokenName : null,
6010
+ operationType: 'TRANSFER',
6011
+ localRawBuild: this.buildLocalRawBuild(
6012
+ 'TRANSFER',
6013
+ inputs,
6014
+ null, // no burn
6015
+ changeAddress,
6016
+ xnaChangeOut,
6017
+ {
6018
+ assetName,
6019
+ transfers: recipients.map(r => ({
6020
+ address: r.address,
6021
+ assetName,
6022
+ amount: r.amount,
6023
+ })),
6024
+ assetChange: assetChangeRawSats > 0
6025
+ ? { address: changeAddress, assetName, amount: assetChangeUnits }
6026
+ : null,
6027
+ ownerReturn: isDepin
6028
+ ? { address: changeAddress, assetName: ownerTokenName, amount: 1 }
6029
+ : null,
6030
+ }
6031
+ ),
6032
+ }
6033
+ );
6034
+ }
6035
+ }
6036
+
6037
+ TransferBuilder_1 = TransferBuilder;
6038
+ return TransferBuilder_1;
6039
+ }
6040
+
5773
6041
  /**
5774
6042
  * Issue Unique Builder
5775
6043
  * Builds transactions for creating UNIQUE assets (NFTs)
@@ -7406,6 +7674,7 @@ function requireBuilders () {
7406
7674
  const IssueSubBuilder = requireIssueSubBuilder();
7407
7675
  const IssueDepinBuilder = requireIssueDepinBuilder();
7408
7676
  const ReissueBuilder = requireReissueBuilder();
7677
+ const TransferBuilder = requireTransferBuilder();
7409
7678
 
7410
7679
  // Advanced Builders
7411
7680
  const IssueUniqueBuilder = requireIssueUniqueBuilder();
@@ -7424,6 +7693,7 @@ function requireBuilders () {
7424
7693
  IssueSubBuilder,
7425
7694
  IssueDepinBuilder,
7426
7695
  ReissueBuilder,
7696
+ TransferBuilder,
7427
7697
 
7428
7698
  // Advanced Builders
7429
7699
  IssueUniqueBuilder,
@@ -7476,7 +7746,8 @@ function requireNeuraiAssets () {
7476
7746
  ReissueBuilder,
7477
7747
  ReissueRestrictedBuilder,
7478
7748
  TagAddressBuilder,
7479
- FreezeAddressBuilder
7749
+ FreezeAddressBuilder,
7750
+ TransferBuilder
7480
7751
  } = requireBuilders();
7481
7752
 
7482
7753
  class NeuraiAssets {
@@ -7593,6 +7864,32 @@ function requireNeuraiAssets () {
7593
7864
  return await builder.build();
7594
7865
  }
7595
7866
 
7867
+ // ========================================
7868
+ // TRANSFER OPERATIONS
7869
+ // ========================================
7870
+
7871
+ /**
7872
+ * Transfer an existing asset to one or more recipients.
7873
+ *
7874
+ * Works for any asset type. DePIN (`&`) assets are soulbound: this method
7875
+ * automatically spends and returns the asset's owner token (`&NAME!`) so the
7876
+ * transfer satisfies Neurai consensus (bad-txns-depin-transfer-not-by-owner).
7877
+ * The owner token is returned to the change address (authority stays with the
7878
+ * sender). For non-DePIN assets no owner token is involved.
7879
+ *
7880
+ * @param {object} params - Transfer parameters
7881
+ * @param {string} params.assetName - Asset to transfer (e.g. 'TOKEN', '$SEC', '&DEVICE')
7882
+ * @param {Array<object>} params.recipients - Recipients
7883
+ * @param {string} params.recipients[].address - Destination address
7884
+ * @param {number} params.recipients[].amount - Amount in asset display units (> 0)
7885
+ * @param {string} [params.changeAddress] - Override change/owner-return address
7886
+ * @returns {Promise<object>} Transaction data
7887
+ */
7888
+ async transferAsset(params) {
7889
+ const builder = new TransferBuilder(this.rpc, this._buildParams(params));
7890
+ return await builder.build();
7891
+ }
7892
+
7596
7893
  // ========================================
7597
7894
  // UNIQUE ASSET (NFT) OPERATIONS
7598
7895
  // ========================================