@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 +22 -0
- package/dist/NeuraiAssets.global.js +298 -1
- package/dist/NeuraiAssets.global.js.map +1 -1
- package/dist/browser.js +298 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.cjs +298 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +298 -1
- package/dist/index.js.map +1 -1
- package/index.d.ts +1 -0
- package/package.json +1 -1
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
|
// ========================================
|