@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 +18 -13
- package/package.json +1 -1
- package/src/NeuraiAssets.js +2 -1
- package/src/builders/BaseAssetTransactionBuilder.js +24 -6
- package/src/builders/FreezeAddressBuilder.js +9 -26
- package/src/builders/IssueQualifierBuilder.js +35 -50
- package/src/builders/IssueRestrictedBuilder.js +63 -29
- package/src/builders/IssueRootBuilder.js +4 -4
- package/src/builders/IssueSubBuilder.js +5 -5
- package/src/builders/IssueUniqueBuilder.js +10 -17
- package/src/builders/ReissueBuilder.js +12 -20
- package/src/builders/ReissueRestrictedBuilder.js +9 -19
- package/src/builders/TagAddressBuilder.js +41 -54
- package/src/constants/burnAddresses.js +17 -17
- package/src/constants/fees.js +2 -2
- package/src/constants/networks.js +6 -6
- package/src/managers/OutputOrderer.js +35 -43
- package/src/managers/OwnerTokenManager.js +9 -4
- package/src/managers/UTXOSelector.js +3 -5
- package/src/utils/assetNameParser.js +3 -0
- package/src/utils/networkDetector.js +2 -2
- package/src/utils/outputFormatter.js +65 -19
- package/tests/unit/utils/assetNameParser.test.js +1 -1
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
|
-
|
|
101
|
-
assetTags: [
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
package/src/NeuraiAssets.js
CHANGED
|
@@ -167,7 +167,8 @@ class NeuraiAssets {
|
|
|
167
167
|
* @returns {Promise<object>} Transaction data
|
|
168
168
|
*/
|
|
169
169
|
async createQualifier(params) {
|
|
170
|
-
const
|
|
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,
|
|
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
|
|
38
|
-
this.getAddresses =
|
|
39
|
-
} else if (Array.isArray(
|
|
40
|
-
this.getAddresses = () =>
|
|
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 =
|
|
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
|
-
//
|
|
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]
|
|
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[
|
|
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[
|
|
178
|
+
outputs.push({ [changeAddress]: operationOutput });
|
|
193
179
|
break;
|
|
194
180
|
|
|
195
181
|
case 'FREEZE_ASSET':
|
|
196
182
|
operationOutput = OutputFormatter.formatFreezeAssetOutput(assetName);
|
|
197
|
-
outputs[changeAddress]
|
|
183
|
+
outputs.push({ [changeAddress]: operationOutput });
|
|
198
184
|
break;
|
|
199
185
|
|
|
200
186
|
case 'UNFREEZE_ASSET':
|
|
201
187
|
operationOutput = OutputFormatter.formatUnfreezeAssetOutput(assetName);
|
|
202
|
-
outputs[changeAddress]
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
* -
|
|
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
|
|
79
|
-
let
|
|
80
|
-
let
|
|
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
|
-
|
|
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
|
|
96
|
-
ownerTokenName = AssetNameParser.getOwnerTokenName(parentQualifierName);
|
|
97
|
+
// Find parent qualifier balance to spend and return as change
|
|
97
98
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
103
|
+
if (error.name === 'InsufficientFundsError') {
|
|
104
104
|
throw new OwnerTokenNotFoundError(
|
|
105
|
-
`You must own the parent qualifier
|
|
106
|
-
|
|
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 =
|
|
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 +
|
|
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
|
|
176
|
-
|
|
175
|
+
// Add parent qualifier inputs if sub-qualifier
|
|
176
|
+
parentQualifierUTXOs.forEach(parentUTXO => {
|
|
177
177
|
inputs.push({
|
|
178
|
-
txid:
|
|
179
|
-
vout:
|
|
180
|
-
address:
|
|
181
|
-
assetName:
|
|
182
|
-
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]
|
|
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]
|
|
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]
|
|
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.
|
|
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
|
-
//
|
|
228
|
-
const allUTXOs =
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
102
|
-
const
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
113
|
-
const actualFee = await this.estimateFee(baseCurrencyUTXOs.length,
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
131
|
-
const inputs =
|
|
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
|
-
|
|
139
|
-
|
|
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]
|
|
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]
|
|
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]
|
|
192
|
+
outputs.push({ [toAddress]: issueRestrictedOutput });
|
|
161
193
|
|
|
162
|
-
//
|
|
194
|
+
// 15. Order outputs (protocol requirement)
|
|
163
195
|
const orderedOutputs = this.outputOrderer.order(outputs);
|
|
164
196
|
|
|
165
|
-
//
|
|
197
|
+
// 16. Create raw transaction
|
|
166
198
|
const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
|
|
167
199
|
|
|
168
|
-
//
|
|
200
|
+
// 17. Format and return result
|
|
201
|
+
const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
|
|
202
|
+
|
|
169
203
|
return this.formatResult(
|
|
170
204
|
rawTx,
|
|
171
|
-
|
|
205
|
+
allUTXOs,
|
|
172
206
|
inputs,
|
|
173
207
|
orderedOutputs,
|
|
174
208
|
actualFee,
|
|
175
209
|
burnInfo.amount,
|
|
176
210
|
{
|
|
177
211
|
assetName,
|
|
178
|
-
ownerTokenName
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
-
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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.
|
|
225
|
-
|
|
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
|
-
//
|
|
223
|
+
// 17. Build list of created NFT names
|
|
231
224
|
const createdNFTs = assetTags.map(tag => `${rootName}#${tag}`);
|
|
232
225
|
|
|
233
|
-
//
|
|
226
|
+
// 18. Format and return result
|
|
234
227
|
const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
|
|
235
228
|
|
|
236
229
|
return this.formatResult(
|