@neuraiproject/neurai-assets 1.0.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.
Files changed (58) hide show
  1. package/README.md +522 -0
  2. package/examples/01-create-root-asset.js +71 -0
  3. package/examples/02-create-sub-asset.js +79 -0
  4. package/examples/03-create-nfts.js +140 -0
  5. package/examples/04-reissue-asset.js +164 -0
  6. package/examples/05-create-qualifier-and-tag.js +209 -0
  7. package/examples/06-create-restricted-asset.js +223 -0
  8. package/examples/07-freeze-and-unfreeze.js +292 -0
  9. package/examples/08-query-assets.js +332 -0
  10. package/examples/09-wallet-integration.js +320 -0
  11. package/examples/README.md +319 -0
  12. package/package.json +43 -0
  13. package/src/NeuraiAssets.js +468 -0
  14. package/src/builders/BaseAssetTransactionBuilder.js +303 -0
  15. package/src/builders/FreezeAddressBuilder.js +271 -0
  16. package/src/builders/IssueQualifierBuilder.js +251 -0
  17. package/src/builders/IssueRestrictedBuilder.js +187 -0
  18. package/src/builders/IssueRootBuilder.js +173 -0
  19. package/src/builders/IssueSubBuilder.js +237 -0
  20. package/src/builders/IssueUniqueBuilder.js +255 -0
  21. package/src/builders/ReissueBuilder.js +246 -0
  22. package/src/builders/ReissueRestrictedBuilder.js +264 -0
  23. package/src/builders/TagAddressBuilder.js +243 -0
  24. package/src/builders/index.js +38 -0
  25. package/src/constants/assetTypes.js +23 -0
  26. package/src/constants/burnAddresses.js +65 -0
  27. package/src/constants/fees.js +61 -0
  28. package/src/constants/index.js +44 -0
  29. package/src/constants/networks.js +112 -0
  30. package/src/errors/AssetErrors.js +135 -0
  31. package/src/errors/ValidationErrors.js +87 -0
  32. package/src/errors/index.js +56 -0
  33. package/src/index.js +68 -0
  34. package/src/managers/BurnManager.js +222 -0
  35. package/src/managers/OutputOrderer.js +289 -0
  36. package/src/managers/OwnerTokenManager.js +265 -0
  37. package/src/managers/UTXOSelector.js +309 -0
  38. package/src/managers/index.js +16 -0
  39. package/src/queries/AssetQueries.js +447 -0
  40. package/src/queries/index.js +10 -0
  41. package/src/utils/amountConverter.js +115 -0
  42. package/src/utils/assetNameParser.js +203 -0
  43. package/src/utils/index.js +16 -0
  44. package/src/utils/networkDetector.js +144 -0
  45. package/src/utils/outputFormatter.js +292 -0
  46. package/src/validators/amountValidator.js +149 -0
  47. package/src/validators/assetNameValidator.js +296 -0
  48. package/src/validators/index.js +16 -0
  49. package/src/validators/ipfsValidator.js +101 -0
  50. package/src/validators/verifierValidator.js +146 -0
  51. package/tests/README.md +126 -0
  52. package/tests/integration/assetLifecycle.test.js +244 -0
  53. package/tests/mocks/rpcMock.js +156 -0
  54. package/tests/unit/NeuraiAssets.test.js +217 -0
  55. package/tests/unit/utils/amountConverter.test.js +171 -0
  56. package/tests/unit/utils/assetNameParser.test.js +203 -0
  57. package/tests/unit/validators/amountValidator.test.js +143 -0
  58. package/tests/unit/validators/assetNameValidator.test.js +228 -0
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Issue Qualifier Builder
3
+ * Builds transactions for creating QUALIFIER assets
4
+ *
5
+ * QUALIFIER assets:
6
+ * - KYC/compliance tags (e.g., #KYC_VERIFIED, #ACCREDITED)
7
+ * - Format: #NAME or #ROOT/SUB
8
+ * - Cost: 2000 XNA (root) or 200 XNA (sub-qualifier)
9
+ * - Quantity: 1-10 units only
10
+ * - Units: Always 0 (non-divisible)
11
+ * - Used to tag addresses for restricted asset compliance
12
+ * - Creates owner token (#QUALIFIER!)
13
+ */
14
+
15
+ const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
16
+ const { OutputFormatter, AssetNameParser } = require('../utils');
17
+ const { AssetExistsError, ParentAssetNotFoundError, OwnerTokenNotFoundError } = require('../errors');
18
+ const { IpfsValidator, AmountValidator } = require('../validators');
19
+
20
+ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
21
+ /**
22
+ * Validate issue QUALIFIER parameters
23
+ * @param {object} params - Issue parameters
24
+ * @throws {Error} If validation fails
25
+ */
26
+ validateParams(params) {
27
+ // Validate required parameters
28
+ if (!params.assetName) {
29
+ throw new Error('assetName is required');
30
+ }
31
+
32
+ if (params.quantity === undefined || params.quantity === null) {
33
+ throw new Error('quantity is required');
34
+ }
35
+
36
+ // Validate asset name (QUALIFIER format: #NAME)
37
+ this.validateAssetName(params.assetName, 'QUALIFIER');
38
+
39
+ // Validate quantity (1-10 only for qualifiers)
40
+ AmountValidator.validateQualifierQuantity(params.quantity);
41
+
42
+ // Validate IPFS hash if provided
43
+ if (params.hasIpfs && params.ipfsHash) {
44
+ IpfsValidator.validate(params.ipfsHash);
45
+ }
46
+
47
+ return true;
48
+ }
49
+
50
+ /**
51
+ * Determine if this is a sub-qualifier
52
+ * @param {string} assetName - Qualifier name
53
+ * @returns {boolean} True if sub-qualifier
54
+ */
55
+ isSubQualifier(assetName) {
56
+ return assetName.includes('/');
57
+ }
58
+
59
+ /**
60
+ * Build QUALIFIER asset issuance transaction
61
+ * @returns {Promise<object>} Transaction result
62
+ */
63
+ async build() {
64
+ // 1. Validate parameters
65
+ await this.validateParams(this.params);
66
+
67
+ const {
68
+ assetName,
69
+ quantity,
70
+ hasIpfs = false,
71
+ ipfsHash = ''
72
+ } = this.params;
73
+
74
+ // 2. Determine if root or sub-qualifier
75
+ const isSub = this.isSubQualifier(assetName);
76
+ const parsed = AssetNameParser.parse(assetName);
77
+
78
+ // 3. If sub-qualifier, check parent exists and get owner token
79
+ let ownerTokenUTXO = null;
80
+ let ownerTokenName = null;
81
+ const addresses = await this._getAddresses();
82
+
83
+ if (isSub) {
84
+ const parentQualifierName = parsed.parent;
85
+
86
+ // Check parent qualifier exists
87
+ const parentExists = await this.assetExists(parentQualifierName);
88
+ if (!parentExists) {
89
+ throw new ParentAssetNotFoundError(
90
+ `Parent qualifier ${parentQualifierName} does not exist. You must create the parent qualifier first.`,
91
+ parentQualifierName
92
+ );
93
+ }
94
+
95
+ // Find parent's owner token
96
+ ownerTokenName = AssetNameParser.getOwnerTokenName(parentQualifierName);
97
+ try {
98
+ ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
99
+ ownerTokenName,
100
+ addresses
101
+ );
102
+ } catch (error) {
103
+ if (error instanceof OwnerTokenNotFoundError) {
104
+ throw new OwnerTokenNotFoundError(
105
+ `You must own the parent qualifier's owner token (${ownerTokenName}) to create a sub-qualifier.`,
106
+ ownerTokenName
107
+ );
108
+ }
109
+ throw error;
110
+ }
111
+ }
112
+
113
+ // 4. Check if qualifier already exists
114
+ const exists = await this.assetExists(assetName);
115
+ if (exists) {
116
+ throw new AssetExistsError(
117
+ `Qualifier ${assetName} already exists on the blockchain`,
118
+ assetName
119
+ );
120
+ }
121
+
122
+ // 5. Get burn information (2000 XNA for root, 200 XNA for sub)
123
+ const burnInfo = isSub
124
+ ? this.burnManager.getIssueSubQualifierBurn()
125
+ : this.burnManager.getIssueQualifierBurn();
126
+
127
+ // 6. Get addresses
128
+ const toAddress = await this.getToAddress();
129
+ const changeAddress = await this.getChangeAddress();
130
+
131
+ // 7. Estimate fee
132
+ const outputCount = isSub ? 4 : 3; // Sub has owner token return
133
+ const estimatedFee = await this.estimateFee(2, outputCount);
134
+
135
+ // 8. Calculate total XNA needed
136
+ const totalXNANeeded = burnInfo.amount + estimatedFee;
137
+
138
+ // 9. Select XNA UTXOs
139
+ const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
140
+ const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
141
+ const totalXNAInput = utxoSelection.totalXNA;
142
+
143
+ // 10. Recalculate fee with actual input count
144
+ const actualInputCount = baseCurrencyUTXOs.length + (ownerTokenUTXO ? 1 : 0);
145
+ const actualFee = await this.estimateFee(actualInputCount, outputCount);
146
+
147
+ // 11. Verify we have enough XNA
148
+ const totalRequired = burnInfo.amount + actualFee;
149
+ if (totalXNAInput < totalRequired) {
150
+ const additionalNeeded = totalRequired - totalXNAInput + 0.001;
151
+ const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
152
+ baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
153
+ }
154
+
155
+ // 12. Calculate XNA change
156
+ const finalTotalInput = baseCurrencyUTXOs.reduce(
157
+ (sum, utxo) => sum + utxo.satoshis / 100000000,
158
+ 0
159
+ );
160
+ const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
161
+
162
+ // 13. Build inputs
163
+ const inputs = [];
164
+
165
+ // Add XNA inputs
166
+ baseCurrencyUTXOs.forEach(utxo => {
167
+ inputs.push({
168
+ txid: utxo.txid,
169
+ vout: utxo.outputIndex,
170
+ address: utxo.address,
171
+ satoshis: utxo.satoshis
172
+ });
173
+ });
174
+
175
+ // Add owner token input if sub-qualifier
176
+ if (ownerTokenUTXO) {
177
+ inputs.push({
178
+ txid: ownerTokenUTXO.txid,
179
+ vout: ownerTokenUTXO.outputIndex,
180
+ address: ownerTokenUTXO.address,
181
+ assetName: ownerTokenUTXO.assetName,
182
+ satoshis: ownerTokenUTXO.satoshis
183
+ });
184
+ }
185
+
186
+ // 14. Build outputs (ORDER CRITICAL!)
187
+ const outputs = {};
188
+
189
+ // First: Burn output
190
+ outputs[burnInfo.address] = burnInfo.amount;
191
+
192
+ // Second: XNA change (if any)
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);
204
+ }
205
+
206
+ // Last: Issue qualifier operation
207
+ const issueQualifierOutput = OutputFormatter.formatIssueQualifierOutput({
208
+ asset_name: assetName,
209
+ asset_quantity: quantity,
210
+ has_ipfs: hasIpfs,
211
+ ipfs_hash: ipfsHash
212
+ });
213
+
214
+ outputs[toAddress] = issueQualifierOutput;
215
+
216
+ // 15. Order outputs (protocol requirement)
217
+ const orderedOutputs = this.outputOrderer.order(outputs);
218
+
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
225
+ const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
226
+
227
+ // 18. Format and return result
228
+ const allUTXOs = ownerTokenUTXO
229
+ ? [...baseCurrencyUTXOs, ownerTokenUTXO]
230
+ : baseCurrencyUTXOs;
231
+
232
+ return this.formatResult(
233
+ rawTx,
234
+ allUTXOs,
235
+ inputs,
236
+ orderedOutputs,
237
+ actualFee,
238
+ burnInfo.amount,
239
+ {
240
+ assetName,
241
+ qualifierType: isSub ? 'SUB_QUALIFIER' : 'QUALIFIER',
242
+ parentQualifier: isSub ? parsed.parent : null,
243
+ ownerTokenName: assetName + '!',
244
+ parentOwnerTokenUsed: ownerTokenName,
245
+ operationType: isSub ? 'ISSUE_SUB_QUALIFIER' : 'ISSUE_QUALIFIER'
246
+ }
247
+ );
248
+ }
249
+ }
250
+
251
+ module.exports = IssueQualifierBuilder;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Issue Restricted Builder
3
+ * Builds transactions for creating RESTRICTED assets (security tokens)
4
+ *
5
+ * RESTRICTED assets:
6
+ * - Security tokens with KYC/compliance controls
7
+ * - Format: $NAME (e.g., $SECURITY, $STOCK)
8
+ * - Cost: 3000 XNA (burned)
9
+ * - Requires verifier string (boolean logic with qualifiers)
10
+ * - Only addresses meeting verifier requirements can receive/hold
11
+ * - Can freeze individual addresses or entire asset
12
+ * - Creates owner token ($ASSET!)
13
+ */
14
+
15
+ const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
16
+ const { OutputFormatter } = require('../utils');
17
+ const { AssetExistsError } = require('../errors');
18
+ const { IpfsValidator, VerifierValidator } = require('../validators');
19
+
20
+ class IssueRestrictedBuilder extends BaseAssetTransactionBuilder {
21
+ /**
22
+ * Validate issue RESTRICTED parameters
23
+ * @param {object} params - Issue parameters
24
+ * @throws {Error} If validation fails
25
+ */
26
+ validateParams(params) {
27
+ // Validate required parameters
28
+ if (!params.assetName) {
29
+ throw new Error('assetName is required');
30
+ }
31
+
32
+ if (params.quantity === undefined || params.quantity === null) {
33
+ throw new Error('quantity is required');
34
+ }
35
+
36
+ if (!params.verifierString) {
37
+ throw new Error('verifierString is required for restricted assets');
38
+ }
39
+
40
+ // Validate asset name (RESTRICTED format: $NAME)
41
+ this.validateAssetName(params.assetName, 'RESTRICTED');
42
+
43
+ // Validate quantity and units
44
+ const units = params.units !== undefined ? params.units : 0;
45
+ this.validateAmount(params.quantity, units);
46
+
47
+ // Validate verifier string (critical for compliance)
48
+ VerifierValidator.validate(params.verifierString);
49
+
50
+ // Validate IPFS hash if provided
51
+ if (params.hasIpfs && params.ipfsHash) {
52
+ IpfsValidator.validate(params.ipfsHash);
53
+ }
54
+
55
+ // Validate reissuable is boolean if provided
56
+ if (params.reissuable !== undefined && typeof params.reissuable !== 'boolean') {
57
+ throw new Error('reissuable must be a boolean');
58
+ }
59
+
60
+ return true;
61
+ }
62
+
63
+ /**
64
+ * Build RESTRICTED asset issuance transaction
65
+ * @returns {Promise<object>} Transaction result
66
+ */
67
+ async build() {
68
+ // 1. Validate parameters
69
+ await this.validateParams(this.params);
70
+
71
+ const {
72
+ assetName,
73
+ quantity,
74
+ units = 0,
75
+ verifierString,
76
+ reissuable = true,
77
+ hasIpfs = false,
78
+ ipfsHash = ''
79
+ } = this.params;
80
+
81
+ // 2. Check if asset already exists
82
+ const exists = await this.assetExists(assetName);
83
+ if (exists) {
84
+ throw new AssetExistsError(
85
+ `Asset ${assetName} already exists on the blockchain`,
86
+ assetName
87
+ );
88
+ }
89
+
90
+ // 3. Extract qualifiers from verifier string for info
91
+ const requiredQualifiers = VerifierValidator.extractQualifiers(verifierString);
92
+
93
+ // 4. Get burn information (3000 XNA for restricted assets)
94
+ const burnInfo = this.burnManager.getIssueRestrictedBurn();
95
+
96
+ // 5. Get addresses
97
+ const addresses = await this._getAddresses();
98
+ const toAddress = await this.getToAddress();
99
+ const changeAddress = await this.getChangeAddress();
100
+
101
+ // 6. Estimate fee
102
+ const estimatedFee = await this.estimateFee(1, 3);
103
+
104
+ // 7. Calculate total XNA needed
105
+ const totalXNANeeded = burnInfo.amount + estimatedFee;
106
+
107
+ // 8. Select XNA UTXOs
108
+ const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
109
+ const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
110
+ const totalXNAInput = utxoSelection.totalXNA;
111
+
112
+ // 9. Recalculate fee with actual input count
113
+ const actualFee = await this.estimateFee(baseCurrencyUTXOs.length, 3);
114
+
115
+ // 10. Verify we have enough XNA
116
+ const totalRequired = burnInfo.amount + actualFee;
117
+ if (totalXNAInput < totalRequired) {
118
+ const additionalNeeded = totalRequired - totalXNAInput + 0.001;
119
+ const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
120
+ baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
121
+ }
122
+
123
+ // 11. Calculate XNA change
124
+ const finalTotalInput = baseCurrencyUTXOs.reduce(
125
+ (sum, utxo) => sum + utxo.satoshis / 100000000,
126
+ 0
127
+ );
128
+ const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
129
+
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
+ }));
137
+
138
+ // 13. Build outputs (ORDER CRITICAL!)
139
+ const outputs = {};
140
+
141
+ // First: Burn output
142
+ outputs[burnInfo.address] = burnInfo.amount;
143
+
144
+ // Second: XNA change (if any)
145
+ if (xnaChange > 0.00000001) {
146
+ outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
147
+ }
148
+
149
+ // Last: Issue restricted operation
150
+ const issueRestrictedOutput = OutputFormatter.formatIssueRestrictedOutput({
151
+ asset_name: assetName,
152
+ asset_quantity: this.toSatoshis(quantity, units),
153
+ verifier_string: verifierString,
154
+ units: units,
155
+ reissuable: reissuable,
156
+ has_ipfs: hasIpfs,
157
+ ipfs_hash: ipfsHash
158
+ });
159
+
160
+ outputs[toAddress] = issueRestrictedOutput;
161
+
162
+ // 14. Order outputs (protocol requirement)
163
+ const orderedOutputs = this.outputOrderer.order(outputs);
164
+
165
+ // 15. Create raw transaction
166
+ const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
167
+
168
+ // 16. Format and return result
169
+ return this.formatResult(
170
+ rawTx,
171
+ baseCurrencyUTXOs,
172
+ inputs,
173
+ orderedOutputs,
174
+ actualFee,
175
+ burnInfo.amount,
176
+ {
177
+ assetName,
178
+ ownerTokenName: assetName + '!',
179
+ verifierString,
180
+ requiredQualifiers,
181
+ operationType: 'ISSUE_RESTRICTED'
182
+ }
183
+ );
184
+ }
185
+ }
186
+
187
+ module.exports = IssueRestrictedBuilder;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Issue Root Builder
3
+ * Builds transactions for creating ROOT assets
4
+ *
5
+ * ROOT assets:
6
+ * - Top-level assets (3-30 uppercase characters)
7
+ * - Cost: 1000 XNA (burned)
8
+ * - Automatically creates owner token (ASSET!)
9
+ * - Can be reissuable or non-reissuable
10
+ * - Optional IPFS metadata
11
+ */
12
+
13
+ const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
14
+ const { OutputFormatter } = require('../utils');
15
+ const { AssetExistsError, InvalidIPFSHashError } = require('../errors');
16
+ const { IpfsValidator } = require('../validators');
17
+
18
+ class IssueRootBuilder extends BaseAssetTransactionBuilder {
19
+ /**
20
+ * Validate issue ROOT parameters
21
+ * @param {object} params - Issue parameters
22
+ * @throws {Error} If validation fails
23
+ */
24
+ validateParams(params) {
25
+ // Validate required parameters
26
+ if (!params.assetName) {
27
+ throw new Error('assetName is required');
28
+ }
29
+
30
+ if (params.quantity === undefined || params.quantity === null) {
31
+ throw new Error('quantity is required');
32
+ }
33
+
34
+ // Validate asset name (ROOT format)
35
+ this.validateAssetName(params.assetName, 'ROOT');
36
+
37
+ // Validate quantity and units
38
+ const units = params.units !== undefined ? params.units : 0;
39
+ this.validateAmount(params.quantity, units);
40
+
41
+ // Validate IPFS hash if provided
42
+ if (params.hasIpfs && params.ipfsHash) {
43
+ IpfsValidator.validate(params.ipfsHash);
44
+ }
45
+
46
+ // Validate reissuable is boolean if provided
47
+ if (params.reissuable !== undefined && typeof params.reissuable !== 'boolean') {
48
+ throw new Error('reissuable must be a boolean');
49
+ }
50
+
51
+ return true;
52
+ }
53
+
54
+ /**
55
+ * Build ROOT asset issuance transaction
56
+ * @returns {Promise<object>} Transaction result
57
+ */
58
+ async build() {
59
+ // 1. Validate parameters
60
+ await this.validateParams(this.params);
61
+
62
+ const {
63
+ assetName,
64
+ quantity,
65
+ units = 0,
66
+ reissuable = true,
67
+ hasIpfs = false,
68
+ ipfsHash = ''
69
+ } = this.params;
70
+
71
+ // 2. Check if asset already exists
72
+ const exists = await this.assetExists(assetName);
73
+ if (exists) {
74
+ throw new AssetExistsError(
75
+ `Asset ${assetName} already exists on the blockchain`,
76
+ assetName
77
+ );
78
+ }
79
+
80
+ // 3. Get burn information
81
+ const burnInfo = this.burnManager.getIssueRootBurn();
82
+
83
+ // 4. Get addresses
84
+ const addresses = await this._getAddresses();
85
+ const toAddress = await this.getToAddress();
86
+ const changeAddress = await this.getChangeAddress();
87
+
88
+ // 5. Estimate fee (rough estimate for initial UTXO selection)
89
+ const estimatedFee = await this.estimateFee(1, 3);
90
+
91
+ // 6. Calculate total XNA needed
92
+ const totalXNANeeded = burnInfo.amount + estimatedFee;
93
+
94
+ // 7. Select UTXOs
95
+ const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
96
+ const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
97
+ const totalXNAInput = utxoSelection.totalXNA;
98
+
99
+ // 8. Recalculate fee with actual input count
100
+ const actualFee = await this.estimateFee(baseCurrencyUTXOs.length, 3);
101
+
102
+ // 9. Verify we still have enough after fee recalculation
103
+ const totalRequired = burnInfo.amount + actualFee;
104
+ if (totalXNAInput < totalRequired) {
105
+ // Need to select more UTXOs
106
+ const additionalNeeded = totalRequired - totalXNAInput + 0.001; // Add small buffer
107
+ const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
108
+ baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
109
+ }
110
+
111
+ // 10. Calculate final totals
112
+ const finalTotalInput = baseCurrencyUTXOs.reduce(
113
+ (sum, utxo) => sum + utxo.satoshis / 100000000,
114
+ 0
115
+ );
116
+ const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
117
+
118
+ // 11. Build inputs
119
+ const inputs = baseCurrencyUTXOs.map(utxo => ({
120
+ txid: utxo.txid,
121
+ vout: utxo.outputIndex,
122
+ address: utxo.address,
123
+ satoshis: utxo.satoshis
124
+ }));
125
+
126
+ // 12. Build outputs (ORDER MATTERS!)
127
+ const outputs = {};
128
+
129
+ // First: Burn output
130
+ outputs[burnInfo.address] = burnInfo.amount;
131
+
132
+ // Second: XNA change (if any)
133
+ if (xnaChange > 0.00000001) {
134
+ // Only add change if meaningful amount
135
+ outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
136
+ }
137
+
138
+ // Last: Issue operation
139
+ const issueOutput = OutputFormatter.formatIssueOutput({
140
+ asset_name: assetName,
141
+ asset_quantity: this.toSatoshis(quantity, units),
142
+ units: units,
143
+ reissuable: reissuable,
144
+ has_ipfs: hasIpfs,
145
+ ipfs_hash: ipfsHash
146
+ });
147
+
148
+ outputs[toAddress] = issueOutput;
149
+
150
+ // 13. Order outputs (critical for protocol)
151
+ const orderedOutputs = this.outputOrderer.order(outputs);
152
+
153
+ // 14. Create raw transaction
154
+ const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
155
+
156
+ // 15. Format and return result
157
+ return this.formatResult(
158
+ rawTx,
159
+ baseCurrencyUTXOs,
160
+ inputs,
161
+ orderedOutputs,
162
+ actualFee,
163
+ burnInfo.amount,
164
+ {
165
+ assetName,
166
+ ownerTokenName: assetName + '!',
167
+ operationType: 'ISSUE_ROOT'
168
+ }
169
+ );
170
+ }
171
+ }
172
+
173
+ module.exports = IssueRootBuilder;