@neuraiproject/neurai-assets 1.0.1 → 1.1.0

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) |
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Example: DEPIN Assets with Post-Quantum Addresses
3
+ *
4
+ * This example demonstrates how to use DEPIN assets with PQ addresses
5
+ * generated by neurai-key using the xna-pq / xna-pq-test networks.
6
+ *
7
+ * PQ address examples:
8
+ * - Mainnet: nq1...
9
+ * - Testnet: tnq1...
10
+ */
11
+
12
+ const NeuraiAssets = require('@neuraiproject/neurai-assets');
13
+
14
+ async function createDepinForPQAddress() {
15
+ const rpc = async (method, params) => {
16
+ console.log(`RPC Call: ${method}`, params);
17
+ // Replace with your actual RPC client
18
+ };
19
+
20
+ const assets = new NeuraiAssets(rpc, {
21
+ network: 'xna-pq', // Or 'xna-pq-test'
22
+ addresses: ['nq1yourpqwalletaddress...'], // PQ addresses from neurai-key
23
+ changeAddress: 'nq1yourpqchangeaddress...',
24
+ toAddress: 'nq1recipientpqaddress...'
25
+ });
26
+
27
+ try {
28
+ const result = await assets.createDepinAsset({
29
+ assetName: '&DEVICE/ROUTER001',
30
+ quantity: 1,
31
+ reissuable: false
32
+ });
33
+
34
+ console.log('DEPIN transaction created');
35
+ console.log('Raw transaction:', result.rawTx);
36
+ console.log('Burn:', result.burnAmount, 'XNA');
37
+ console.log('Owner token:', result.ownerTokenName);
38
+ } catch (error) {
39
+ console.error('Error creating DEPIN asset for PQ address:', error.message);
40
+ }
41
+ }
42
+
43
+ async function queryDepinValidityForPQAddress() {
44
+ const rpc = async (method, params) => {
45
+ console.log(`RPC Call: ${method}`, params);
46
+
47
+ if (method === 'checkdepinvalidity') {
48
+ return {
49
+ has_asset: true,
50
+ amount: 1,
51
+ valid: 1,
52
+ blocked: false
53
+ };
54
+ }
55
+
56
+ if (method === 'listdepinholders') {
57
+ return [
58
+ { address: 'nq1firstholder...', amount: 1, valid: 1 },
59
+ { address: 'nq1secondholder...', amount: 1, valid: 0 }
60
+ ];
61
+ }
62
+ };
63
+
64
+ const assets = new NeuraiAssets(rpc, {
65
+ network: 'xna-pq',
66
+ addresses: ['nq1yourpqwalletaddress...'],
67
+ changeAddress: 'nq1yourpqchangeaddress...',
68
+ toAddress: 'nq1recipientpqaddress...'
69
+ });
70
+
71
+ try {
72
+ const validity = await assets.checkDepinValidity('&DEVICE/ROUTER001', 'nq1recipientpqaddress...');
73
+ console.log('DEPIN validity for PQ address:', validity);
74
+
75
+ const holders = await assets.listDepinHolders('&DEVICE/ROUTER001');
76
+ console.log('DEPIN holders:', holders);
77
+ } catch (error) {
78
+ console.error('Error querying DEPIN PQ data:', error.message);
79
+ }
80
+ }
81
+
82
+ createDepinForPQAddress();
83
+ queryDepinValidityForPQAddress();
@@ -163,6 +163,23 @@ Learn complete integration with `@neuraiproject/neurai-jswallet`.
163
163
 
164
164
  ---
165
165
 
166
+ ### 10. DEPIN with Post-Quantum Addresses
167
+ **File:** [10-depin-post-quantum.js](10-depin-post-quantum.js)
168
+
169
+ Learn how to use DEPIN assets with PQ addresses generated by `neurai-key`.
170
+
171
+ - PQ mainnet network: `xna-pq`
172
+ - PQ testnet network: `xna-pq-test`
173
+ - PQ address formats: `nq1...` and `tnq1...`
174
+
175
+ **Topics covered:**
176
+ - Initializing `NeuraiAssets` with PQ networks
177
+ - Creating DEPIN assets sent to PQ addresses
178
+ - Querying DEPIN validity for PQ holders
179
+ - Listing DEPIN holders with PQ addresses
180
+
181
+ ---
182
+
166
183
  ## Running the Examples
167
184
 
168
185
  ### Prerequisites
@@ -184,9 +201,9 @@ node 01-create-root-asset.js
184
201
 
185
202
  1. **Mock RPC**: All examples use mock RPC functions. Replace with your actual RPC client in production.
186
203
 
187
- 2. **Network**: Examples use mainnet (`'xna'`). Change to `'xna-test'` for testnet.
204
+ 2. **Network**: Examples use mainnet (`'xna'`). Change to `'xna-test'` for testnet. PQ networks are `xna-pq` and `xna-pq-test`.
188
205
 
189
- 3. **Addresses**: Replace placeholder addresses with your actual wallet addresses.
206
+ 3. **Addresses**: Replace placeholder addresses with your actual wallet addresses. PQ examples use `nq1...` / `tnq1...` addresses.
190
207
 
191
208
  4. **Testing**: Test on testnet first before using mainnet.
192
209
 
@@ -232,6 +249,7 @@ const txid = await wallet.broadcastTransaction(signedTx);
232
249
  | QUALIFIER | `#NAME` | 2000 XNA | `#KYC_VERIFIED` |
233
250
  | SUB_QUALIFIER | `#ROOT/#SUB` | 200 XNA | `#KYC/#LEVEL2` |
234
251
  | RESTRICTED | `$NAME` | 3000 XNA | `$SECURITY` |
252
+ | DEPIN | `&NAME` or `&ROOT/SUB` | 10 XNA | `&DEVICE/ROUTER001` |
235
253
  | OWNER | `NAME!` | N/A | `MYTOKEN!` |
236
254
 
237
255
  ## Common Patterns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuraiproject/neurai-assets",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Non-custodial Neurai asset management library for JavaScript",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -29,13 +29,16 @@
29
29
  "publishConfig": {
30
30
  "access": "public"
31
31
  },
32
- "dependencies": {},
33
32
  "devDependencies": {
34
33
  "chai": "^4.3.10",
35
- "mocha": "^10.2.0"
34
+ "mocha": "^11.7.5"
35
+ },
36
+ "overrides": {
37
+ "diff": "8.0.4",
38
+ "serialize-javascript": "7.0.5"
36
39
  },
37
40
  "peerDependencies": {
38
- "@neuraiproject/neurai-rpc": "^0.4.6"
41
+ "@neuraiproject/neurai-rpc": "^0.4.7"
39
42
  },
40
43
  "engines": {
41
44
  "node": ">=14.0.0"
@@ -25,6 +25,7 @@ const { AssetQueries } = require('./queries');
25
25
  const {
26
26
  IssueRootBuilder,
27
27
  IssueSubBuilder,
28
+ IssueDepinBuilder,
28
29
  IssueUniqueBuilder,
29
30
  IssueQualifierBuilder,
30
31
  IssueRestrictedBuilder,
@@ -119,6 +120,21 @@ class NeuraiAssets {
119
120
  return await builder.build();
120
121
  }
121
122
 
123
+ /**
124
+ * Create a DEPIN asset
125
+ * @param {object} params - DEPIN creation parameters
126
+ * @param {string} params.assetName - Asset name (&NAME or &NAME/SUB)
127
+ * @param {number} params.quantity - Total supply
128
+ * @param {boolean} [params.reissuable=true] - Can mint more later
129
+ * @param {boolean} [params.hasIpfs=false] - Has IPFS metadata
130
+ * @param {string} [params.ipfsHash] - IPFS hash
131
+ * @returns {Promise<object>} Transaction data
132
+ */
133
+ async createDepinAsset(params) {
134
+ const builder = new IssueDepinBuilder(this.rpc, this._buildParams(params));
135
+ return await builder.build();
136
+ }
137
+
122
138
  /**
123
139
  * Reissue (mint more) of a ROOT or SUB asset
124
140
  * @param {object} params - Reissue parameters
@@ -167,7 +183,8 @@ class NeuraiAssets {
167
183
  * @returns {Promise<object>} Transaction data
168
184
  */
169
185
  async createQualifier(params) {
170
- const builder = new IssueQualifierBuilder(this.rpc, this._buildParams(params));
186
+ const normalized = { ...params, assetName: params.assetName || params.qualifierName };
187
+ const builder = new IssueQualifierBuilder(this.rpc, this._buildParams(normalized));
171
188
  return await builder.build();
172
189
  }
173
190
 
@@ -438,6 +455,25 @@ class NeuraiAssets {
438
455
  return await this.queries.cancelSnapshotRequest(assetName, blockHeight);
439
456
  }
440
457
 
458
+ /**
459
+ * List DEPIN holders with validity status
460
+ * @param {string} assetName - DEPIN asset name
461
+ * @returns {Promise<Array>} Holder entries
462
+ */
463
+ async listDepinHolders(assetName) {
464
+ return await this.queries.listDepinHolders(assetName);
465
+ }
466
+
467
+ /**
468
+ * Check DEPIN validity for an address
469
+ * @param {string} assetName - DEPIN asset name
470
+ * @param {string} address - Address to query
471
+ * @returns {Promise<object>} Validity details
472
+ */
473
+ async checkDepinValidity(assetName, address) {
474
+ return await this.queries.checkDepinValidity(assetName, address);
475
+ }
476
+
441
477
  /**
442
478
  * Check if asset exists
443
479
  * @param {string} assetName - Asset name
@@ -450,7 +486,7 @@ class NeuraiAssets {
450
486
  /**
451
487
  * Get asset type from name
452
488
  * @param {string} assetName - Asset name
453
- * @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'OWNER')
489
+ * @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'DEPIN', 'OWNER')
454
490
  */
455
491
  getAssetType(assetName) {
456
492
  return this.queries.getAssetType(assetName);
@@ -225,6 +225,9 @@ class BaseAssetTransactionBuilder {
225
225
  case 'RESTRICTED':
226
226
  AssetNameValidator.validateRestricted(assetName);
227
227
  break;
228
+ case 'DEPIN':
229
+ AssetNameValidator.validateDepin(assetName);
230
+ break;
228
231
  default:
229
232
  throw new Error(`Unknown asset type: ${type}`);
230
233
  }
@@ -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
 
@@ -162,13 +155,6 @@ class FreezeAddressBuilder extends BaseAssetTransactionBuilder {
162
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
- outputs.push(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.push({ [targetAddresses[0]]: operationOutput });
169
+ outputs.push({ [changeAddress]: operationOutput });
184
170
  break;
185
171
 
186
172
  case 'UNFREEZE_ADDRESSES':
@@ -189,7 +175,7 @@ class FreezeAddressBuilder extends BaseAssetTransactionBuilder {
189
175
  asset_name: assetName,
190
176
  addresses: targetAddresses
191
177
  });
192
- outputs.push({ [targetAddresses[0]]: operationOutput });
178
+ outputs.push({ [changeAddress]: operationOutput });
193
179
  break;
194
180
 
195
181
  case 'FREEZE_ASSET':
@@ -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(
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Issue DePIN Builder
3
+ * Builds transactions for creating DEPIN assets.
4
+ *
5
+ * DEPIN assets:
6
+ * - Soulbound assets
7
+ * - Format: &NAME or &NAME/SUB
8
+ * - Cost: 10 XNA (same burn as UNIQUE assets)
9
+ * - Units: Always 0
10
+ * - Owner token is auto-created by the node
11
+ */
12
+
13
+ const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
14
+ const { OutputFormatter } = require('../utils');
15
+ const { AssetExistsError } = require('../errors');
16
+ const { IpfsValidator } = require('../validators');
17
+
18
+ class IssueDepinBuilder extends BaseAssetTransactionBuilder {
19
+ /**
20
+ * Validate issue DEPIN parameters
21
+ * @param {object} params - Issue parameters
22
+ * @throws {Error} If validation fails
23
+ */
24
+ validateParams(params) {
25
+ if (!params.assetName) {
26
+ throw new Error('assetName is required');
27
+ }
28
+
29
+ if (params.quantity === undefined || params.quantity === null) {
30
+ throw new Error('quantity is required');
31
+ }
32
+
33
+ this.validateAssetName(params.assetName, 'DEPIN');
34
+ this.validateAmount(params.quantity, 0);
35
+
36
+ if (params.units !== undefined && params.units !== 0) {
37
+ throw new Error('DEPIN assets must use units=0');
38
+ }
39
+
40
+ if (params.hasIpfs && params.ipfsHash) {
41
+ IpfsValidator.validate(params.ipfsHash);
42
+ }
43
+
44
+ if (params.reissuable !== undefined && typeof params.reissuable !== 'boolean') {
45
+ throw new Error('reissuable must be a boolean');
46
+ }
47
+
48
+ return true;
49
+ }
50
+
51
+ /**
52
+ * Build DEPIN asset issuance transaction
53
+ * @returns {Promise<object>} Transaction result
54
+ */
55
+ async build() {
56
+ await this.validateParams(this.params);
57
+
58
+ const {
59
+ assetName,
60
+ quantity,
61
+ reissuable = true,
62
+ hasIpfs = false,
63
+ ipfsHash = ''
64
+ } = this.params;
65
+
66
+ const exists = await this.assetExists(assetName);
67
+ if (exists) {
68
+ throw new AssetExistsError(
69
+ `Asset ${assetName} already exists on the blockchain`,
70
+ assetName
71
+ );
72
+ }
73
+
74
+ const burnInfo = this.burnManager.getIssueDepinBurn();
75
+ const toAddress = await this.getToAddress();
76
+ const changeAddress = await this.getChangeAddress();
77
+
78
+ const estimatedFee = await this.estimateFee(1, 3);
79
+ const totalXNANeeded = burnInfo.amount + estimatedFee;
80
+
81
+ const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
82
+ const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
83
+ const totalXNAInput = utxoSelection.totalXNA;
84
+
85
+ const actualFee = await this.estimateFee(baseCurrencyUTXOs.length, 3);
86
+ const totalRequired = burnInfo.amount + actualFee;
87
+
88
+ if (totalXNAInput < totalRequired) {
89
+ const additionalNeeded = totalRequired - totalXNAInput + 0.001;
90
+ const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
91
+ baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
92
+ }
93
+
94
+ const finalTotalInput = baseCurrencyUTXOs.reduce(
95
+ (sum, utxo) => sum + utxo.satoshis / 100000000,
96
+ 0
97
+ );
98
+ const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
99
+
100
+ const inputs = baseCurrencyUTXOs.map(utxo => ({
101
+ txid: utxo.txid,
102
+ vout: utxo.outputIndex,
103
+ address: utxo.address,
104
+ satoshis: utxo.satoshis
105
+ }));
106
+
107
+ const outputs = [];
108
+ outputs.push({ [burnInfo.address]: burnInfo.amount });
109
+
110
+ if (xnaChange > 0.00000001) {
111
+ outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
112
+ }
113
+
114
+ const issueOutput = OutputFormatter.formatIssueOutput({
115
+ asset_name: assetName,
116
+ asset_quantity: this.toSatoshis(quantity, 0),
117
+ units: 0,
118
+ reissuable,
119
+ has_ipfs: hasIpfs,
120
+ ipfs_hash: ipfsHash
121
+ });
122
+
123
+ outputs.push({ [toAddress]: issueOutput });
124
+
125
+ const orderedOutputs = this.outputOrderer.order(outputs);
126
+ const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
127
+
128
+ return this.formatResult(
129
+ rawTx,
130
+ baseCurrencyUTXOs,
131
+ inputs,
132
+ orderedOutputs,
133
+ actualFee,
134
+ burnInfo.amount,
135
+ {
136
+ assetName,
137
+ ownerTokenName: `${assetName}!`,
138
+ operationType: 'ISSUE_DEPIN'
139
+ }
140
+ );
141
+ }
142
+ }
143
+
144
+ module.exports = IssueDepinBuilder;
@@ -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,16 +172,16 @@ 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
187
  const outputs = [];
@@ -194,21 +194,14 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
194
194
  outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
195
195
  }
196
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
- outputs.push(ownerTokenReturn);
204
- }
205
-
206
197
  // Last: Issue qualifier operation
207
198
  const issueQualifierOutput = OutputFormatter.formatIssueQualifierOutput({
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
207
  outputs.push({ [toAddress]: issueQualifierOutput });
@@ -216,18 +209,11 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
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
  );