@neuraiproject/neurai-assets 1.0.0 → 1.0.2

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
@@ -96,23 +96,23 @@ const result = await assets.reissueAsset({
96
96
  ### Create UNIQUE Assets (NFTs)
97
97
 
98
98
  ```javascript
99
+ // Without IPFS metadata
99
100
  const result = await assets.createUniqueAssets({
100
- rootAssetName: 'MYTOKEN',
101
- assetTags: [
102
- {
103
- tag: 'NFT001',
104
- hasIpfs: true,
105
- ipfsHash: 'QmNFT1...'
106
- },
107
- {
108
- tag: 'NFT002',
109
- hasIpfs: true,
110
- ipfsHash: 'QmNFT2...'
111
- }
112
- ]
101
+ rootName: 'MYTOKEN',
102
+ assetTags: ['NFT001', 'NFT002', 'NFT003']
103
+ });
104
+
105
+ // With IPFS metadata (ipfsHashes must be same length as assetTags)
106
+ const result = await assets.createUniqueAssets({
107
+ rootName: 'MYTOKEN',
108
+ assetTags: ['NFT001', 'NFT002'],
109
+ ipfsHashes: ['QmNFT1...', 'QmNFT2...']
113
110
  });
114
111
  ```
115
112
 
113
+ > **Note**: UNIQUE asset properties (`units`, `reissuable`) are always `0` and are
114
+ > set automatically by the node — they cannot be configured per asset.
115
+
116
116
  ### Create QUALIFIER (KYC Tags)
117
117
 
118
118
  ```javascript
@@ -363,6 +363,11 @@ When you create an asset, an **owner token** is automatically generated (e.g., `
363
363
 
364
364
  The library automatically validates that the owner token is returned in each operation to prevent accidental loss.
365
365
 
366
+ > **UNIQUE assets exception**: When issuing UNIQUE assets (`ROOT#TAG`), the Neurai node
367
+ > returns the owner token automatically as part of processing the `issue_unique` operation.
368
+ > The library does not add a manual return output for this case — doing so would duplicate
369
+ > the owner token in the outputs and cause the transaction to fail with "Assets would be burnt".
370
+
366
371
  ## Operation Costs
367
372
 
368
373
  | Operation | Cost (XNA burned) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuraiproject/neurai-assets",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Non-custodial Neurai asset management library for JavaScript",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -167,7 +167,8 @@ class NeuraiAssets {
167
167
  * @returns {Promise<object>} Transaction data
168
168
  */
169
169
  async createQualifier(params) {
170
- const builder = new IssueQualifierBuilder(this.rpc, this._buildParams(params));
170
+ const normalized = { ...params, assetName: params.assetName || params.qualifierName };
171
+ const builder = new IssueQualifierBuilder(this.rpc, this._buildParams(normalized));
171
172
  return await builder.build();
172
173
  }
173
174
 
@@ -24,27 +24,45 @@ class BaseAssetTransactionBuilder {
24
24
  * @param {string[]|Function} addresses - Wallet addresses or function that returns addresses
25
25
  * @param {object} params - Transaction parameters
26
26
  */
27
- constructor(rpc, network, addresses, params) {
27
+ constructor(rpc, networkOrParams, addresses, params) {
28
28
  if (!rpc || typeof rpc !== 'function') {
29
29
  throw new Error('RPC function is required');
30
30
  }
31
31
 
32
+ // Support both (rpc, network, addresses, params) and (rpc, params) calling forms.
33
+ // NeuraiAssets passes a single merged params object as the second argument.
34
+ let network, actualAddresses, actualParams;
35
+ if (
36
+ typeof networkOrParams === 'object' &&
37
+ networkOrParams !== null &&
38
+ !Array.isArray(networkOrParams) &&
39
+ addresses === undefined
40
+ ) {
41
+ network = networkOrParams.network;
42
+ actualAddresses = networkOrParams.addresses;
43
+ actualParams = networkOrParams;
44
+ } else {
45
+ network = networkOrParams;
46
+ actualAddresses = addresses;
47
+ actualParams = params;
48
+ }
49
+
32
50
  if (!network) {
33
51
  throw new Error('Network is required');
34
52
  }
35
53
 
36
54
  // Addresses can be an array or a function that returns addresses
37
- if (typeof addresses === 'function') {
38
- this.getAddresses = addresses;
39
- } else if (Array.isArray(addresses)) {
40
- this.getAddresses = () => addresses;
55
+ if (typeof actualAddresses === 'function') {
56
+ this.getAddresses = actualAddresses;
57
+ } else if (Array.isArray(actualAddresses)) {
58
+ this.getAddresses = () => actualAddresses;
41
59
  } else {
42
60
  throw new Error('Addresses must be an array or a function');
43
61
  }
44
62
 
45
63
  this.rpc = rpc;
46
64
  this.network = network;
47
- this.params = params || {};
65
+ this.params = actualParams || {};
48
66
 
49
67
  // Initialize managers
50
68
  this.burnManager = new BurnManager(network);
@@ -47,14 +47,7 @@ class FreezeAddressBuilder extends BaseAssetTransactionBuilder {
47
47
  );
48
48
  }
49
49
 
50
- // Basic address validation
51
- const validPrefixes = this.network === 'xna' ? ['N'] : ['m', 'n'];
52
- if (!validPrefixes.some(prefix => address.startsWith(prefix))) {
53
- throw new InvalidAddressError(
54
- `addresses[${index}] has invalid prefix for network ${this.network}`,
55
- address
56
- );
57
- }
50
+ // Address prefix validation is left to the node (varies by network)
58
51
  });
59
52
  }
60
53
 
@@ -155,20 +148,13 @@ class FreezeAddressBuilder extends BaseAssetTransactionBuilder {
155
148
  });
156
149
 
157
150
  // 12. Build outputs (ORDER CRITICAL!)
158
- const outputs = {};
151
+ const outputs = [];
159
152
 
160
153
  // First: XNA change (if any)
161
154
  if (xnaChange > 0.00000001) {
162
- outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
155
+ outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
163
156
  }
164
157
 
165
- // Second: Owner token return (CRITICAL - must return or lost forever!)
166
- const ownerTokenReturn = this.ownerTokenManager.createOwnerTokenReturnOutput(
167
- ownerTokenName,
168
- changeAddress
169
- );
170
- Object.assign(outputs, ownerTokenReturn);
171
-
172
158
  // Last: Freeze/Unfreeze operation
173
159
  let operationOutput;
174
160
  let targetAddresses = [];
@@ -180,7 +166,7 @@ class FreezeAddressBuilder extends BaseAssetTransactionBuilder {
180
166
  asset_name: assetName,
181
167
  addresses: targetAddresses
182
168
  });
183
- outputs[targetAddresses[0]] = operationOutput;
169
+ outputs.push({ [changeAddress]: operationOutput });
184
170
  break;
185
171
 
186
172
  case 'UNFREEZE_ADDRESSES':
@@ -189,17 +175,17 @@ class FreezeAddressBuilder extends BaseAssetTransactionBuilder {
189
175
  asset_name: assetName,
190
176
  addresses: targetAddresses
191
177
  });
192
- outputs[targetAddresses[0]] = operationOutput;
178
+ outputs.push({ [changeAddress]: operationOutput });
193
179
  break;
194
180
 
195
181
  case 'FREEZE_ASSET':
196
182
  operationOutput = OutputFormatter.formatFreezeAssetOutput(assetName);
197
- outputs[changeAddress] = operationOutput;
183
+ outputs.push({ [changeAddress]: operationOutput });
198
184
  break;
199
185
 
200
186
  case 'UNFREEZE_ASSET':
201
187
  operationOutput = OutputFormatter.formatUnfreezeAssetOutput(assetName);
202
- outputs[changeAddress] = operationOutput;
188
+ outputs.push({ [changeAddress]: operationOutput });
203
189
  break;
204
190
 
205
191
  default:
@@ -209,13 +195,10 @@ class FreezeAddressBuilder extends BaseAssetTransactionBuilder {
209
195
  // 13. Order outputs (protocol requirement)
210
196
  const orderedOutputs = this.outputOrderer.order(outputs);
211
197
 
212
- // 14. Validate owner token is returned (safety check)
213
- this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
214
-
215
- // 15. Create raw transaction
198
+ // 14. Create raw transaction
216
199
  const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
217
200
 
218
- // 16. Format and return result
201
+ // 15. Format and return result
219
202
  const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
220
203
 
221
204
  return this.formatResult(
@@ -9,7 +9,8 @@
9
9
  * - Quantity: 1-10 units only
10
10
  * - Units: Always 0 (non-divisible)
11
11
  * - Used to tag addresses for restricted asset compliance
12
- * - Creates owner token (#QUALIFIER!)
12
+ * - Root qualifiers do not create owner tokens
13
+ * - Sub-qualifiers consume and return the parent qualifier asset itself
13
14
  */
14
15
 
15
16
  const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
@@ -75,13 +76,14 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
75
76
  const isSub = this.isSubQualifier(assetName);
76
77
  const parsed = AssetNameParser.parse(assetName);
77
78
 
78
- // 3. If sub-qualifier, check parent exists and get owner token
79
- let ownerTokenUTXO = null;
80
- let ownerTokenName = null;
79
+ // 3. If sub-qualifier, check parent exists and get parent qualifier input
80
+ let parentQualifierUTXOs = [];
81
+ let parentQualifierQuantity = null;
82
+ let parentQualifierName = null;
81
83
  const addresses = await this._getAddresses();
82
84
 
83
85
  if (isSub) {
84
- const parentQualifierName = parsed.parent;
86
+ parentQualifierName = parsed.parent;
85
87
 
86
88
  // Check parent qualifier exists
87
89
  const parentExists = await this.assetExists(parentQualifierName);
@@ -92,18 +94,16 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
92
94
  );
93
95
  }
94
96
 
95
- // Find parent's owner token
96
- ownerTokenName = AssetNameParser.getOwnerTokenName(parentQualifierName);
97
+ // Find parent qualifier balance to spend and return as change
97
98
  try {
98
- ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
99
- ownerTokenName,
100
- addresses
101
- );
99
+ const selection = await this.utxoSelector.selectAssetUTXOs(addresses, parentQualifierName, 1);
100
+ parentQualifierUTXOs = selection.utxos;
101
+ parentQualifierQuantity = selection.totalAmount;
102
102
  } catch (error) {
103
- if (error instanceof OwnerTokenNotFoundError) {
103
+ if (error.name === 'InsufficientFundsError') {
104
104
  throw new OwnerTokenNotFoundError(
105
- `You must own the parent qualifier's owner token (${ownerTokenName}) to create a sub-qualifier.`,
106
- ownerTokenName
105
+ `You must own the parent qualifier asset (${parentQualifierName}) to create a sub-qualifier.`,
106
+ parentQualifierName
107
107
  );
108
108
  }
109
109
  throw error;
@@ -129,7 +129,7 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
129
129
  const changeAddress = await this.getChangeAddress();
130
130
 
131
131
  // 7. Estimate fee
132
- const outputCount = isSub ? 4 : 3; // Sub has owner token return
132
+ const outputCount = 3;
133
133
  const estimatedFee = await this.estimateFee(2, outputCount);
134
134
 
135
135
  // 8. Calculate total XNA needed
@@ -141,7 +141,7 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
141
141
  const totalXNAInput = utxoSelection.totalXNA;
142
142
 
143
143
  // 10. Recalculate fee with actual input count
144
- const actualInputCount = baseCurrencyUTXOs.length + (ownerTokenUTXO ? 1 : 0);
144
+ const actualInputCount = baseCurrencyUTXOs.length + parentQualifierUTXOs.length;
145
145
  const actualFee = await this.estimateFee(actualInputCount, outputCount);
146
146
 
147
147
  // 11. Verify we have enough XNA
@@ -172,35 +172,26 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
172
172
  });
173
173
  });
174
174
 
175
- // Add owner token input if sub-qualifier
176
- if (ownerTokenUTXO) {
175
+ // Add parent qualifier inputs if sub-qualifier
176
+ parentQualifierUTXOs.forEach(parentUTXO => {
177
177
  inputs.push({
178
- txid: ownerTokenUTXO.txid,
179
- vout: ownerTokenUTXO.outputIndex,
180
- address: ownerTokenUTXO.address,
181
- assetName: ownerTokenUTXO.assetName,
182
- satoshis: ownerTokenUTXO.satoshis
178
+ txid: parentUTXO.txid,
179
+ vout: parentUTXO.outputIndex,
180
+ address: parentUTXO.address,
181
+ assetName: parentUTXO.assetName,
182
+ satoshis: parentUTXO.satoshis
183
183
  });
184
- }
184
+ });
185
185
 
186
186
  // 14. Build outputs (ORDER CRITICAL!)
187
- const outputs = {};
187
+ const outputs = [];
188
188
 
189
189
  // First: Burn output
190
- outputs[burnInfo.address] = burnInfo.amount;
190
+ outputs.push({ [burnInfo.address]: burnInfo.amount });
191
191
 
192
192
  // Second: XNA change (if any)
193
193
  if (xnaChange > 0.00000001) {
194
- outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
195
- }
196
-
197
- // Third: Owner token return (if sub-qualifier)
198
- if (ownerTokenUTXO && ownerTokenName) {
199
- const ownerTokenReturn = this.ownerTokenManager.createOwnerTokenReturnOutput(
200
- ownerTokenName,
201
- changeAddress
202
- );
203
- Object.assign(outputs, ownerTokenReturn);
194
+ outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
204
195
  }
205
196
 
206
197
  // Last: Issue qualifier operation
@@ -208,26 +199,21 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
208
199
  asset_name: assetName,
209
200
  asset_quantity: quantity,
210
201
  has_ipfs: hasIpfs,
211
- ipfs_hash: ipfsHash
202
+ ipfs_hash: ipfsHash,
203
+ root_change_address: isSub ? changeAddress : undefined,
204
+ change_quantity: isSub ? parentQualifierQuantity : undefined
212
205
  });
213
206
 
214
- outputs[toAddress] = issueQualifierOutput;
207
+ outputs.push({ [toAddress]: issueQualifierOutput });
215
208
 
216
209
  // 15. Order outputs (protocol requirement)
217
210
  const orderedOutputs = this.outputOrderer.order(outputs);
218
211
 
219
- // 16. Validate owner token is returned if sub-qualifier
220
- if (ownerTokenUTXO) {
221
- this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
222
- }
223
-
224
- // 17. Create raw transaction
212
+ // 16. Create raw transaction
225
213
  const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
226
214
 
227
- // 18. Format and return result
228
- const allUTXOs = ownerTokenUTXO
229
- ? [...baseCurrencyUTXOs, ownerTokenUTXO]
230
- : baseCurrencyUTXOs;
215
+ // 17. Format and return result
216
+ const allUTXOs = [...baseCurrencyUTXOs, ...parentQualifierUTXOs];
231
217
 
232
218
  return this.formatResult(
233
219
  rawTx,
@@ -240,8 +226,7 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
240
226
  assetName,
241
227
  qualifierType: isSub ? 'SUB_QUALIFIER' : 'QUALIFIER',
242
228
  parentQualifier: isSub ? parsed.parent : null,
243
- ownerTokenName: assetName + '!',
244
- parentOwnerTokenUsed: ownerTokenName,
229
+ parentQualifierUsed: parentQualifierName,
245
230
  operationType: isSub ? 'ISSUE_SUB_QUALIFIER' : 'ISSUE_QUALIFIER'
246
231
  }
247
232
  );
@@ -9,12 +9,12 @@
9
9
  * - Requires verifier string (boolean logic with qualifiers)
10
10
  * - Only addresses meeting verifier requirements can receive/hold
11
11
  * - Can freeze individual addresses or entire asset
12
- * - Creates owner token ($ASSET!)
12
+ * - Creates owner token (ASSET!)
13
13
  */
14
14
 
15
15
  const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
16
- const { OutputFormatter } = require('../utils');
17
- const { AssetExistsError } = require('../errors');
16
+ const { OutputFormatter, AssetNameParser } = require('../utils');
17
+ const { AssetExistsError, OwnerTokenNotFoundError } = require('../errors');
18
18
  const { IpfsValidator, VerifierValidator } = require('../validators');
19
19
 
20
20
  class IssueRestrictedBuilder extends BaseAssetTransactionBuilder {
@@ -98,21 +98,39 @@ class IssueRestrictedBuilder extends BaseAssetTransactionBuilder {
98
98
  const toAddress = await this.getToAddress();
99
99
  const changeAddress = await this.getChangeAddress();
100
100
 
101
- // 6. Estimate fee
102
- const estimatedFee = await this.estimateFee(1, 3);
101
+ // 6. Find owner token UTXO (CRITICAL: node requires it as input)
102
+ const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
103
+ let ownerTokenUTXO;
104
+ try {
105
+ ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
106
+ ownerTokenName,
107
+ addresses
108
+ );
109
+ } catch (error) {
110
+ if (error instanceof OwnerTokenNotFoundError) {
111
+ throw new OwnerTokenNotFoundError(
112
+ `You must own the owner token (${ownerTokenName}) to issue the restricted asset ${assetName}.`,
113
+ ownerTokenName
114
+ );
115
+ }
116
+ throw error;
117
+ }
103
118
 
104
- // 7. Calculate total XNA needed
119
+ // 7. Estimate fee (+1 for owner token input)
120
+ const estimatedFee = await this.estimateFee(2, 4);
121
+
122
+ // 8. Calculate total XNA needed
105
123
  const totalXNANeeded = burnInfo.amount + estimatedFee;
106
124
 
107
- // 8. Select XNA UTXOs
125
+ // 9. Select XNA UTXOs
108
126
  const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
109
127
  const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
110
128
  const totalXNAInput = utxoSelection.totalXNA;
111
129
 
112
- // 9. Recalculate fee with actual input count
113
- const actualFee = await this.estimateFee(baseCurrencyUTXOs.length, 3);
130
+ // 10. Recalculate fee with actual input count (+1 for owner token)
131
+ const actualFee = await this.estimateFee(baseCurrencyUTXOs.length + 1, 4);
114
132
 
115
- // 10. Verify we have enough XNA
133
+ // 11. Verify we have enough XNA
116
134
  const totalRequired = burnInfo.amount + actualFee;
117
135
  if (totalXNAInput < totalRequired) {
118
136
  const additionalNeeded = totalRequired - totalXNAInput + 0.001;
@@ -120,30 +138,43 @@ class IssueRestrictedBuilder extends BaseAssetTransactionBuilder {
120
138
  baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
121
139
  }
122
140
 
123
- // 11. Calculate XNA change
141
+ // 12. Calculate XNA change
124
142
  const finalTotalInput = baseCurrencyUTXOs.reduce(
125
143
  (sum, utxo) => sum + utxo.satoshis / 100000000,
126
144
  0
127
145
  );
128
146
  const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
129
147
 
130
- // 12. Build inputs
131
- const inputs = baseCurrencyUTXOs.map(utxo => ({
132
- txid: utxo.txid,
133
- vout: utxo.outputIndex,
134
- address: utxo.address,
135
- satoshis: utxo.satoshis
136
- }));
148
+ // 13. Build inputs (XNA + owner token)
149
+ const inputs = [];
137
150
 
138
- // 13. Build outputs (ORDER CRITICAL!)
139
- const outputs = {};
151
+ baseCurrencyUTXOs.forEach(utxo => {
152
+ inputs.push({
153
+ txid: utxo.txid,
154
+ vout: utxo.outputIndex,
155
+ address: utxo.address,
156
+ satoshis: utxo.satoshis
157
+ });
158
+ });
159
+
160
+ // Add owner token input (node requires it to issue restricted asset)
161
+ inputs.push({
162
+ txid: ownerTokenUTXO.txid,
163
+ vout: ownerTokenUTXO.outputIndex,
164
+ address: ownerTokenUTXO.address,
165
+ assetName: ownerTokenUTXO.assetName,
166
+ satoshis: ownerTokenUTXO.satoshis
167
+ });
168
+
169
+ // 14. Build outputs (ORDER CRITICAL!)
170
+ const outputs = [];
140
171
 
141
172
  // First: Burn output
142
- outputs[burnInfo.address] = burnInfo.amount;
173
+ outputs.push({ [burnInfo.address]: burnInfo.amount });
143
174
 
144
175
  // Second: XNA change (if any)
145
176
  if (xnaChange > 0.00000001) {
146
- outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
177
+ outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
147
178
  }
148
179
 
149
180
  // Last: Issue restricted operation
@@ -154,28 +185,31 @@ class IssueRestrictedBuilder extends BaseAssetTransactionBuilder {
154
185
  units: units,
155
186
  reissuable: reissuable,
156
187
  has_ipfs: hasIpfs,
157
- ipfs_hash: ipfsHash
188
+ ipfs_hash: ipfsHash,
189
+ owner_change_address: changeAddress
158
190
  });
159
191
 
160
- outputs[toAddress] = issueRestrictedOutput;
192
+ outputs.push({ [toAddress]: issueRestrictedOutput });
161
193
 
162
- // 14. Order outputs (protocol requirement)
194
+ // 15. Order outputs (protocol requirement)
163
195
  const orderedOutputs = this.outputOrderer.order(outputs);
164
196
 
165
- // 15. Create raw transaction
197
+ // 16. Create raw transaction
166
198
  const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
167
199
 
168
- // 16. Format and return result
200
+ // 17. Format and return result
201
+ const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
202
+
169
203
  return this.formatResult(
170
204
  rawTx,
171
- baseCurrencyUTXOs,
205
+ allUTXOs,
172
206
  inputs,
173
207
  orderedOutputs,
174
208
  actualFee,
175
209
  burnInfo.amount,
176
210
  {
177
211
  assetName,
178
- ownerTokenName: assetName + '!',
212
+ ownerTokenName,
179
213
  verifierString,
180
214
  requiredQualifiers,
181
215
  operationType: 'ISSUE_RESTRICTED'
@@ -124,15 +124,15 @@ class IssueRootBuilder extends BaseAssetTransactionBuilder {
124
124
  }));
125
125
 
126
126
  // 12. Build outputs (ORDER MATTERS!)
127
- const outputs = {};
127
+ const outputs = [];
128
128
 
129
129
  // First: Burn output
130
- outputs[burnInfo.address] = burnInfo.amount;
130
+ outputs.push({ [burnInfo.address]: burnInfo.amount });
131
131
 
132
132
  // Second: XNA change (if any)
133
133
  if (xnaChange > 0.00000001) {
134
134
  // Only add change if meaningful amount
135
- outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
135
+ outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
136
136
  }
137
137
 
138
138
  // Last: Issue operation
@@ -145,7 +145,7 @@ class IssueRootBuilder extends BaseAssetTransactionBuilder {
145
145
  ipfs_hash: ipfsHash
146
146
  });
147
147
 
148
- outputs[toAddress] = issueOutput;
148
+ outputs.push({ [toAddress]: issueOutput });
149
149
 
150
150
  // 13. Order outputs (critical for protocol)
151
151
  const orderedOutputs = this.outputOrderer.order(outputs);
@@ -175,14 +175,14 @@ class IssueSubBuilder extends BaseAssetTransactionBuilder {
175
175
  });
176
176
 
177
177
  // 15. Build outputs (ORDER CRITICAL!)
178
- const outputs = {};
178
+ const outputs = [];
179
179
 
180
180
  // First: Burn output
181
- outputs[burnInfo.address] = burnInfo.amount;
181
+ outputs.push({ [burnInfo.address]: burnInfo.amount });
182
182
 
183
183
  // Second: XNA change (if any)
184
184
  if (xnaChange > 0.00000001) {
185
- outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
185
+ outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
186
186
  }
187
187
 
188
188
  // Third: Owner token return (CRITICAL - must return or lost forever!)
@@ -190,7 +190,7 @@ class IssueSubBuilder extends BaseAssetTransactionBuilder {
190
190
  ownerTokenName,
191
191
  changeAddress // Return owner token to change address
192
192
  );
193
- Object.assign(outputs, ownerTokenReturn);
193
+ outputs.push(ownerTokenReturn);
194
194
 
195
195
  // Last: Issue operation
196
196
  const issueOutput = OutputFormatter.formatIssueOutput({
@@ -202,7 +202,7 @@ class IssueSubBuilder extends BaseAssetTransactionBuilder {
202
202
  ipfs_hash: ipfsHash
203
203
  });
204
204
 
205
- outputs[toAddress] = issueOutput;
205
+ outputs.push({ [toAddress]: issueOutput });
206
206
 
207
207
  // 16. Order outputs (protocol requirement)
208
208
  const orderedOutputs = this.outputOrderer.order(outputs);
@@ -192,45 +192,38 @@ class IssueUniqueBuilder extends BaseAssetTransactionBuilder {
192
192
  });
193
193
 
194
194
  // 14. Build outputs (ORDER CRITICAL!)
195
- const outputs = {};
195
+ const outputs = [];
196
196
 
197
197
  // First: Burn output
198
- outputs[burnInfo.address] = burnInfo.amount;
198
+ outputs.push({ [burnInfo.address]: burnInfo.amount });
199
199
 
200
200
  // Second: XNA change (if any)
201
201
  if (xnaChange > 0.00000001) {
202
- outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
202
+ outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
203
203
  }
204
204
 
205
- // Third: Owner token return (CRITICAL - must return or lost forever!)
206
- const ownerTokenReturn = this.ownerTokenManager.createOwnerTokenReturnOutput(
207
- ownerTokenName,
208
- changeAddress
209
- );
210
- Object.assign(outputs, ownerTokenReturn);
211
-
212
205
  // Last: Issue unique operation
206
+ // NOTE: owner token return is handled automatically by the node when processing
207
+ // issue_unique — adding it manually would cause TOKEN! to appear twice in outputs
213
208
  const issueUniqueOutput = OutputFormatter.formatIssueUniqueOutput({
214
209
  root_name: rootName,
215
210
  asset_tags: assetTags,
216
211
  ipfs_hashes: ipfsHashes.length > 0 ? ipfsHashes : undefined
217
212
  });
218
213
 
219
- outputs[toAddress] = issueUniqueOutput;
214
+ outputs.push({ [toAddress]: issueUniqueOutput });
220
215
 
221
216
  // 15. Order outputs (protocol requirement)
222
217
  const orderedOutputs = this.outputOrderer.order(outputs);
223
218
 
224
- // 16. Validate owner token is returned (safety check)
225
- this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
226
-
227
- // 17. Create raw transaction
219
+ // 16. Create raw transaction
220
+ // NOTE: validateOwnerTokenReturn removed — the node returns TOKEN! automatically
228
221
  const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
229
222
 
230
- // 18. Build list of created NFT names
223
+ // 17. Build list of created NFT names
231
224
  const createdNFTs = assetTags.map(tag => `${rootName}#${tag}`);
232
225
 
233
- // 19. Format and return result
226
+ // 18. Format and return result
234
227
  const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
235
228
 
236
229
  return this.formatResult(