@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,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;
|