@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.
- package/README.md +522 -0
- package/examples/01-create-root-asset.js +71 -0
- package/examples/02-create-sub-asset.js +79 -0
- package/examples/03-create-nfts.js +140 -0
- package/examples/04-reissue-asset.js +164 -0
- package/examples/05-create-qualifier-and-tag.js +209 -0
- package/examples/06-create-restricted-asset.js +223 -0
- package/examples/07-freeze-and-unfreeze.js +292 -0
- package/examples/08-query-assets.js +332 -0
- package/examples/09-wallet-integration.js +320 -0
- package/examples/README.md +319 -0
- package/package.json +43 -0
- package/src/NeuraiAssets.js +468 -0
- package/src/builders/BaseAssetTransactionBuilder.js +303 -0
- package/src/builders/FreezeAddressBuilder.js +271 -0
- package/src/builders/IssueQualifierBuilder.js +251 -0
- package/src/builders/IssueRestrictedBuilder.js +187 -0
- package/src/builders/IssueRootBuilder.js +173 -0
- package/src/builders/IssueSubBuilder.js +237 -0
- package/src/builders/IssueUniqueBuilder.js +255 -0
- package/src/builders/ReissueBuilder.js +246 -0
- package/src/builders/ReissueRestrictedBuilder.js +264 -0
- package/src/builders/TagAddressBuilder.js +243 -0
- package/src/builders/index.js +38 -0
- package/src/constants/assetTypes.js +23 -0
- package/src/constants/burnAddresses.js +65 -0
- package/src/constants/fees.js +61 -0
- package/src/constants/index.js +44 -0
- package/src/constants/networks.js +112 -0
- package/src/errors/AssetErrors.js +135 -0
- package/src/errors/ValidationErrors.js +87 -0
- package/src/errors/index.js +56 -0
- package/src/index.js +68 -0
- package/src/managers/BurnManager.js +222 -0
- package/src/managers/OutputOrderer.js +289 -0
- package/src/managers/OwnerTokenManager.js +265 -0
- package/src/managers/UTXOSelector.js +309 -0
- package/src/managers/index.js +16 -0
- package/src/queries/AssetQueries.js +447 -0
- package/src/queries/index.js +10 -0
- package/src/utils/amountConverter.js +115 -0
- package/src/utils/assetNameParser.js +203 -0
- package/src/utils/index.js +16 -0
- package/src/utils/networkDetector.js +144 -0
- package/src/utils/outputFormatter.js +292 -0
- package/src/validators/amountValidator.js +149 -0
- package/src/validators/assetNameValidator.js +296 -0
- package/src/validators/index.js +16 -0
- package/src/validators/ipfsValidator.js +101 -0
- package/src/validators/verifierValidator.js +146 -0
- package/tests/README.md +126 -0
- package/tests/integration/assetLifecycle.test.js +244 -0
- package/tests/mocks/rpcMock.js +156 -0
- package/tests/unit/NeuraiAssets.test.js +217 -0
- package/tests/unit/utils/amountConverter.test.js +171 -0
- package/tests/unit/utils/assetNameParser.test.js +203 -0
- package/tests/unit/validators/amountValidator.test.js +143 -0
- package/tests/unit/validators/assetNameValidator.test.js +228 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue Sub Builder
|
|
3
|
+
* Builds transactions for creating SUB assets
|
|
4
|
+
*
|
|
5
|
+
* SUB assets:
|
|
6
|
+
* - Child of a ROOT asset (format: ROOT/SUBNAME)
|
|
7
|
+
* - Cost: 200 XNA (burned)
|
|
8
|
+
* - Requires parent's owner token (ROOT!)
|
|
9
|
+
* - Creates own owner token (ROOT/SUB!)
|
|
10
|
+
* - Parent owner token must be returned in outputs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
|
|
14
|
+
const { OutputFormatter, AssetNameParser } = require('../utils');
|
|
15
|
+
const {
|
|
16
|
+
AssetExistsError,
|
|
17
|
+
ParentAssetNotFoundError,
|
|
18
|
+
OwnerTokenNotFoundError
|
|
19
|
+
} = require('../errors');
|
|
20
|
+
const { IpfsValidator } = require('../validators');
|
|
21
|
+
|
|
22
|
+
class IssueSubBuilder extends BaseAssetTransactionBuilder {
|
|
23
|
+
/**
|
|
24
|
+
* Validate issue SUB parameters
|
|
25
|
+
* @param {object} params - Issue parameters
|
|
26
|
+
* @throws {Error} If validation fails
|
|
27
|
+
*/
|
|
28
|
+
validateParams(params) {
|
|
29
|
+
// Validate required parameters
|
|
30
|
+
if (!params.assetName) {
|
|
31
|
+
throw new Error('assetName is required');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (params.quantity === undefined || params.quantity === null) {
|
|
35
|
+
throw new Error('quantity is required');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Validate asset name (SUB format: ROOT/SUBNAME)
|
|
39
|
+
this.validateAssetName(params.assetName, 'SUB');
|
|
40
|
+
|
|
41
|
+
// Validate quantity and units
|
|
42
|
+
const units = params.units !== undefined ? params.units : 0;
|
|
43
|
+
this.validateAmount(params.quantity, units);
|
|
44
|
+
|
|
45
|
+
// Validate IPFS hash if provided
|
|
46
|
+
if (params.hasIpfs && params.ipfsHash) {
|
|
47
|
+
IpfsValidator.validate(params.ipfsHash);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build SUB asset issuance transaction
|
|
55
|
+
* @returns {Promise<object>} Transaction result
|
|
56
|
+
*/
|
|
57
|
+
async build() {
|
|
58
|
+
// 1. Validate parameters
|
|
59
|
+
await this.validateParams(this.params);
|
|
60
|
+
|
|
61
|
+
const {
|
|
62
|
+
assetName,
|
|
63
|
+
quantity,
|
|
64
|
+
units = 0,
|
|
65
|
+
reissuable = true,
|
|
66
|
+
hasIpfs = false,
|
|
67
|
+
ipfsHash = ''
|
|
68
|
+
} = this.params;
|
|
69
|
+
|
|
70
|
+
// 2. Parse asset name to get parent
|
|
71
|
+
const parsed = AssetNameParser.parse(assetName);
|
|
72
|
+
const parentAssetName = parsed.parent;
|
|
73
|
+
|
|
74
|
+
if (!parentAssetName) {
|
|
75
|
+
throw new Error('Cannot parse parent asset from SUB asset name');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Check if parent asset exists
|
|
79
|
+
const parentExists = await this.assetExists(parentAssetName);
|
|
80
|
+
if (!parentExists) {
|
|
81
|
+
throw new ParentAssetNotFoundError(
|
|
82
|
+
`Parent asset ${parentAssetName} does not exist. You must create the ROOT asset first.`,
|
|
83
|
+
parentAssetName
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 4. Check if SUB asset already exists
|
|
88
|
+
const subExists = await this.assetExists(assetName);
|
|
89
|
+
if (subExists) {
|
|
90
|
+
throw new AssetExistsError(
|
|
91
|
+
`Asset ${assetName} already exists on the blockchain`,
|
|
92
|
+
assetName
|
|
93
|
+
);
|
|
94
|
+
}
|
|
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. Find parent's owner token (CRITICAL: must have this)
|
|
102
|
+
const ownerTokenName = AssetNameParser.getOwnerTokenName(parentAssetName);
|
|
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 parent asset's owner token (${ownerTokenName}) to create a SUB asset. ` +
|
|
113
|
+
`The owner token proves you control the parent asset.`,
|
|
114
|
+
ownerTokenName
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 7. Get burn information
|
|
121
|
+
const burnInfo = this.burnManager.getIssueSubBurn();
|
|
122
|
+
|
|
123
|
+
// 8. Estimate fee
|
|
124
|
+
// Inputs: XNA UTXOs + owner token UTXO
|
|
125
|
+
// Outputs: burn + change + owner token return + issue operation
|
|
126
|
+
const estimatedFee = await this.estimateFee(2, 4);
|
|
127
|
+
|
|
128
|
+
// 9. Calculate total XNA needed
|
|
129
|
+
const totalXNANeeded = burnInfo.amount + estimatedFee;
|
|
130
|
+
|
|
131
|
+
// 10. Select XNA UTXOs
|
|
132
|
+
const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
|
|
133
|
+
const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
|
|
134
|
+
const totalXNAInput = utxoSelection.totalXNA;
|
|
135
|
+
|
|
136
|
+
// 11. Recalculate fee with actual input count
|
|
137
|
+
const actualInputCount = baseCurrencyUTXOs.length + 1; // +1 for owner token
|
|
138
|
+
const actualFee = await this.estimateFee(actualInputCount, 4);
|
|
139
|
+
|
|
140
|
+
// 12. Verify we have enough XNA
|
|
141
|
+
const totalRequired = burnInfo.amount + actualFee;
|
|
142
|
+
if (totalXNAInput < totalRequired) {
|
|
143
|
+
const additionalNeeded = totalRequired - totalXNAInput + 0.001;
|
|
144
|
+
const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
|
|
145
|
+
baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 13. Calculate XNA change
|
|
149
|
+
const finalTotalInput = baseCurrencyUTXOs.reduce(
|
|
150
|
+
(sum, utxo) => sum + utxo.satoshis / 100000000,
|
|
151
|
+
0
|
|
152
|
+
);
|
|
153
|
+
const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
|
|
154
|
+
|
|
155
|
+
// 14. Build inputs (XNA + owner token)
|
|
156
|
+
const inputs = [];
|
|
157
|
+
|
|
158
|
+
// Add XNA inputs
|
|
159
|
+
baseCurrencyUTXOs.forEach(utxo => {
|
|
160
|
+
inputs.push({
|
|
161
|
+
txid: utxo.txid,
|
|
162
|
+
vout: utxo.outputIndex,
|
|
163
|
+
address: utxo.address,
|
|
164
|
+
satoshis: utxo.satoshis
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Add owner token input
|
|
169
|
+
inputs.push({
|
|
170
|
+
txid: ownerTokenUTXO.txid,
|
|
171
|
+
vout: ownerTokenUTXO.outputIndex,
|
|
172
|
+
address: ownerTokenUTXO.address,
|
|
173
|
+
assetName: ownerTokenUTXO.assetName,
|
|
174
|
+
satoshis: ownerTokenUTXO.satoshis
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// 15. Build outputs (ORDER CRITICAL!)
|
|
178
|
+
const outputs = {};
|
|
179
|
+
|
|
180
|
+
// First: Burn output
|
|
181
|
+
outputs[burnInfo.address] = burnInfo.amount;
|
|
182
|
+
|
|
183
|
+
// Second: XNA change (if any)
|
|
184
|
+
if (xnaChange > 0.00000001) {
|
|
185
|
+
outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Third: Owner token return (CRITICAL - must return or lost forever!)
|
|
189
|
+
const ownerTokenReturn = this.ownerTokenManager.createOwnerTokenReturnOutput(
|
|
190
|
+
ownerTokenName,
|
|
191
|
+
changeAddress // Return owner token to change address
|
|
192
|
+
);
|
|
193
|
+
Object.assign(outputs, ownerTokenReturn);
|
|
194
|
+
|
|
195
|
+
// Last: Issue operation
|
|
196
|
+
const issueOutput = OutputFormatter.formatIssueOutput({
|
|
197
|
+
asset_name: assetName,
|
|
198
|
+
asset_quantity: this.toSatoshis(quantity, units),
|
|
199
|
+
units: units,
|
|
200
|
+
reissuable: reissuable,
|
|
201
|
+
has_ipfs: hasIpfs,
|
|
202
|
+
ipfs_hash: ipfsHash
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
outputs[toAddress] = issueOutput;
|
|
206
|
+
|
|
207
|
+
// 16. Order outputs (protocol requirement)
|
|
208
|
+
const orderedOutputs = this.outputOrderer.order(outputs);
|
|
209
|
+
|
|
210
|
+
// 17. Validate owner token is returned (safety check)
|
|
211
|
+
this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
|
|
212
|
+
|
|
213
|
+
// 18. Create raw transaction
|
|
214
|
+
const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
|
|
215
|
+
|
|
216
|
+
// 19. Format and return result
|
|
217
|
+
const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
|
|
218
|
+
|
|
219
|
+
return this.formatResult(
|
|
220
|
+
rawTx,
|
|
221
|
+
allUTXOs,
|
|
222
|
+
inputs,
|
|
223
|
+
orderedOutputs,
|
|
224
|
+
actualFee,
|
|
225
|
+
burnInfo.amount,
|
|
226
|
+
{
|
|
227
|
+
assetName,
|
|
228
|
+
parentAssetName,
|
|
229
|
+
ownerTokenName: assetName + '!',
|
|
230
|
+
parentOwnerTokenUsed: ownerTokenName,
|
|
231
|
+
operationType: 'ISSUE_SUB'
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = IssueSubBuilder;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue Unique Builder
|
|
3
|
+
* Builds transactions for creating UNIQUE assets (NFTs)
|
|
4
|
+
*
|
|
5
|
+
* UNIQUE assets:
|
|
6
|
+
* - Non-fungible tokens (NFTs)
|
|
7
|
+
* - Format: ROOT#TAG (e.g., MYNFT#001)
|
|
8
|
+
* - Cost: 10 XNA per NFT (burned)
|
|
9
|
+
* - Requires parent's owner token (ROOT!)
|
|
10
|
+
* - Properties: quantity=1, units=0, reissuable=false (always)
|
|
11
|
+
* - Can create multiple NFTs in single transaction
|
|
12
|
+
* - Each NFT can have unique IPFS metadata
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
|
|
16
|
+
const { OutputFormatter, AssetNameParser } = require('../utils');
|
|
17
|
+
const {
|
|
18
|
+
ParentAssetNotFoundError,
|
|
19
|
+
OwnerTokenNotFoundError,
|
|
20
|
+
AssetExistsError
|
|
21
|
+
} = require('../errors');
|
|
22
|
+
const { IpfsValidator } = require('../validators');
|
|
23
|
+
|
|
24
|
+
class IssueUniqueBuilder extends BaseAssetTransactionBuilder {
|
|
25
|
+
/**
|
|
26
|
+
* Validate issue UNIQUE parameters
|
|
27
|
+
* @param {object} params - Issue parameters
|
|
28
|
+
* @throws {Error} If validation fails
|
|
29
|
+
*/
|
|
30
|
+
validateParams(params) {
|
|
31
|
+
// Validate required parameters
|
|
32
|
+
if (!params.rootName) {
|
|
33
|
+
throw new Error('rootName is required (parent asset name)');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!params.assetTags || !Array.isArray(params.assetTags) || params.assetTags.length === 0) {
|
|
37
|
+
throw new Error('assetTags is required and must be a non-empty array');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate root name
|
|
41
|
+
this.validateAssetName(params.rootName, 'ROOT');
|
|
42
|
+
|
|
43
|
+
// Validate each tag
|
|
44
|
+
params.assetTags.forEach((tag, index) => {
|
|
45
|
+
if (!tag || typeof tag !== 'string') {
|
|
46
|
+
throw new Error(`assetTags[${index}] must be a non-empty string`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Validate full unique asset name
|
|
50
|
+
const fullName = `${params.rootName}#${tag}`;
|
|
51
|
+
this.validateAssetName(fullName, 'UNIQUE');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Validate IPFS hashes if provided
|
|
55
|
+
if (params.ipfsHashes) {
|
|
56
|
+
if (!Array.isArray(params.ipfsHashes)) {
|
|
57
|
+
throw new Error('ipfsHashes must be an array');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (params.ipfsHashes.length !== params.assetTags.length) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`ipfsHashes array length (${params.ipfsHashes.length}) must match ` +
|
|
63
|
+
`assetTags array length (${params.assetTags.length})`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
params.ipfsHashes.forEach((hash, index) => {
|
|
68
|
+
if (hash) {
|
|
69
|
+
IpfsValidator.validate(hash);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build UNIQUE asset issuance transaction
|
|
79
|
+
* @returns {Promise<object>} Transaction result
|
|
80
|
+
*/
|
|
81
|
+
async build() {
|
|
82
|
+
// 1. Validate parameters
|
|
83
|
+
await this.validateParams(this.params);
|
|
84
|
+
|
|
85
|
+
const {
|
|
86
|
+
rootName,
|
|
87
|
+
assetTags,
|
|
88
|
+
ipfsHashes = []
|
|
89
|
+
} = this.params;
|
|
90
|
+
|
|
91
|
+
// 2. Check if parent asset exists
|
|
92
|
+
const parentExists = await this.assetExists(rootName);
|
|
93
|
+
if (!parentExists) {
|
|
94
|
+
throw new ParentAssetNotFoundError(
|
|
95
|
+
`Parent asset ${rootName} does not exist. You must create the ROOT asset first.`,
|
|
96
|
+
rootName
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 3. Check if any of the unique assets already exist
|
|
101
|
+
for (const tag of assetTags) {
|
|
102
|
+
const fullName = `${rootName}#${tag}`;
|
|
103
|
+
const exists = await this.assetExists(fullName);
|
|
104
|
+
if (exists) {
|
|
105
|
+
throw new AssetExistsError(
|
|
106
|
+
`Unique asset ${fullName} already exists on the blockchain`,
|
|
107
|
+
fullName
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 4. Get addresses
|
|
113
|
+
const addresses = await this._getAddresses();
|
|
114
|
+
const toAddress = await this.getToAddress();
|
|
115
|
+
const changeAddress = await this.getChangeAddress();
|
|
116
|
+
|
|
117
|
+
// 5. Find parent's owner token (CRITICAL: must have this)
|
|
118
|
+
const ownerTokenName = AssetNameParser.getOwnerTokenName(rootName);
|
|
119
|
+
let ownerTokenUTXO;
|
|
120
|
+
try {
|
|
121
|
+
ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
|
|
122
|
+
ownerTokenName,
|
|
123
|
+
addresses
|
|
124
|
+
);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof OwnerTokenNotFoundError) {
|
|
127
|
+
throw new OwnerTokenNotFoundError(
|
|
128
|
+
`You must own the parent asset's owner token (${ownerTokenName}) to create UNIQUE assets. ` +
|
|
129
|
+
`The owner token proves you control the parent asset.`,
|
|
130
|
+
ownerTokenName
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 6. Get burn information (cost = 10 XNA per NFT)
|
|
137
|
+
const nftCount = assetTags.length;
|
|
138
|
+
const burnInfo = this.burnManager.getIssueUniqueBurn(nftCount);
|
|
139
|
+
|
|
140
|
+
// 7. Estimate fee
|
|
141
|
+
// Inputs: XNA UTXOs + owner token UTXO
|
|
142
|
+
// Outputs: burn + change + owner token return + issue_unique operation
|
|
143
|
+
const estimatedFee = await this.estimateFee(2, 4);
|
|
144
|
+
|
|
145
|
+
// 8. Calculate total XNA needed
|
|
146
|
+
const totalXNANeeded = burnInfo.amount + estimatedFee;
|
|
147
|
+
|
|
148
|
+
// 9. Select XNA UTXOs
|
|
149
|
+
const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
|
|
150
|
+
const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
|
|
151
|
+
const totalXNAInput = utxoSelection.totalXNA;
|
|
152
|
+
|
|
153
|
+
// 10. Recalculate fee with actual input count
|
|
154
|
+
const actualInputCount = baseCurrencyUTXOs.length + 1; // +1 for owner token
|
|
155
|
+
const actualFee = await this.estimateFee(actualInputCount, 4);
|
|
156
|
+
|
|
157
|
+
// 11. Verify we have enough XNA
|
|
158
|
+
const totalRequired = burnInfo.amount + actualFee;
|
|
159
|
+
if (totalXNAInput < totalRequired) {
|
|
160
|
+
const additionalNeeded = totalRequired - totalXNAInput + 0.001;
|
|
161
|
+
const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
|
|
162
|
+
baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 12. Calculate XNA change
|
|
166
|
+
const finalTotalInput = baseCurrencyUTXOs.reduce(
|
|
167
|
+
(sum, utxo) => sum + utxo.satoshis / 100000000,
|
|
168
|
+
0
|
|
169
|
+
);
|
|
170
|
+
const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
|
|
171
|
+
|
|
172
|
+
// 13. Build inputs (XNA + owner token)
|
|
173
|
+
const inputs = [];
|
|
174
|
+
|
|
175
|
+
// Add XNA inputs
|
|
176
|
+
baseCurrencyUTXOs.forEach(utxo => {
|
|
177
|
+
inputs.push({
|
|
178
|
+
txid: utxo.txid,
|
|
179
|
+
vout: utxo.outputIndex,
|
|
180
|
+
address: utxo.address,
|
|
181
|
+
satoshis: utxo.satoshis
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Add owner token input
|
|
186
|
+
inputs.push({
|
|
187
|
+
txid: ownerTokenUTXO.txid,
|
|
188
|
+
vout: ownerTokenUTXO.outputIndex,
|
|
189
|
+
address: ownerTokenUTXO.address,
|
|
190
|
+
assetName: ownerTokenUTXO.assetName,
|
|
191
|
+
satoshis: ownerTokenUTXO.satoshis
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 14. Build outputs (ORDER CRITICAL!)
|
|
195
|
+
const outputs = {};
|
|
196
|
+
|
|
197
|
+
// First: Burn output
|
|
198
|
+
outputs[burnInfo.address] = burnInfo.amount;
|
|
199
|
+
|
|
200
|
+
// Second: XNA change (if any)
|
|
201
|
+
if (xnaChange > 0.00000001) {
|
|
202
|
+
outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
|
|
203
|
+
}
|
|
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
|
+
// Last: Issue unique operation
|
|
213
|
+
const issueUniqueOutput = OutputFormatter.formatIssueUniqueOutput({
|
|
214
|
+
root_name: rootName,
|
|
215
|
+
asset_tags: assetTags,
|
|
216
|
+
ipfs_hashes: ipfsHashes.length > 0 ? ipfsHashes : undefined
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
outputs[toAddress] = issueUniqueOutput;
|
|
220
|
+
|
|
221
|
+
// 15. Order outputs (protocol requirement)
|
|
222
|
+
const orderedOutputs = this.outputOrderer.order(outputs);
|
|
223
|
+
|
|
224
|
+
// 16. Validate owner token is returned (safety check)
|
|
225
|
+
this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
|
|
226
|
+
|
|
227
|
+
// 17. Create raw transaction
|
|
228
|
+
const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
|
|
229
|
+
|
|
230
|
+
// 18. Build list of created NFT names
|
|
231
|
+
const createdNFTs = assetTags.map(tag => `${rootName}#${tag}`);
|
|
232
|
+
|
|
233
|
+
// 19. Format and return result
|
|
234
|
+
const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
|
|
235
|
+
|
|
236
|
+
return this.formatResult(
|
|
237
|
+
rawTx,
|
|
238
|
+
allUTXOs,
|
|
239
|
+
inputs,
|
|
240
|
+
orderedOutputs,
|
|
241
|
+
actualFee,
|
|
242
|
+
burnInfo.amount,
|
|
243
|
+
{
|
|
244
|
+
rootName,
|
|
245
|
+
assetTags,
|
|
246
|
+
createdNFTs,
|
|
247
|
+
nftCount,
|
|
248
|
+
ownerTokenUsed: ownerTokenName,
|
|
249
|
+
operationType: 'ISSUE_UNIQUE'
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = IssueUniqueBuilder;
|