@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,289 @@
1
+ /**
2
+ * Output Orderer
3
+ * Orders transaction outputs according to Neurai protocol requirements
4
+ *
5
+ * CRITICAL: Output ordering is mandatory for asset transactions
6
+ *
7
+ * Correct order:
8
+ * 1. All XNA outputs (burn addresses + change) FIRST
9
+ * 2. Owner token change outputs SECOND
10
+ * 3. Asset operations (issue, reissue, transfer, etc.) LAST
11
+ *
12
+ * Incorrect ordering will cause transaction rejection by the network.
13
+ */
14
+
15
+ const { AssetNameParser } = require('../utils');
16
+
17
+ class OutputOrderer {
18
+ /**
19
+ * Order outputs according to protocol requirements
20
+ * @param {object} outputs - Unordered outputs object
21
+ * @returns {object} Ordered outputs object
22
+ */
23
+ order(outputs) {
24
+ if (!outputs || typeof outputs !== 'object') {
25
+ throw new Error('Outputs must be an object');
26
+ }
27
+
28
+ // Categorize outputs
29
+ const xnaOutputs = []; // XNA amounts (burn + change)
30
+ const ownerOutputs = []; // Owner token transfers
31
+ const assetOutputs = []; // Asset operations and transfers
32
+
33
+ for (const [address, value] of Object.entries(outputs)) {
34
+ if (typeof value === 'number') {
35
+ // XNA output (numeric value)
36
+ xnaOutputs.push({ address, value, order: 1 });
37
+ } else if (typeof value === 'object') {
38
+ // Complex output (transfer or operation)
39
+ if (value.transfer) {
40
+ // Check if it's an owner token transfer
41
+ if (this.isOwnerTokenTransfer(value.transfer)) {
42
+ ownerOutputs.push({ address, value, order: 2 });
43
+ } else {
44
+ // Regular asset transfer
45
+ assetOutputs.push({ address, value, order: 3 });
46
+ }
47
+ } else {
48
+ // Asset operation (issue, reissue, etc.)
49
+ assetOutputs.push({ address, value, order: 3 });
50
+ }
51
+ }
52
+ }
53
+
54
+ // Build ordered output object
55
+ const orderedOutputs = {};
56
+
57
+ // 1. Add XNA outputs first
58
+ xnaOutputs.forEach(({ address, value }) => {
59
+ orderedOutputs[address] = value;
60
+ });
61
+
62
+ // 2. Add owner token outputs second
63
+ ownerOutputs.forEach(({ address, value }) => {
64
+ orderedOutputs[address] = value;
65
+ });
66
+
67
+ // 3. Add asset operations/transfers last
68
+ assetOutputs.forEach(({ address, value }) => {
69
+ orderedOutputs[address] = value;
70
+ });
71
+
72
+ return orderedOutputs;
73
+ }
74
+
75
+ /**
76
+ * Check if a transfer output contains an owner token
77
+ * @param {object} transfer - Transfer object
78
+ * @returns {boolean} True if contains owner token
79
+ */
80
+ isOwnerTokenTransfer(transfer) {
81
+ if (!transfer || typeof transfer !== 'object') {
82
+ return false;
83
+ }
84
+
85
+ // Check if any asset name in transfer ends with '!'
86
+ return Object.keys(transfer).some(assetName => {
87
+ return AssetNameParser.isOwnerToken(assetName);
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Validate output ordering
93
+ * Ensures outputs are in correct order
94
+ *
95
+ * @param {object} outputs - Outputs to validate
96
+ * @returns {boolean} True if valid
97
+ * @throws {Error} If ordering is invalid
98
+ */
99
+ validateOrdering(outputs) {
100
+ const entries = Object.entries(outputs);
101
+ let currentCategory = 0; // 0 = not started, 1 = XNA, 2 = owner, 3 = assets
102
+
103
+ for (const [address, value] of entries) {
104
+ let category;
105
+
106
+ if (typeof value === 'number') {
107
+ category = 1; // XNA
108
+ } else if (value.transfer && this.isOwnerTokenTransfer(value.transfer)) {
109
+ category = 2; // Owner token
110
+ } else {
111
+ category = 3; // Asset operation/transfer
112
+ }
113
+
114
+ // Check if we're going backwards in category order
115
+ if (category < currentCategory) {
116
+ throw new Error(
117
+ 'Invalid output ordering. Outputs must be ordered as: ' +
118
+ '1) XNA outputs, 2) Owner token outputs, 3) Asset operations'
119
+ );
120
+ }
121
+
122
+ currentCategory = category;
123
+ }
124
+
125
+ return true;
126
+ }
127
+
128
+ /**
129
+ * Get output category for debugging
130
+ * @param {*} value - Output value
131
+ * @returns {string} Category name
132
+ */
133
+ getOutputCategory(value) {
134
+ if (typeof value === 'number') {
135
+ return 'XNA';
136
+ } else if (value.transfer && this.isOwnerTokenTransfer(value.transfer)) {
137
+ return 'OWNER_TOKEN';
138
+ } else if (value.transfer) {
139
+ return 'ASSET_TRANSFER';
140
+ } else if (value.issue) {
141
+ return 'ISSUE';
142
+ } else if (value.issue_unique) {
143
+ return 'ISSUE_UNIQUE';
144
+ } else if (value.issue_restricted) {
145
+ return 'ISSUE_RESTRICTED';
146
+ } else if (value.issue_qualifier) {
147
+ return 'ISSUE_QUALIFIER';
148
+ } else if (value.reissue) {
149
+ return 'REISSUE';
150
+ } else if (value.reissue_restricted) {
151
+ return 'REISSUE_RESTRICTED';
152
+ } else if (value.tag_addresses) {
153
+ return 'TAG_ADDRESSES';
154
+ } else if (value.untag_addresses) {
155
+ return 'UNTAG_ADDRESSES';
156
+ } else if (value.freeze_addresses) {
157
+ return 'FREEZE_ADDRESSES';
158
+ } else if (value.unfreeze_addresses) {
159
+ return 'UNFREEZE_ADDRESSES';
160
+ } else if (value.freeze_asset) {
161
+ return 'FREEZE_ASSET';
162
+ } else if (value.unfreeze_asset) {
163
+ return 'UNFREEZE_ASSET';
164
+ } else {
165
+ return 'UNKNOWN';
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Debug output ordering
171
+ * Returns detailed information about output categories
172
+ *
173
+ * @param {object} outputs - Outputs to analyze
174
+ * @returns {Array} Array of { address, category, order }
175
+ */
176
+ debugOrdering(outputs) {
177
+ const debug = [];
178
+
179
+ for (const [address, value] of Object.entries(outputs)) {
180
+ const category = this.getOutputCategory(value);
181
+ let order;
182
+
183
+ if (typeof value === 'number') {
184
+ order = 1;
185
+ } else if (value.transfer && this.isOwnerTokenTransfer(value.transfer)) {
186
+ order = 2;
187
+ } else {
188
+ order = 3;
189
+ }
190
+
191
+ debug.push({
192
+ address,
193
+ category,
194
+ order,
195
+ value
196
+ });
197
+ }
198
+
199
+ return debug;
200
+ }
201
+
202
+ /**
203
+ * Merge multiple output objects with proper ordering
204
+ * Useful when building outputs from multiple sources
205
+ *
206
+ * @param {...object} outputObjects - Multiple output objects to merge
207
+ * @returns {object} Merged and ordered outputs
208
+ */
209
+ merge(...outputObjects) {
210
+ const merged = {};
211
+
212
+ // Merge all objects
213
+ for (const outputs of outputObjects) {
214
+ if (outputs && typeof outputs === 'object') {
215
+ Object.assign(merged, outputs);
216
+ }
217
+ }
218
+
219
+ // Order the merged result
220
+ return this.order(merged);
221
+ }
222
+
223
+ /**
224
+ * Add XNA output (convenience method)
225
+ * @param {object} outputs - Existing outputs
226
+ * @param {string} address - Address
227
+ * @param {number} amount - XNA amount
228
+ * @returns {object} Updated outputs (not ordered yet)
229
+ */
230
+ addXNAOutput(outputs, address, amount) {
231
+ return {
232
+ ...outputs,
233
+ [address]: amount
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Add owner token output (convenience method)
239
+ * @param {object} outputs - Existing outputs
240
+ * @param {string} address - Address
241
+ * @param {string} ownerTokenName - Owner token name
242
+ * @returns {object} Updated outputs (not ordered yet)
243
+ */
244
+ addOwnerTokenOutput(outputs, address, ownerTokenName) {
245
+ return {
246
+ ...outputs,
247
+ [address]: {
248
+ transfer: {
249
+ [ownerTokenName]: 1.0
250
+ }
251
+ }
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Add asset transfer output (convenience method)
257
+ * @param {object} outputs - Existing outputs
258
+ * @param {string} address - Address
259
+ * @param {string} assetName - Asset name
260
+ * @param {number} amount - Amount
261
+ * @returns {object} Updated outputs (not ordered yet)
262
+ */
263
+ addAssetTransferOutput(outputs, address, assetName, amount) {
264
+ return {
265
+ ...outputs,
266
+ [address]: {
267
+ transfer: {
268
+ [assetName]: amount
269
+ }
270
+ }
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Add asset operation output (convenience method)
276
+ * @param {object} outputs - Existing outputs
277
+ * @param {string} address - Address
278
+ * @param {object} operation - Operation object (issue, reissue, etc.)
279
+ * @returns {object} Updated outputs (not ordered yet)
280
+ */
281
+ addOperationOutput(outputs, address, operation) {
282
+ return {
283
+ ...outputs,
284
+ [address]: operation
285
+ };
286
+ }
287
+ }
288
+
289
+ module.exports = OutputOrderer;
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Owner Token Manager
3
+ * CRITICAL: Manages owner token UTXOs and ensures they are properly returned
4
+ *
5
+ * Owner tokens (ASSET!) are required for:
6
+ * - Reissuing assets
7
+ * - Creating sub-assets
8
+ * - Managing restricted assets (freeze/unfreeze)
9
+ *
10
+ * WARNING: If an owner token is not included in transaction outputs,
11
+ * it will be PERMANENTLY LOST and the asset can never be reissued or managed.
12
+ */
13
+
14
+ const { AssetNameParser } = require('../utils');
15
+ const {
16
+ OwnerTokenNotFoundError,
17
+ OwnerTokenNotReturnedError,
18
+ AssetError
19
+ } = require('../errors');
20
+
21
+ class OwnerTokenManager {
22
+ /**
23
+ * @param {Function} rpc - RPC function to call Neurai node
24
+ */
25
+ constructor(rpc) {
26
+ if (!rpc || typeof rpc !== 'function') {
27
+ throw new Error('RPC function is required');
28
+ }
29
+ this.rpc = rpc;
30
+ }
31
+
32
+ /**
33
+ * Find owner token UTXO in wallet addresses
34
+ * @param {string} ownerTokenName - Owner token name (e.g., 'MYTOKEN!')
35
+ * @param {string[]} addresses - Array of wallet addresses
36
+ * @returns {Promise<object>} Owner token UTXO
37
+ * @throws {OwnerTokenNotFoundError} If owner token not found
38
+ */
39
+ async findOwnerTokenUTXO(ownerTokenName, addresses) {
40
+ if (!ownerTokenName || !ownerTokenName.endsWith('!')) {
41
+ throw new Error('Owner token name must end with !');
42
+ }
43
+
44
+ if (!Array.isArray(addresses) || addresses.length === 0) {
45
+ throw new Error('Addresses array is required');
46
+ }
47
+
48
+ try {
49
+ // Query UTXOs for the owner token
50
+ const utxos = await this.rpc('getaddressutxos', [{ addresses }]);
51
+
52
+ // Filter for the specific owner token
53
+ const ownerTokenUTXOs = utxos.filter(utxo => utxo.assetName === ownerTokenName);
54
+
55
+ if (ownerTokenUTXOs.length === 0) {
56
+ throw new OwnerTokenNotFoundError(
57
+ `Owner token ${ownerTokenName} not found in wallet addresses. ` +
58
+ `You must own the owner token to perform this operation.`,
59
+ ownerTokenName
60
+ );
61
+ }
62
+
63
+ // Owner tokens should be indivisible (only 1 UTXO typically)
64
+ // But if split, return the first one found
65
+ return ownerTokenUTXOs[0];
66
+ } catch (error) {
67
+ if (error instanceof OwnerTokenNotFoundError) {
68
+ throw error;
69
+ }
70
+
71
+ throw new AssetError(
72
+ `Failed to find owner token ${ownerTokenName}: ${error.message}`
73
+ );
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Find owner token UTXO by base asset name
79
+ * @param {string} assetName - Base asset name (without !)
80
+ * @param {string[]} addresses - Array of wallet addresses
81
+ * @returns {Promise<object>} Owner token UTXO
82
+ */
83
+ async findOwnerTokenByAssetName(assetName, addresses) {
84
+ const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
85
+ return this.findOwnerTokenUTXO(ownerTokenName, addresses);
86
+ }
87
+
88
+ /**
89
+ * Create owner token return output
90
+ * Owner tokens must always be returned to an address or they're lost forever
91
+ *
92
+ * @param {string} ownerTokenName - Owner token name (e.g., 'MYTOKEN!')
93
+ * @param {string} returnAddress - Address to return owner token to
94
+ * @returns {object} Output object for owner token transfer
95
+ */
96
+ createOwnerTokenReturnOutput(ownerTokenName, returnAddress) {
97
+ if (!ownerTokenName || !ownerTokenName.endsWith('!')) {
98
+ throw new Error('Owner token name must end with !');
99
+ }
100
+
101
+ if (!returnAddress) {
102
+ throw new Error('Return address is required');
103
+ }
104
+
105
+ // Owner tokens are always exactly 1.0
106
+ return {
107
+ [returnAddress]: {
108
+ transfer: {
109
+ [ownerTokenName]: 1.0
110
+ }
111
+ }
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Validate that owner token inputs are properly returned in outputs
117
+ * CRITICAL: This prevents permanent loss of owner tokens
118
+ *
119
+ * @param {Array} inputs - Transaction inputs
120
+ * @param {object} outputs - Transaction outputs
121
+ * @returns {boolean} True if valid
122
+ * @throws {OwnerTokenNotReturnedError} If owner token not returned
123
+ */
124
+ validateOwnerTokenReturn(inputs, outputs) {
125
+ // Find all owner token inputs
126
+ const ownerTokenInputs = inputs.filter(input => {
127
+ return input.assetName && input.assetName.endsWith('!');
128
+ });
129
+
130
+ // If no owner tokens in inputs, validation passes
131
+ if (ownerTokenInputs.length === 0) {
132
+ return true;
133
+ }
134
+
135
+ // Check each owner token is in outputs
136
+ for (const ownerInput of ownerTokenInputs) {
137
+ const ownerTokenName = ownerInput.assetName;
138
+ let foundInOutputs = false;
139
+
140
+ // Check all outputs for owner token
141
+ for (const [address, output] of Object.entries(outputs)) {
142
+ // Check if output has transfer field
143
+ if (output && typeof output === 'object' && output.transfer) {
144
+ // Check if owner token is in the transfer
145
+ if (output.transfer[ownerTokenName]) {
146
+ foundInOutputs = true;
147
+ break;
148
+ }
149
+ }
150
+ }
151
+
152
+ if (!foundInOutputs) {
153
+ throw new OwnerTokenNotReturnedError(
154
+ `CRITICAL: Owner token ${ownerTokenName} is not returned in outputs! ` +
155
+ `This will result in PERMANENT LOSS of the owner token and you will ` +
156
+ `never be able to reissue or manage this asset again. ` +
157
+ `The owner token MUST be included in the transaction outputs.`,
158
+ ownerTokenName
159
+ );
160
+ }
161
+ }
162
+
163
+ return true;
164
+ }
165
+
166
+ /**
167
+ * Check if wallet owns an owner token
168
+ * @param {string} ownerTokenName - Owner token name (e.g., 'MYTOKEN!')
169
+ * @param {string[]} addresses - Array of wallet addresses
170
+ * @returns {Promise<boolean>} True if wallet owns the owner token
171
+ */
172
+ async hasOwnerToken(ownerTokenName, addresses) {
173
+ try {
174
+ await this.findOwnerTokenUTXO(ownerTokenName, addresses);
175
+ return true;
176
+ } catch (error) {
177
+ if (error instanceof OwnerTokenNotFoundError) {
178
+ return false;
179
+ }
180
+ throw error;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Get all owner tokens in wallet
186
+ * @param {string[]} addresses - Array of wallet addresses
187
+ * @returns {Promise<Array>} Array of owner token UTXOs
188
+ */
189
+ async getAllOwnerTokens(addresses) {
190
+ if (!Array.isArray(addresses) || addresses.length === 0) {
191
+ throw new Error('Addresses array is required');
192
+ }
193
+
194
+ try {
195
+ const utxos = await this.rpc('getaddressutxos', [{ addresses }]);
196
+
197
+ // Filter for owner tokens (asset names ending with !)
198
+ const ownerTokenUTXOs = utxos.filter(utxo => {
199
+ return utxo.assetName && utxo.assetName.endsWith('!');
200
+ });
201
+
202
+ return ownerTokenUTXOs;
203
+ } catch (error) {
204
+ throw new AssetError(
205
+ `Failed to get owner tokens: ${error.message}`
206
+ );
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Verify owner token quantity is correct (always 1)
212
+ * @param {object} ownerTokenUTXO - Owner token UTXO
213
+ * @returns {boolean} True if valid
214
+ * @throws {Error} If quantity is not 1
215
+ */
216
+ validateOwnerTokenQuantity(ownerTokenUTXO) {
217
+ // Owner tokens should always have satoshis = 100000000 (1.0 with 8 decimals)
218
+ const expectedSatoshis = 100000000;
219
+
220
+ if (ownerTokenUTXO.satoshis !== expectedSatoshis) {
221
+ throw new Error(
222
+ `Invalid owner token quantity. Expected ${expectedSatoshis} satoshis, ` +
223
+ `got ${ownerTokenUTXO.satoshis}. Owner tokens must always be exactly 1.0`
224
+ );
225
+ }
226
+
227
+ return true;
228
+ }
229
+
230
+ /**
231
+ * Add owner token input and output to transaction
232
+ * Convenience method that handles both finding and returning owner token
233
+ *
234
+ * @param {string} assetName - Base asset name (without !)
235
+ * @param {string[]} addresses - Wallet addresses
236
+ * @param {string} returnAddress - Address to return owner token to
237
+ * @returns {Promise<object>} { input, output }
238
+ */
239
+ async prepareOwnerTokenForTransaction(assetName, addresses, returnAddress) {
240
+ // Find owner token UTXO
241
+ const ownerTokenUTXO = await this.findOwnerTokenByAssetName(assetName, addresses);
242
+
243
+ // Validate quantity
244
+ this.validateOwnerTokenQuantity(ownerTokenUTXO);
245
+
246
+ // Create input
247
+ const input = {
248
+ txid: ownerTokenUTXO.txid,
249
+ vout: ownerTokenUTXO.outputIndex,
250
+ address: ownerTokenUTXO.address,
251
+ assetName: ownerTokenUTXO.assetName,
252
+ satoshis: ownerTokenUTXO.satoshis
253
+ };
254
+
255
+ // Create output
256
+ const output = this.createOwnerTokenReturnOutput(
257
+ ownerTokenUTXO.assetName,
258
+ returnAddress
259
+ );
260
+
261
+ return { input, output, utxo: ownerTokenUTXO };
262
+ }
263
+ }
264
+
265
+ module.exports = OwnerTokenManager;