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