@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,447 @@
1
+ /**
2
+ * Asset Queries
3
+ * Wrapper methods for querying asset information from the blockchain
4
+ *
5
+ * Provides convenient access to all asset-related RPC query methods:
6
+ * - Asset metadata (getassetdata)
7
+ * - Asset listings (listassets, listmyassets)
8
+ * - Holder information (listaddressesbyasset)
9
+ * - Balance queries (listassetbalancesbyaddress)
10
+ * - Qualifier checks (checkaddresstag, listtagsforaddress)
11
+ * - Restriction checks (checkaddressrestriction, checkglobalrestriction)
12
+ * - Verifier validation (isvalidverifierstring)
13
+ */
14
+
15
+ const { AssetNotFoundError, InvalidAddressError } = require('../errors');
16
+
17
+ class AssetQueries {
18
+ /**
19
+ * @param {Function} rpc - RPC function to call Neurai node
20
+ */
21
+ constructor(rpc) {
22
+ if (!rpc || typeof rpc !== 'function') {
23
+ throw new Error('RPC function is required');
24
+ }
25
+ this.rpc = rpc;
26
+ }
27
+
28
+ /**
29
+ * Get asset metadata
30
+ * @param {string} assetName - Asset name
31
+ * @returns {Promise<object>} Asset data
32
+ * @throws {AssetNotFoundError} If asset doesn't exist
33
+ */
34
+ async getAssetData(assetName) {
35
+ if (!assetName) {
36
+ throw new Error('Asset name is required');
37
+ }
38
+
39
+ try {
40
+ const assetData = await this.rpc('getassetdata', [assetName]);
41
+
42
+ if (!assetData) {
43
+ throw new AssetNotFoundError(
44
+ `Asset ${assetName} not found on blockchain`,
45
+ assetName
46
+ );
47
+ }
48
+
49
+ return assetData;
50
+ } catch (error) {
51
+ if (error instanceof AssetNotFoundError) {
52
+ throw error;
53
+ }
54
+
55
+ // RPC error - likely asset doesn't exist
56
+ if (error.message && error.message.includes('not found')) {
57
+ throw new AssetNotFoundError(
58
+ `Asset ${assetName} not found on blockchain`,
59
+ assetName
60
+ );
61
+ }
62
+
63
+ throw new Error(`Failed to get asset data: ${error.message}`);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * List all assets on the blockchain
69
+ * @param {string} filter - Filter pattern (e.g., 'MY*' for all assets starting with MY)
70
+ * @param {boolean} verbose - Include detailed information
71
+ * @param {number} count - Maximum number to return
72
+ * @param {number} start - Starting index (for pagination)
73
+ * @returns {Promise<Array|object>} Array of asset names or detailed objects
74
+ */
75
+ async listAssets(filter = '*', verbose = false, count = 100, start = 0) {
76
+ try {
77
+ const assets = await this.rpc('listassets', [filter, verbose, count, start]);
78
+ return assets || [];
79
+ } catch (error) {
80
+ throw new Error(`Failed to list assets: ${error.message}`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * List assets owned by wallet
86
+ * @param {string} assetName - Filter by asset name (default: '*' for all)
87
+ * @param {boolean} verbose - Include detailed information
88
+ * @param {number} count - Maximum number to return
89
+ * @param {number} start - Starting index (for pagination)
90
+ * @param {number} confs - Minimum confirmations (default: 1)
91
+ * @returns {Promise<object>} Object with asset names as keys and amounts as values
92
+ */
93
+ async listMyAssets(assetName = '*', verbose = false, count = 100, start = 0, confs = 1) {
94
+ try {
95
+ const myAssets = await this.rpc('listmyassets', [assetName, verbose, count, start, confs]);
96
+ return myAssets || {};
97
+ } catch (error) {
98
+ throw new Error(`Failed to list my assets: ${error.message}`);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * List all addresses holding a specific asset
104
+ * @param {string} assetName - Asset name
105
+ * @param {boolean} onlyCount - Return only count instead of full list
106
+ * @param {number} count - Maximum number to return
107
+ * @param {number} start - Starting index (for pagination)
108
+ * @returns {Promise<Array|number>} Array of {address, amount} or count
109
+ */
110
+ async listAddressesByAsset(assetName, onlyCount = false, count = 100, start = 0) {
111
+ if (!assetName) {
112
+ throw new Error('Asset name is required');
113
+ }
114
+
115
+ try {
116
+ const result = await this.rpc('listaddressesbyasset', [assetName, onlyCount, count, start]);
117
+ return result || (onlyCount ? 0 : []);
118
+ } catch (error) {
119
+ if (error.message && error.message.includes('not found')) {
120
+ throw new AssetNotFoundError(
121
+ `Asset ${assetName} not found on blockchain`,
122
+ assetName
123
+ );
124
+ }
125
+ throw new Error(`Failed to list addresses by asset: ${error.message}`);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * List asset balances for a specific address
131
+ * @param {string} address - Address to query
132
+ * @param {boolean} onlyTotal - Return only count instead of full list
133
+ * @param {number} count - Maximum number to return
134
+ * @param {number} start - Starting index (for pagination)
135
+ * @returns {Promise<Array|number>} Array of {asset, amount} or count
136
+ */
137
+ async listAssetBalancesByAddress(address, onlyTotal = false, count = 100, start = 0) {
138
+ if (!address) {
139
+ throw new Error('Address is required');
140
+ }
141
+
142
+ try {
143
+ const result = await this.rpc('listassetbalancesbyaddress', [address, onlyTotal, count, start]);
144
+ return result || (onlyTotal ? 0 : []);
145
+ } catch (error) {
146
+ throw new Error(`Failed to list asset balances by address: ${error.message}`);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Check if an address has a specific qualifier tag
152
+ * @param {string} address - Address to check
153
+ * @param {string} qualifierName - Qualifier name (e.g., '#KYC_VERIFIED')
154
+ * @returns {Promise<boolean>} True if address has the tag
155
+ */
156
+ async checkAddressTag(address, qualifierName) {
157
+ if (!address) {
158
+ throw new Error('Address is required');
159
+ }
160
+
161
+ if (!qualifierName) {
162
+ throw new Error('Qualifier name is required');
163
+ }
164
+
165
+ try {
166
+ const result = await this.rpc('checkaddresstag', [address, qualifierName]);
167
+ return result === true || result === 1;
168
+ } catch (error) {
169
+ // If tag doesn't exist or address doesn't have it, return false
170
+ if (error.message && (error.message.includes('not found') || error.message.includes('does not have'))) {
171
+ return false;
172
+ }
173
+ throw new Error(`Failed to check address tag: ${error.message}`);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * List all qualifiers assigned to an address
179
+ * @param {string} address - Address to query
180
+ * @returns {Promise<Array>} Array of qualifier names
181
+ */
182
+ async listTagsForAddress(address) {
183
+ if (!address) {
184
+ throw new Error('Address is required');
185
+ }
186
+
187
+ try {
188
+ const tags = await this.rpc('listtagsforaddress', [address]);
189
+ return tags || [];
190
+ } catch (error) {
191
+ // If no tags found, return empty array
192
+ if (error.message && error.message.includes('not found')) {
193
+ return [];
194
+ }
195
+ throw new Error(`Failed to list tags for address: ${error.message}`);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * List all addresses with a specific qualifier tag
201
+ * @param {string} qualifierName - Qualifier name
202
+ * @returns {Promise<Array>} Array of addresses
203
+ */
204
+ async listAddressesForTag(qualifierName) {
205
+ if (!qualifierName) {
206
+ throw new Error('Qualifier name is required');
207
+ }
208
+
209
+ try {
210
+ const addresses = await this.rpc('listaddressesfortag', [qualifierName]);
211
+ return addresses || [];
212
+ } catch (error) {
213
+ if (error.message && error.message.includes('not found')) {
214
+ throw new AssetNotFoundError(
215
+ `Qualifier ${qualifierName} not found on blockchain`,
216
+ qualifierName
217
+ );
218
+ }
219
+ throw new Error(`Failed to list addresses for tag: ${error.message}`);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Check if an address can hold a restricted asset
225
+ * @param {string} address - Address to check
226
+ * @param {string} restrictedAssetName - Restricted asset name (e.g., '$SECURITY')
227
+ * @returns {Promise<boolean>} True if address meets verifier requirements
228
+ */
229
+ async checkAddressRestriction(address, restrictedAssetName) {
230
+ if (!address) {
231
+ throw new Error('Address is required');
232
+ }
233
+
234
+ if (!restrictedAssetName) {
235
+ throw new Error('Restricted asset name is required');
236
+ }
237
+
238
+ try {
239
+ const result = await this.rpc('checkaddressrestriction', [address, restrictedAssetName]);
240
+ return result === true || result === 1;
241
+ } catch (error) {
242
+ // If address doesn't meet requirements, return false
243
+ if (error.message && (error.message.includes('not found') || error.message.includes('does not meet'))) {
244
+ return false;
245
+ }
246
+ throw new Error(`Failed to check address restriction: ${error.message}`);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Check if an address is frozen for a restricted asset
252
+ * @param {string} address - Address to check
253
+ * @param {string} restrictedAssetName - Restricted asset name
254
+ * @returns {Promise<boolean>} True if address is frozen
255
+ */
256
+ async isAddressFrozen(address, restrictedAssetName) {
257
+ if (!address) {
258
+ throw new Error('Address is required');
259
+ }
260
+
261
+ if (!restrictedAssetName) {
262
+ throw new Error('Restricted asset name is required');
263
+ }
264
+
265
+ try {
266
+ const result = await this.rpc('checkaddressrestriction', [address, restrictedAssetName]);
267
+ // If result has frozen property, check it
268
+ if (typeof result === 'object' && result.frozen !== undefined) {
269
+ return result.frozen === true || result.frozen === 1;
270
+ }
271
+ return false;
272
+ } catch (error) {
273
+ throw new Error(`Failed to check if address is frozen: ${error.message}`);
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Check if an asset is globally frozen
279
+ * @param {string} restrictedAssetName - Restricted asset name
280
+ * @returns {Promise<boolean>} True if asset is globally frozen
281
+ */
282
+ async checkGlobalRestriction(restrictedAssetName) {
283
+ if (!restrictedAssetName) {
284
+ throw new Error('Restricted asset name is required');
285
+ }
286
+
287
+ try {
288
+ const result = await this.rpc('checkglobalrestriction', [restrictedAssetName]);
289
+ return result === true || result === 1;
290
+ } catch (error) {
291
+ if (error.message && error.message.includes('not found')) {
292
+ throw new AssetNotFoundError(
293
+ `Restricted asset ${restrictedAssetName} not found on blockchain`,
294
+ restrictedAssetName
295
+ );
296
+ }
297
+ throw new Error(`Failed to check global restriction: ${error.message}`);
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Get verifier string for a restricted asset
303
+ * @param {string} restrictedAssetName - Restricted asset name
304
+ * @returns {Promise<string>} Verifier string
305
+ */
306
+ async getVerifierString(restrictedAssetName) {
307
+ if (!restrictedAssetName) {
308
+ throw new Error('Restricted asset name is required');
309
+ }
310
+
311
+ try {
312
+ const result = await this.rpc('getverifierstring', [restrictedAssetName]);
313
+ return result || '';
314
+ } catch (error) {
315
+ if (error.message && error.message.includes('not found')) {
316
+ throw new AssetNotFoundError(
317
+ `Restricted asset ${restrictedAssetName} not found on blockchain`,
318
+ restrictedAssetName
319
+ );
320
+ }
321
+ throw new Error(`Failed to get verifier string: ${error.message}`);
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Validate verifier string syntax
327
+ * @param {string} verifierString - Verifier string to validate
328
+ * @returns {Promise<boolean>} True if valid
329
+ */
330
+ async isValidVerifierString(verifierString) {
331
+ if (!verifierString) {
332
+ throw new Error('Verifier string is required');
333
+ }
334
+
335
+ try {
336
+ const result = await this.rpc('isvalidverifierstring', [verifierString]);
337
+ return result === true || result === 1;
338
+ } catch (error) {
339
+ // If validation fails, return false
340
+ return false;
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Get snapshot of asset ownership at a specific block
346
+ * @param {string} assetName - Asset name
347
+ * @param {number} blockHeight - Block height for snapshot
348
+ * @returns {Promise<object>} Snapshot request result
349
+ */
350
+ async getSnapshotRequest(assetName, blockHeight) {
351
+ if (!assetName) {
352
+ throw new Error('Asset name is required');
353
+ }
354
+
355
+ if (!blockHeight || typeof blockHeight !== 'number') {
356
+ throw new Error('Block height must be a number');
357
+ }
358
+
359
+ try {
360
+ const result = await this.rpc('getsnapshotrequest', [assetName, blockHeight]);
361
+ return result;
362
+ } catch (error) {
363
+ throw new Error(`Failed to get snapshot request: ${error.message}`);
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Cancel a snapshot request
369
+ * @param {string} assetName - Asset name
370
+ * @param {number} blockHeight - Block height of snapshot to cancel
371
+ * @returns {Promise<boolean>} True if cancelled successfully
372
+ */
373
+ async cancelSnapshotRequest(assetName, blockHeight) {
374
+ if (!assetName) {
375
+ throw new Error('Asset name is required');
376
+ }
377
+
378
+ if (!blockHeight || typeof blockHeight !== 'number') {
379
+ throw new Error('Block height must be a number');
380
+ }
381
+
382
+ try {
383
+ const result = await this.rpc('cancelsnapshotrequest', [assetName, blockHeight]);
384
+ return result === true || result === 1;
385
+ } catch (error) {
386
+ throw new Error(`Failed to cancel snapshot request: ${error.message}`);
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Get total count of assets on blockchain
392
+ * @returns {Promise<number>} Total asset count
393
+ */
394
+ async getAssetCount() {
395
+ try {
396
+ // List all assets with count only
397
+ const assets = await this.listAssets('*', false, 1, 0);
398
+ return Array.isArray(assets) ? assets.length : 0;
399
+ } catch (error) {
400
+ throw new Error(`Failed to get asset count: ${error.message}`);
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Check if asset exists
406
+ * @param {string} assetName - Asset name
407
+ * @returns {Promise<boolean>} True if asset exists
408
+ */
409
+ async assetExists(assetName) {
410
+ try {
411
+ await this.getAssetData(assetName);
412
+ return true;
413
+ } catch (error) {
414
+ if (error instanceof AssetNotFoundError) {
415
+ return false;
416
+ }
417
+ throw error;
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Get asset type from name
423
+ * @param {string} assetName - Asset name
424
+ * @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'OWNER')
425
+ */
426
+ getAssetType(assetName) {
427
+ if (!assetName) {
428
+ throw new Error('Asset name is required');
429
+ }
430
+
431
+ if (assetName.endsWith('!')) {
432
+ return 'OWNER';
433
+ } else if (assetName.startsWith('#')) {
434
+ return assetName.includes('/') ? 'SUB_QUALIFIER' : 'QUALIFIER';
435
+ } else if (assetName.startsWith('$')) {
436
+ return 'RESTRICTED';
437
+ } else if (assetName.includes('#')) {
438
+ return 'UNIQUE';
439
+ } else if (assetName.includes('/')) {
440
+ return 'SUB';
441
+ } else {
442
+ return 'ROOT';
443
+ }
444
+ }
445
+ }
446
+
447
+ module.exports = AssetQueries;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Query Module
3
+ * Exports all asset query functionality
4
+ */
5
+
6
+ const AssetQueries = require('./AssetQueries');
7
+
8
+ module.exports = {
9
+ AssetQueries
10
+ };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Amount Converter
3
+ * Converts between user amounts and satoshis (protocol internal format)
4
+ */
5
+
6
+ class AmountConverter {
7
+ /**
8
+ * Convert user amount to satoshis
9
+ * @param {number} amount - User-friendly amount (e.g., 1.5)
10
+ * @param {number} units - Decimal places (0-8)
11
+ * @returns {number} Amount in satoshis
12
+ */
13
+ static toSatoshis(amount, units) {
14
+ if (typeof amount !== 'number' || isNaN(amount)) {
15
+ throw new Error('Amount must be a valid number');
16
+ }
17
+
18
+ if (typeof units !== 'number' || isNaN(units) || units < 0 || units > 8) {
19
+ throw new Error('Units must be a number between 0 and 8');
20
+ }
21
+
22
+ // Calculate multiplier
23
+ const multiplier = Math.pow(10, units);
24
+
25
+ // Convert to satoshis and round to avoid floating point issues
26
+ const satoshis = Math.round(amount * multiplier);
27
+
28
+ return satoshis;
29
+ }
30
+
31
+ /**
32
+ * Convert satoshis to user amount
33
+ * @param {number} satoshis - Amount in satoshis
34
+ * @param {number} units - Decimal places (0-8)
35
+ * @returns {number} User-friendly amount
36
+ */
37
+ static fromSatoshis(satoshis, units) {
38
+ if (typeof satoshis !== 'number' || isNaN(satoshis)) {
39
+ throw new Error('Satoshis must be a valid number');
40
+ }
41
+
42
+ if (typeof units !== 'number' || isNaN(units) || units < 0 || units > 8) {
43
+ throw new Error('Units must be a number between 0 and 8');
44
+ }
45
+
46
+ // Calculate divisor
47
+ const divisor = Math.pow(10, units);
48
+
49
+ // Convert to user amount
50
+ const amount = satoshis / divisor;
51
+
52
+ return amount;
53
+ }
54
+
55
+ /**
56
+ * Format amount with proper decimal places
57
+ * @param {number} amount - Amount to format
58
+ * @param {number} units - Decimal places
59
+ * @returns {string} Formatted amount
60
+ */
61
+ static format(amount, units) {
62
+ if (units === 0) {
63
+ return amount.toString();
64
+ }
65
+
66
+ return amount.toFixed(units);
67
+ }
68
+
69
+ /**
70
+ * Parse formatted amount string
71
+ * @param {string} formattedAmount - Formatted amount string
72
+ * @returns {number} Parsed amount
73
+ */
74
+ static parse(formattedAmount) {
75
+ const num = parseFloat(formattedAmount);
76
+ if (isNaN(num)) {
77
+ throw new Error('Invalid number format');
78
+ }
79
+ return num;
80
+ }
81
+
82
+ /**
83
+ * Get decimal places from amount
84
+ * @param {number} amount - Amount to check
85
+ * @returns {number} Number of decimal places
86
+ */
87
+ static getDecimalPlaces(amount) {
88
+ const match = ('' + amount).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
89
+ if (!match) return 0;
90
+ return Math.max(
91
+ 0,
92
+ (match[1] ? match[1].length : 0) - (match[2] ? +match[2] : 0)
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Adjust amount to proper units
98
+ * If amount has more decimals than units allow, round it
99
+ * @param {number} amount - Amount to adjust
100
+ * @param {number} units - Target decimal places
101
+ * @returns {number} Adjusted amount
102
+ */
103
+ static adjustToUnits(amount, units) {
104
+ const decimalPlaces = this.getDecimalPlaces(amount);
105
+ if (decimalPlaces <= units) {
106
+ return amount;
107
+ }
108
+
109
+ // Round to units decimal places
110
+ const multiplier = Math.pow(10, units);
111
+ return Math.round(amount * multiplier) / multiplier;
112
+ }
113
+ }
114
+
115
+ module.exports = AmountConverter;