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