@neuraiproject/neurai-assets 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +522 -0
  2. package/examples/01-create-root-asset.js +71 -0
  3. package/examples/02-create-sub-asset.js +79 -0
  4. package/examples/03-create-nfts.js +140 -0
  5. package/examples/04-reissue-asset.js +164 -0
  6. package/examples/05-create-qualifier-and-tag.js +209 -0
  7. package/examples/06-create-restricted-asset.js +223 -0
  8. package/examples/07-freeze-and-unfreeze.js +292 -0
  9. package/examples/08-query-assets.js +332 -0
  10. package/examples/09-wallet-integration.js +320 -0
  11. package/examples/README.md +319 -0
  12. package/package.json +43 -0
  13. package/src/NeuraiAssets.js +468 -0
  14. package/src/builders/BaseAssetTransactionBuilder.js +303 -0
  15. package/src/builders/FreezeAddressBuilder.js +271 -0
  16. package/src/builders/IssueQualifierBuilder.js +251 -0
  17. package/src/builders/IssueRestrictedBuilder.js +187 -0
  18. package/src/builders/IssueRootBuilder.js +173 -0
  19. package/src/builders/IssueSubBuilder.js +237 -0
  20. package/src/builders/IssueUniqueBuilder.js +255 -0
  21. package/src/builders/ReissueBuilder.js +246 -0
  22. package/src/builders/ReissueRestrictedBuilder.js +264 -0
  23. package/src/builders/TagAddressBuilder.js +243 -0
  24. package/src/builders/index.js +38 -0
  25. package/src/constants/assetTypes.js +23 -0
  26. package/src/constants/burnAddresses.js +65 -0
  27. package/src/constants/fees.js +61 -0
  28. package/src/constants/index.js +44 -0
  29. package/src/constants/networks.js +112 -0
  30. package/src/errors/AssetErrors.js +135 -0
  31. package/src/errors/ValidationErrors.js +87 -0
  32. package/src/errors/index.js +56 -0
  33. package/src/index.js +68 -0
  34. package/src/managers/BurnManager.js +222 -0
  35. package/src/managers/OutputOrderer.js +289 -0
  36. package/src/managers/OwnerTokenManager.js +265 -0
  37. package/src/managers/UTXOSelector.js +309 -0
  38. package/src/managers/index.js +16 -0
  39. package/src/queries/AssetQueries.js +447 -0
  40. package/src/queries/index.js +10 -0
  41. package/src/utils/amountConverter.js +115 -0
  42. package/src/utils/assetNameParser.js +203 -0
  43. package/src/utils/index.js +16 -0
  44. package/src/utils/networkDetector.js +144 -0
  45. package/src/utils/outputFormatter.js +292 -0
  46. package/src/validators/amountValidator.js +149 -0
  47. package/src/validators/assetNameValidator.js +296 -0
  48. package/src/validators/index.js +16 -0
  49. package/src/validators/ipfsValidator.js +101 -0
  50. package/src/validators/verifierValidator.js +146 -0
  51. package/tests/README.md +126 -0
  52. package/tests/integration/assetLifecycle.test.js +244 -0
  53. package/tests/mocks/rpcMock.js +156 -0
  54. package/tests/unit/NeuraiAssets.test.js +217 -0
  55. package/tests/unit/utils/amountConverter.test.js +171 -0
  56. package/tests/unit/utils/assetNameParser.test.js +203 -0
  57. package/tests/unit/validators/amountValidator.test.js +143 -0
  58. package/tests/unit/validators/assetNameValidator.test.js +228 -0
@@ -0,0 +1,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;