@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,237 @@
1
+ /**
2
+ * Issue Sub Builder
3
+ * Builds transactions for creating SUB assets
4
+ *
5
+ * SUB assets:
6
+ * - Child of a ROOT asset (format: ROOT/SUBNAME)
7
+ * - Cost: 200 XNA (burned)
8
+ * - Requires parent's owner token (ROOT!)
9
+ * - Creates own owner token (ROOT/SUB!)
10
+ * - Parent owner token must be returned in outputs
11
+ */
12
+
13
+ const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
14
+ const { OutputFormatter, AssetNameParser } = require('../utils');
15
+ const {
16
+ AssetExistsError,
17
+ ParentAssetNotFoundError,
18
+ OwnerTokenNotFoundError
19
+ } = require('../errors');
20
+ const { IpfsValidator } = require('../validators');
21
+
22
+ class IssueSubBuilder extends BaseAssetTransactionBuilder {
23
+ /**
24
+ * Validate issue SUB parameters
25
+ * @param {object} params - Issue parameters
26
+ * @throws {Error} If validation fails
27
+ */
28
+ validateParams(params) {
29
+ // Validate required parameters
30
+ if (!params.assetName) {
31
+ throw new Error('assetName is required');
32
+ }
33
+
34
+ if (params.quantity === undefined || params.quantity === null) {
35
+ throw new Error('quantity is required');
36
+ }
37
+
38
+ // Validate asset name (SUB format: ROOT/SUBNAME)
39
+ this.validateAssetName(params.assetName, 'SUB');
40
+
41
+ // Validate quantity and units
42
+ const units = params.units !== undefined ? params.units : 0;
43
+ this.validateAmount(params.quantity, units);
44
+
45
+ // Validate IPFS hash if provided
46
+ if (params.hasIpfs && params.ipfsHash) {
47
+ IpfsValidator.validate(params.ipfsHash);
48
+ }
49
+
50
+ return true;
51
+ }
52
+
53
+ /**
54
+ * Build SUB asset issuance transaction
55
+ * @returns {Promise<object>} Transaction result
56
+ */
57
+ async build() {
58
+ // 1. Validate parameters
59
+ await this.validateParams(this.params);
60
+
61
+ const {
62
+ assetName,
63
+ quantity,
64
+ units = 0,
65
+ reissuable = true,
66
+ hasIpfs = false,
67
+ ipfsHash = ''
68
+ } = this.params;
69
+
70
+ // 2. Parse asset name to get parent
71
+ const parsed = AssetNameParser.parse(assetName);
72
+ const parentAssetName = parsed.parent;
73
+
74
+ if (!parentAssetName) {
75
+ throw new Error('Cannot parse parent asset from SUB asset name');
76
+ }
77
+
78
+ // 3. Check if parent asset exists
79
+ const parentExists = await this.assetExists(parentAssetName);
80
+ if (!parentExists) {
81
+ throw new ParentAssetNotFoundError(
82
+ `Parent asset ${parentAssetName} does not exist. You must create the ROOT asset first.`,
83
+ parentAssetName
84
+ );
85
+ }
86
+
87
+ // 4. Check if SUB asset already exists
88
+ const subExists = await this.assetExists(assetName);
89
+ if (subExists) {
90
+ throw new AssetExistsError(
91
+ `Asset ${assetName} already exists on the blockchain`,
92
+ assetName
93
+ );
94
+ }
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. Find parent's owner token (CRITICAL: must have this)
102
+ const ownerTokenName = AssetNameParser.getOwnerTokenName(parentAssetName);
103
+ let ownerTokenUTXO;
104
+ try {
105
+ ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
106
+ ownerTokenName,
107
+ addresses
108
+ );
109
+ } catch (error) {
110
+ if (error instanceof OwnerTokenNotFoundError) {
111
+ throw new OwnerTokenNotFoundError(
112
+ `You must own the parent asset's owner token (${ownerTokenName}) to create a SUB asset. ` +
113
+ `The owner token proves you control the parent asset.`,
114
+ ownerTokenName
115
+ );
116
+ }
117
+ throw error;
118
+ }
119
+
120
+ // 7. Get burn information
121
+ const burnInfo = this.burnManager.getIssueSubBurn();
122
+
123
+ // 8. Estimate fee
124
+ // Inputs: XNA UTXOs + owner token UTXO
125
+ // Outputs: burn + change + owner token return + issue operation
126
+ const estimatedFee = await this.estimateFee(2, 4);
127
+
128
+ // 9. Calculate total XNA needed
129
+ const totalXNANeeded = burnInfo.amount + estimatedFee;
130
+
131
+ // 10. Select XNA UTXOs
132
+ const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
133
+ const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
134
+ const totalXNAInput = utxoSelection.totalXNA;
135
+
136
+ // 11. Recalculate fee with actual input count
137
+ const actualInputCount = baseCurrencyUTXOs.length + 1; // +1 for owner token
138
+ const actualFee = await this.estimateFee(actualInputCount, 4);
139
+
140
+ // 12. Verify we have enough XNA
141
+ const totalRequired = burnInfo.amount + actualFee;
142
+ if (totalXNAInput < totalRequired) {
143
+ const additionalNeeded = totalRequired - totalXNAInput + 0.001;
144
+ const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
145
+ baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
146
+ }
147
+
148
+ // 13. Calculate XNA change
149
+ const finalTotalInput = baseCurrencyUTXOs.reduce(
150
+ (sum, utxo) => sum + utxo.satoshis / 100000000,
151
+ 0
152
+ );
153
+ const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
154
+
155
+ // 14. Build inputs (XNA + owner token)
156
+ const inputs = [];
157
+
158
+ // Add XNA inputs
159
+ baseCurrencyUTXOs.forEach(utxo => {
160
+ inputs.push({
161
+ txid: utxo.txid,
162
+ vout: utxo.outputIndex,
163
+ address: utxo.address,
164
+ satoshis: utxo.satoshis
165
+ });
166
+ });
167
+
168
+ // Add owner token input
169
+ inputs.push({
170
+ txid: ownerTokenUTXO.txid,
171
+ vout: ownerTokenUTXO.outputIndex,
172
+ address: ownerTokenUTXO.address,
173
+ assetName: ownerTokenUTXO.assetName,
174
+ satoshis: ownerTokenUTXO.satoshis
175
+ });
176
+
177
+ // 15. Build outputs (ORDER CRITICAL!)
178
+ const outputs = {};
179
+
180
+ // First: Burn output
181
+ outputs[burnInfo.address] = burnInfo.amount;
182
+
183
+ // Second: XNA change (if any)
184
+ if (xnaChange > 0.00000001) {
185
+ outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
186
+ }
187
+
188
+ // Third: Owner token return (CRITICAL - must return or lost forever!)
189
+ const ownerTokenReturn = this.ownerTokenManager.createOwnerTokenReturnOutput(
190
+ ownerTokenName,
191
+ changeAddress // Return owner token to change address
192
+ );
193
+ Object.assign(outputs, ownerTokenReturn);
194
+
195
+ // Last: Issue operation
196
+ const issueOutput = OutputFormatter.formatIssueOutput({
197
+ asset_name: assetName,
198
+ asset_quantity: this.toSatoshis(quantity, units),
199
+ units: units,
200
+ reissuable: reissuable,
201
+ has_ipfs: hasIpfs,
202
+ ipfs_hash: ipfsHash
203
+ });
204
+
205
+ outputs[toAddress] = issueOutput;
206
+
207
+ // 16. Order outputs (protocol requirement)
208
+ const orderedOutputs = this.outputOrderer.order(outputs);
209
+
210
+ // 17. Validate owner token is returned (safety check)
211
+ this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
212
+
213
+ // 18. Create raw transaction
214
+ const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
215
+
216
+ // 19. Format and return result
217
+ const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
218
+
219
+ return this.formatResult(
220
+ rawTx,
221
+ allUTXOs,
222
+ inputs,
223
+ orderedOutputs,
224
+ actualFee,
225
+ burnInfo.amount,
226
+ {
227
+ assetName,
228
+ parentAssetName,
229
+ ownerTokenName: assetName + '!',
230
+ parentOwnerTokenUsed: ownerTokenName,
231
+ operationType: 'ISSUE_SUB'
232
+ }
233
+ );
234
+ }
235
+ }
236
+
237
+ module.exports = IssueSubBuilder;
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Issue Unique Builder
3
+ * Builds transactions for creating UNIQUE assets (NFTs)
4
+ *
5
+ * UNIQUE assets:
6
+ * - Non-fungible tokens (NFTs)
7
+ * - Format: ROOT#TAG (e.g., MYNFT#001)
8
+ * - Cost: 10 XNA per NFT (burned)
9
+ * - Requires parent's owner token (ROOT!)
10
+ * - Properties: quantity=1, units=0, reissuable=false (always)
11
+ * - Can create multiple NFTs in single transaction
12
+ * - Each NFT can have unique IPFS metadata
13
+ */
14
+
15
+ const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
16
+ const { OutputFormatter, AssetNameParser } = require('../utils');
17
+ const {
18
+ ParentAssetNotFoundError,
19
+ OwnerTokenNotFoundError,
20
+ AssetExistsError
21
+ } = require('../errors');
22
+ const { IpfsValidator } = require('../validators');
23
+
24
+ class IssueUniqueBuilder extends BaseAssetTransactionBuilder {
25
+ /**
26
+ * Validate issue UNIQUE parameters
27
+ * @param {object} params - Issue parameters
28
+ * @throws {Error} If validation fails
29
+ */
30
+ validateParams(params) {
31
+ // Validate required parameters
32
+ if (!params.rootName) {
33
+ throw new Error('rootName is required (parent asset name)');
34
+ }
35
+
36
+ if (!params.assetTags || !Array.isArray(params.assetTags) || params.assetTags.length === 0) {
37
+ throw new Error('assetTags is required and must be a non-empty array');
38
+ }
39
+
40
+ // Validate root name
41
+ this.validateAssetName(params.rootName, 'ROOT');
42
+
43
+ // Validate each tag
44
+ params.assetTags.forEach((tag, index) => {
45
+ if (!tag || typeof tag !== 'string') {
46
+ throw new Error(`assetTags[${index}] must be a non-empty string`);
47
+ }
48
+
49
+ // Validate full unique asset name
50
+ const fullName = `${params.rootName}#${tag}`;
51
+ this.validateAssetName(fullName, 'UNIQUE');
52
+ });
53
+
54
+ // Validate IPFS hashes if provided
55
+ if (params.ipfsHashes) {
56
+ if (!Array.isArray(params.ipfsHashes)) {
57
+ throw new Error('ipfsHashes must be an array');
58
+ }
59
+
60
+ if (params.ipfsHashes.length !== params.assetTags.length) {
61
+ throw new Error(
62
+ `ipfsHashes array length (${params.ipfsHashes.length}) must match ` +
63
+ `assetTags array length (${params.assetTags.length})`
64
+ );
65
+ }
66
+
67
+ params.ipfsHashes.forEach((hash, index) => {
68
+ if (hash) {
69
+ IpfsValidator.validate(hash);
70
+ }
71
+ });
72
+ }
73
+
74
+ return true;
75
+ }
76
+
77
+ /**
78
+ * Build UNIQUE asset issuance transaction
79
+ * @returns {Promise<object>} Transaction result
80
+ */
81
+ async build() {
82
+ // 1. Validate parameters
83
+ await this.validateParams(this.params);
84
+
85
+ const {
86
+ rootName,
87
+ assetTags,
88
+ ipfsHashes = []
89
+ } = this.params;
90
+
91
+ // 2. Check if parent asset exists
92
+ const parentExists = await this.assetExists(rootName);
93
+ if (!parentExists) {
94
+ throw new ParentAssetNotFoundError(
95
+ `Parent asset ${rootName} does not exist. You must create the ROOT asset first.`,
96
+ rootName
97
+ );
98
+ }
99
+
100
+ // 3. Check if any of the unique assets already exist
101
+ for (const tag of assetTags) {
102
+ const fullName = `${rootName}#${tag}`;
103
+ const exists = await this.assetExists(fullName);
104
+ if (exists) {
105
+ throw new AssetExistsError(
106
+ `Unique asset ${fullName} already exists on the blockchain`,
107
+ fullName
108
+ );
109
+ }
110
+ }
111
+
112
+ // 4. Get addresses
113
+ const addresses = await this._getAddresses();
114
+ const toAddress = await this.getToAddress();
115
+ const changeAddress = await this.getChangeAddress();
116
+
117
+ // 5. Find parent's owner token (CRITICAL: must have this)
118
+ const ownerTokenName = AssetNameParser.getOwnerTokenName(rootName);
119
+ let ownerTokenUTXO;
120
+ try {
121
+ ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
122
+ ownerTokenName,
123
+ addresses
124
+ );
125
+ } catch (error) {
126
+ if (error instanceof OwnerTokenNotFoundError) {
127
+ throw new OwnerTokenNotFoundError(
128
+ `You must own the parent asset's owner token (${ownerTokenName}) to create UNIQUE assets. ` +
129
+ `The owner token proves you control the parent asset.`,
130
+ ownerTokenName
131
+ );
132
+ }
133
+ throw error;
134
+ }
135
+
136
+ // 6. Get burn information (cost = 10 XNA per NFT)
137
+ const nftCount = assetTags.length;
138
+ const burnInfo = this.burnManager.getIssueUniqueBurn(nftCount);
139
+
140
+ // 7. Estimate fee
141
+ // Inputs: XNA UTXOs + owner token UTXO
142
+ // Outputs: burn + change + owner token return + issue_unique operation
143
+ const estimatedFee = await this.estimateFee(2, 4);
144
+
145
+ // 8. Calculate total XNA needed
146
+ const totalXNANeeded = burnInfo.amount + estimatedFee;
147
+
148
+ // 9. Select XNA UTXOs
149
+ const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
150
+ const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
151
+ const totalXNAInput = utxoSelection.totalXNA;
152
+
153
+ // 10. Recalculate fee with actual input count
154
+ const actualInputCount = baseCurrencyUTXOs.length + 1; // +1 for owner token
155
+ const actualFee = await this.estimateFee(actualInputCount, 4);
156
+
157
+ // 11. Verify we have enough XNA
158
+ const totalRequired = burnInfo.amount + actualFee;
159
+ if (totalXNAInput < totalRequired) {
160
+ const additionalNeeded = totalRequired - totalXNAInput + 0.001;
161
+ const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
162
+ baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
163
+ }
164
+
165
+ // 12. Calculate XNA change
166
+ const finalTotalInput = baseCurrencyUTXOs.reduce(
167
+ (sum, utxo) => sum + utxo.satoshis / 100000000,
168
+ 0
169
+ );
170
+ const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
171
+
172
+ // 13. Build inputs (XNA + owner token)
173
+ const inputs = [];
174
+
175
+ // Add XNA inputs
176
+ baseCurrencyUTXOs.forEach(utxo => {
177
+ inputs.push({
178
+ txid: utxo.txid,
179
+ vout: utxo.outputIndex,
180
+ address: utxo.address,
181
+ satoshis: utxo.satoshis
182
+ });
183
+ });
184
+
185
+ // Add owner token input
186
+ inputs.push({
187
+ txid: ownerTokenUTXO.txid,
188
+ vout: ownerTokenUTXO.outputIndex,
189
+ address: ownerTokenUTXO.address,
190
+ assetName: ownerTokenUTXO.assetName,
191
+ satoshis: ownerTokenUTXO.satoshis
192
+ });
193
+
194
+ // 14. Build outputs (ORDER CRITICAL!)
195
+ const outputs = {};
196
+
197
+ // First: Burn output
198
+ outputs[burnInfo.address] = burnInfo.amount;
199
+
200
+ // Second: XNA change (if any)
201
+ if (xnaChange > 0.00000001) {
202
+ outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
203
+ }
204
+
205
+ // Third: Owner token return (CRITICAL - must return or lost forever!)
206
+ const ownerTokenReturn = this.ownerTokenManager.createOwnerTokenReturnOutput(
207
+ ownerTokenName,
208
+ changeAddress
209
+ );
210
+ Object.assign(outputs, ownerTokenReturn);
211
+
212
+ // Last: Issue unique operation
213
+ const issueUniqueOutput = OutputFormatter.formatIssueUniqueOutput({
214
+ root_name: rootName,
215
+ asset_tags: assetTags,
216
+ ipfs_hashes: ipfsHashes.length > 0 ? ipfsHashes : undefined
217
+ });
218
+
219
+ outputs[toAddress] = issueUniqueOutput;
220
+
221
+ // 15. Order outputs (protocol requirement)
222
+ const orderedOutputs = this.outputOrderer.order(outputs);
223
+
224
+ // 16. Validate owner token is returned (safety check)
225
+ this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
226
+
227
+ // 17. Create raw transaction
228
+ const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
229
+
230
+ // 18. Build list of created NFT names
231
+ const createdNFTs = assetTags.map(tag => `${rootName}#${tag}`);
232
+
233
+ // 19. Format and return result
234
+ const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
235
+
236
+ return this.formatResult(
237
+ rawTx,
238
+ allUTXOs,
239
+ inputs,
240
+ orderedOutputs,
241
+ actualFee,
242
+ burnInfo.amount,
243
+ {
244
+ rootName,
245
+ assetTags,
246
+ createdNFTs,
247
+ nftCount,
248
+ ownerTokenUsed: ownerTokenName,
249
+ operationType: 'ISSUE_UNIQUE'
250
+ }
251
+ );
252
+ }
253
+ }
254
+
255
+ module.exports = IssueUniqueBuilder;