@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,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Asset Transaction Builder
|
|
3
|
+
* Abstract base class for all transaction builders
|
|
4
|
+
*
|
|
5
|
+
* Provides common functionality:
|
|
6
|
+
* - UTXO selection
|
|
7
|
+
* - Fee estimation
|
|
8
|
+
* - Output ordering
|
|
9
|
+
* - Raw transaction creation
|
|
10
|
+
* - Validation
|
|
11
|
+
*
|
|
12
|
+
* Subclasses must implement:
|
|
13
|
+
* - validateParams(params)
|
|
14
|
+
* - build()
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { BurnManager, OwnerTokenManager, UTXOSelector, OutputOrderer } = require('../managers');
|
|
18
|
+
const { AssetNameValidator, AmountValidator } = require('../validators');
|
|
19
|
+
|
|
20
|
+
class BaseAssetTransactionBuilder {
|
|
21
|
+
/**
|
|
22
|
+
* @param {Function} rpc - RPC function
|
|
23
|
+
* @param {string} network - Network type ('xna' or 'xna-test')
|
|
24
|
+
* @param {string[]|Function} addresses - Wallet addresses or function that returns addresses
|
|
25
|
+
* @param {object} params - Transaction parameters
|
|
26
|
+
*/
|
|
27
|
+
constructor(rpc, network, addresses, params) {
|
|
28
|
+
if (!rpc || typeof rpc !== 'function') {
|
|
29
|
+
throw new Error('RPC function is required');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!network) {
|
|
33
|
+
throw new Error('Network is required');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Addresses can be an array or a function that returns addresses
|
|
37
|
+
if (typeof addresses === 'function') {
|
|
38
|
+
this.getAddresses = addresses;
|
|
39
|
+
} else if (Array.isArray(addresses)) {
|
|
40
|
+
this.getAddresses = () => addresses;
|
|
41
|
+
} else {
|
|
42
|
+
throw new Error('Addresses must be an array or a function');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.rpc = rpc;
|
|
46
|
+
this.network = network;
|
|
47
|
+
this.params = params || {};
|
|
48
|
+
|
|
49
|
+
// Initialize managers
|
|
50
|
+
this.burnManager = new BurnManager(network);
|
|
51
|
+
this.ownerTokenManager = new OwnerTokenManager(rpc);
|
|
52
|
+
this.utxoSelector = new UTXOSelector(rpc);
|
|
53
|
+
this.outputOrderer = new OutputOrderer();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get wallet addresses
|
|
58
|
+
* @returns {string[]} Array of addresses
|
|
59
|
+
*/
|
|
60
|
+
async _getAddresses() {
|
|
61
|
+
const addresses = await this.getAddresses();
|
|
62
|
+
if (!Array.isArray(addresses) || addresses.length === 0) {
|
|
63
|
+
throw new Error('No addresses available');
|
|
64
|
+
}
|
|
65
|
+
return addresses;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate transaction parameters
|
|
70
|
+
* Must be implemented by subclasses
|
|
71
|
+
* @param {object} params - Parameters to validate
|
|
72
|
+
* @throws {Error} If validation fails
|
|
73
|
+
*/
|
|
74
|
+
validateParams(params) {
|
|
75
|
+
throw new Error('validateParams must be implemented by subclass');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build the transaction
|
|
80
|
+
* Must be implemented by subclasses
|
|
81
|
+
* @returns {Promise<object>} Transaction result
|
|
82
|
+
*/
|
|
83
|
+
async build() {
|
|
84
|
+
throw new Error('build must be implemented by subclass');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Estimate transaction fee
|
|
89
|
+
* @param {number} inputCount - Number of inputs
|
|
90
|
+
* @param {number} outputCount - Number of outputs
|
|
91
|
+
* @returns {Promise<number>} Estimated fee in XNA
|
|
92
|
+
*/
|
|
93
|
+
async estimateFee(inputCount, outputCount) {
|
|
94
|
+
const feeRate = await this.utxoSelector.getFeeRate();
|
|
95
|
+
return this.utxoSelector.estimateFee(inputCount, outputCount, feeRate);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Select UTXOs for transaction
|
|
100
|
+
* @param {number} xnaAmount - Required XNA amount (for fees + burn)
|
|
101
|
+
* @param {string|null} assetName - Asset name if needed
|
|
102
|
+
* @param {number} assetAmount - Asset amount if needed
|
|
103
|
+
* @returns {Promise<object>} Selected UTXOs
|
|
104
|
+
*/
|
|
105
|
+
async selectUTXOs(xnaAmount, assetName = null, assetAmount = 0) {
|
|
106
|
+
const addresses = await this._getAddresses();
|
|
107
|
+
return this.utxoSelector.selectMixedUTXOs(
|
|
108
|
+
addresses,
|
|
109
|
+
xnaAmount,
|
|
110
|
+
assetName,
|
|
111
|
+
assetAmount
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build raw transaction using RPC
|
|
117
|
+
* @param {Array} inputs - Transaction inputs
|
|
118
|
+
* @param {object} outputs - Transaction outputs (must be ordered)
|
|
119
|
+
* @returns {Promise<string>} Raw transaction hex
|
|
120
|
+
*/
|
|
121
|
+
async buildRawTransaction(inputs, outputs) {
|
|
122
|
+
try {
|
|
123
|
+
// Format inputs for createrawtransaction
|
|
124
|
+
const formattedInputs = inputs.map(input => ({
|
|
125
|
+
txid: input.txid,
|
|
126
|
+
vout: input.vout !== undefined ? input.vout : input.outputIndex
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
// Call createrawtransaction
|
|
130
|
+
const rawTx = await this.rpc('createrawtransaction', [
|
|
131
|
+
formattedInputs,
|
|
132
|
+
outputs
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
return rawTx;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new Error(`Failed to create raw transaction: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Calculate change amount
|
|
143
|
+
* @param {number} totalInput - Total input amount
|
|
144
|
+
* @param {number} totalOutput - Total output amount (including fee)
|
|
145
|
+
* @returns {number} Change amount
|
|
146
|
+
*/
|
|
147
|
+
calculateChange(totalInput, totalOutput) {
|
|
148
|
+
const change = totalInput - totalOutput;
|
|
149
|
+
if (change < 0) {
|
|
150
|
+
throw new Error('Insufficient funds: inputs < outputs');
|
|
151
|
+
}
|
|
152
|
+
return change;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get change address
|
|
157
|
+
* Uses first address from wallet by default
|
|
158
|
+
* Can be overridden by params.changeAddress
|
|
159
|
+
* @returns {Promise<string>} Change address
|
|
160
|
+
*/
|
|
161
|
+
async getChangeAddress() {
|
|
162
|
+
if (this.params.changeAddress) {
|
|
163
|
+
return this.params.changeAddress;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const addresses = await this._getAddresses();
|
|
167
|
+
return addresses[0];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get recipient address
|
|
172
|
+
* Uses first address from wallet if not specified
|
|
173
|
+
* @returns {Promise<string>} Recipient address
|
|
174
|
+
*/
|
|
175
|
+
async getToAddress() {
|
|
176
|
+
if (this.params.toAddress) {
|
|
177
|
+
return this.params.toAddress;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const addresses = await this._getAddresses();
|
|
181
|
+
return addresses[0];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Common validation for asset name
|
|
186
|
+
* @param {string} assetName - Asset name to validate
|
|
187
|
+
* @param {string} type - Expected type ('ROOT', 'SUB', etc.)
|
|
188
|
+
*/
|
|
189
|
+
validateAssetName(assetName, type) {
|
|
190
|
+
if (!assetName) {
|
|
191
|
+
throw new Error('Asset name is required');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
switch (type) {
|
|
195
|
+
case 'ROOT':
|
|
196
|
+
AssetNameValidator.validateRoot(assetName);
|
|
197
|
+
break;
|
|
198
|
+
case 'SUB':
|
|
199
|
+
AssetNameValidator.validateSub(assetName);
|
|
200
|
+
break;
|
|
201
|
+
case 'UNIQUE':
|
|
202
|
+
AssetNameValidator.validateUnique(assetName);
|
|
203
|
+
break;
|
|
204
|
+
case 'QUALIFIER':
|
|
205
|
+
AssetNameValidator.validateQualifier(assetName);
|
|
206
|
+
break;
|
|
207
|
+
case 'RESTRICTED':
|
|
208
|
+
AssetNameValidator.validateRestricted(assetName);
|
|
209
|
+
break;
|
|
210
|
+
default:
|
|
211
|
+
throw new Error(`Unknown asset type: ${type}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Common validation for amount and units
|
|
217
|
+
* @param {number} quantity - Quantity to validate
|
|
218
|
+
* @param {number} units - Units (decimal places)
|
|
219
|
+
*/
|
|
220
|
+
validateAmount(quantity, units) {
|
|
221
|
+
AmountValidator.validate(quantity, units);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Convert amount to satoshis
|
|
226
|
+
* @param {number} amount - Amount in asset units
|
|
227
|
+
* @param {number} units - Decimal places
|
|
228
|
+
* @returns {number} Amount in satoshis
|
|
229
|
+
*/
|
|
230
|
+
toSatoshis(amount, units) {
|
|
231
|
+
return Math.round(amount * Math.pow(10, units));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Convert satoshis to amount
|
|
236
|
+
* @param {number} satoshis - Amount in satoshis
|
|
237
|
+
* @param {number} units - Decimal places
|
|
238
|
+
* @returns {number} Amount in asset units
|
|
239
|
+
*/
|
|
240
|
+
fromSatoshis(satoshis, units) {
|
|
241
|
+
return satoshis / Math.pow(10, units);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Format transaction result
|
|
246
|
+
* @param {string} rawTx - Raw transaction hex
|
|
247
|
+
* @param {Array} utxos - UTXOs used
|
|
248
|
+
* @param {Array} inputs - Transaction inputs
|
|
249
|
+
* @param {object} outputs - Transaction outputs
|
|
250
|
+
* @param {number} fee - Transaction fee
|
|
251
|
+
* @param {number} burnAmount - Burn amount
|
|
252
|
+
* @param {object} extra - Extra information
|
|
253
|
+
* @returns {object} Formatted result
|
|
254
|
+
*/
|
|
255
|
+
formatResult(rawTx, utxos, inputs, outputs, fee, burnAmount, extra = {}) {
|
|
256
|
+
return {
|
|
257
|
+
rawTx,
|
|
258
|
+
utxos,
|
|
259
|
+
inputs,
|
|
260
|
+
outputs,
|
|
261
|
+
fee,
|
|
262
|
+
burnAmount,
|
|
263
|
+
...extra
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Check if asset exists (to prevent creating duplicates)
|
|
269
|
+
* @param {string} assetName - Asset name to check
|
|
270
|
+
* @returns {Promise<boolean>} True if exists
|
|
271
|
+
*/
|
|
272
|
+
async assetExists(assetName) {
|
|
273
|
+
try {
|
|
274
|
+
const assetData = await this.rpc('getassetdata', [assetName]);
|
|
275
|
+
return assetData !== null && assetData !== undefined;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
// If asset doesn't exist, RPC will throw error
|
|
278
|
+
if (error.message && error.message.includes('not found')) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
// Re-throw other errors
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get asset data from blockchain
|
|
288
|
+
* @param {string} assetName - Asset name
|
|
289
|
+
* @returns {Promise<object|null>} Asset data or null if not found
|
|
290
|
+
*/
|
|
291
|
+
async getAssetData(assetName) {
|
|
292
|
+
try {
|
|
293
|
+
return await this.rpc('getassetdata', [assetName]);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (error.message && error.message.includes('not found')) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = BaseAssetTransactionBuilder;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freeze Address Builder
|
|
3
|
+
* Builds transactions for freezing/unfreezing addresses and assets
|
|
4
|
+
*
|
|
5
|
+
* Freeze operations (restricted assets only):
|
|
6
|
+
* - Freeze specific addresses (prevent trading)
|
|
7
|
+
* - Unfreeze specific addresses (allow trading again)
|
|
8
|
+
* - Global asset freeze (freeze entire asset)
|
|
9
|
+
* - Global asset unfreeze (unfreeze entire asset)
|
|
10
|
+
* - Cost: No burn (but requires fee)
|
|
11
|
+
* - Requires restricted asset's owner token ($ASSET!)
|
|
12
|
+
* - Owner token must be returned
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
|
|
16
|
+
const { OutputFormatter, AssetNameParser } = require('../utils');
|
|
17
|
+
const { AssetNotFoundError, OwnerTokenNotFoundError, InvalidAddressError } = require('../errors');
|
|
18
|
+
|
|
19
|
+
class FreezeAddressBuilder extends BaseAssetTransactionBuilder {
|
|
20
|
+
/**
|
|
21
|
+
* Validate freeze/unfreeze parameters
|
|
22
|
+
* @param {object} params - Freeze parameters
|
|
23
|
+
* @param {string} operationType - Operation type
|
|
24
|
+
* @throws {Error} If validation fails
|
|
25
|
+
*/
|
|
26
|
+
validateParams(params, operationType) {
|
|
27
|
+
// Validate required parameters
|
|
28
|
+
if (!params.assetName) {
|
|
29
|
+
throw new Error('assetName is required');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Validate asset name is restricted
|
|
33
|
+
this.validateAssetName(params.assetName, 'RESTRICTED');
|
|
34
|
+
|
|
35
|
+
// For address-specific operations, validate addresses
|
|
36
|
+
if (operationType === 'FREEZE_ADDRESSES' || operationType === 'UNFREEZE_ADDRESSES') {
|
|
37
|
+
if (!params.addresses || !Array.isArray(params.addresses) || params.addresses.length === 0) {
|
|
38
|
+
throw new Error('addresses is required and must be a non-empty array');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Validate each address
|
|
42
|
+
params.addresses.forEach((address, index) => {
|
|
43
|
+
if (!address || typeof address !== 'string') {
|
|
44
|
+
throw new InvalidAddressError(
|
|
45
|
+
`addresses[${index}] must be a non-empty string`,
|
|
46
|
+
address
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Basic address validation
|
|
51
|
+
const validPrefixes = this.network === 'xna' ? ['N'] : ['m', 'n'];
|
|
52
|
+
if (!validPrefixes.some(prefix => address.startsWith(prefix))) {
|
|
53
|
+
throw new InvalidAddressError(
|
|
54
|
+
`addresses[${index}] has invalid prefix for network ${this.network}`,
|
|
55
|
+
address
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build freeze operation transaction
|
|
66
|
+
* @param {string} operationType - Operation type
|
|
67
|
+
* @returns {Promise<object>} Transaction result
|
|
68
|
+
*/
|
|
69
|
+
async buildFreezeOperation(operationType) {
|
|
70
|
+
// 1. Validate parameters
|
|
71
|
+
await this.validateParams(this.params, operationType);
|
|
72
|
+
|
|
73
|
+
const { assetName } = this.params;
|
|
74
|
+
|
|
75
|
+
// 2. Check if asset exists and is restricted
|
|
76
|
+
const assetData = await this.getAssetData(assetName);
|
|
77
|
+
if (!assetData) {
|
|
78
|
+
throw new AssetNotFoundError(
|
|
79
|
+
`Asset ${assetName} does not exist on the blockchain`,
|
|
80
|
+
assetName
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. Get wallet addresses
|
|
85
|
+
const addresses = await this._getAddresses();
|
|
86
|
+
const changeAddress = await this.getChangeAddress();
|
|
87
|
+
|
|
88
|
+
// 4. Find owner token (CRITICAL: must have this)
|
|
89
|
+
const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
|
|
90
|
+
let ownerTokenUTXO;
|
|
91
|
+
try {
|
|
92
|
+
ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
|
|
93
|
+
ownerTokenName,
|
|
94
|
+
addresses
|
|
95
|
+
);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error instanceof OwnerTokenNotFoundError) {
|
|
98
|
+
throw new OwnerTokenNotFoundError(
|
|
99
|
+
`You must own the restricted asset's owner token (${ownerTokenName}) to freeze/unfreeze addresses or the asset.`,
|
|
100
|
+
ownerTokenName
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 5. No burn for freeze operations (only fee)
|
|
107
|
+
const burnAmount = 0;
|
|
108
|
+
|
|
109
|
+
// 6. Estimate fee
|
|
110
|
+
const estimatedFee = await this.estimateFee(2, 3);
|
|
111
|
+
|
|
112
|
+
// 7. Select XNA UTXOs (only for fee, no burn)
|
|
113
|
+
const utxoSelection = await this.selectUTXOs(estimatedFee, null, 0);
|
|
114
|
+
const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
|
|
115
|
+
const totalXNAInput = utxoSelection.totalXNA;
|
|
116
|
+
|
|
117
|
+
// 8. Recalculate fee with actual input count
|
|
118
|
+
const actualInputCount = baseCurrencyUTXOs.length + 1; // +1 for owner token
|
|
119
|
+
const actualFee = await this.estimateFee(actualInputCount, 3);
|
|
120
|
+
|
|
121
|
+
// 9. Verify we have enough XNA for fee
|
|
122
|
+
if (totalXNAInput < actualFee) {
|
|
123
|
+
const additionalNeeded = actualFee - totalXNAInput + 0.001;
|
|
124
|
+
const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
|
|
125
|
+
baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 10. Calculate XNA change
|
|
129
|
+
const finalTotalInput = baseCurrencyUTXOs.reduce(
|
|
130
|
+
(sum, utxo) => sum + utxo.satoshis / 100000000,
|
|
131
|
+
0
|
|
132
|
+
);
|
|
133
|
+
const xnaChange = finalTotalInput - actualFee;
|
|
134
|
+
|
|
135
|
+
// 11. Build inputs (XNA + owner token)
|
|
136
|
+
const inputs = [];
|
|
137
|
+
|
|
138
|
+
// Add XNA inputs
|
|
139
|
+
baseCurrencyUTXOs.forEach(utxo => {
|
|
140
|
+
inputs.push({
|
|
141
|
+
txid: utxo.txid,
|
|
142
|
+
vout: utxo.outputIndex,
|
|
143
|
+
address: utxo.address,
|
|
144
|
+
satoshis: utxo.satoshis
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Add owner token input
|
|
149
|
+
inputs.push({
|
|
150
|
+
txid: ownerTokenUTXO.txid,
|
|
151
|
+
vout: ownerTokenUTXO.outputIndex,
|
|
152
|
+
address: ownerTokenUTXO.address,
|
|
153
|
+
assetName: ownerTokenUTXO.assetName,
|
|
154
|
+
satoshis: ownerTokenUTXO.satoshis
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// 12. Build outputs (ORDER CRITICAL!)
|
|
158
|
+
const outputs = {};
|
|
159
|
+
|
|
160
|
+
// First: XNA change (if any)
|
|
161
|
+
if (xnaChange > 0.00000001) {
|
|
162
|
+
outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
|
|
163
|
+
}
|
|
164
|
+
|
|
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
|
+
// Last: Freeze/Unfreeze operation
|
|
173
|
+
let operationOutput;
|
|
174
|
+
let targetAddresses = [];
|
|
175
|
+
|
|
176
|
+
switch (operationType) {
|
|
177
|
+
case 'FREEZE_ADDRESSES':
|
|
178
|
+
targetAddresses = this.params.addresses;
|
|
179
|
+
operationOutput = OutputFormatter.formatFreezeAddressesOutput({
|
|
180
|
+
asset_name: assetName,
|
|
181
|
+
addresses: targetAddresses
|
|
182
|
+
});
|
|
183
|
+
outputs[targetAddresses[0]] = operationOutput;
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
case 'UNFREEZE_ADDRESSES':
|
|
187
|
+
targetAddresses = this.params.addresses;
|
|
188
|
+
operationOutput = OutputFormatter.formatUnfreezeAddressesOutput({
|
|
189
|
+
asset_name: assetName,
|
|
190
|
+
addresses: targetAddresses
|
|
191
|
+
});
|
|
192
|
+
outputs[targetAddresses[0]] = operationOutput;
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
case 'FREEZE_ASSET':
|
|
196
|
+
operationOutput = OutputFormatter.formatFreezeAssetOutput(assetName);
|
|
197
|
+
outputs[changeAddress] = operationOutput;
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
case 'UNFREEZE_ASSET':
|
|
201
|
+
operationOutput = OutputFormatter.formatUnfreezeAssetOutput(assetName);
|
|
202
|
+
outputs[changeAddress] = operationOutput;
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
default:
|
|
206
|
+
throw new Error(`Unknown freeze operation type: ${operationType}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 13. Order outputs (protocol requirement)
|
|
210
|
+
const orderedOutputs = this.outputOrderer.order(outputs);
|
|
211
|
+
|
|
212
|
+
// 14. Validate owner token is returned (safety check)
|
|
213
|
+
this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
|
|
214
|
+
|
|
215
|
+
// 15. Create raw transaction
|
|
216
|
+
const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
|
|
217
|
+
|
|
218
|
+
// 16. Format and return result
|
|
219
|
+
const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
|
|
220
|
+
|
|
221
|
+
return this.formatResult(
|
|
222
|
+
rawTx,
|
|
223
|
+
allUTXOs,
|
|
224
|
+
inputs,
|
|
225
|
+
orderedOutputs,
|
|
226
|
+
actualFee,
|
|
227
|
+
burnAmount,
|
|
228
|
+
{
|
|
229
|
+
assetName,
|
|
230
|
+
ownerTokenUsed: ownerTokenName,
|
|
231
|
+
targetAddresses: targetAddresses.length > 0 ? targetAddresses : null,
|
|
232
|
+
addressCount: targetAddresses.length,
|
|
233
|
+
operationType
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Build freeze addresses transaction
|
|
240
|
+
* @returns {Promise<object>} Transaction result
|
|
241
|
+
*/
|
|
242
|
+
async build() {
|
|
243
|
+
return this.buildFreezeOperation('FREEZE_ADDRESSES');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Build unfreeze addresses transaction
|
|
248
|
+
* @returns {Promise<object>} Transaction result
|
|
249
|
+
*/
|
|
250
|
+
async buildUnfreeze() {
|
|
251
|
+
return this.buildFreezeOperation('UNFREEZE_ADDRESSES');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Build global freeze asset transaction
|
|
256
|
+
* @returns {Promise<object>} Transaction result
|
|
257
|
+
*/
|
|
258
|
+
async buildGlobalFreeze() {
|
|
259
|
+
return this.buildFreezeOperation('FREEZE_ASSET');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Build global unfreeze asset transaction
|
|
264
|
+
* @returns {Promise<object>} Transaction result
|
|
265
|
+
*/
|
|
266
|
+
async buildGlobalUnfreeze() {
|
|
267
|
+
return this.buildFreezeOperation('UNFREEZE_ASSET');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = FreezeAddressBuilder;
|