@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,246 @@
1
+ /**
2
+ * Reissue Builder
3
+ * Builds transactions for reissuing (minting more supply) assets
4
+ *
5
+ * Reissue:
6
+ * - Mints additional supply of an existing asset
7
+ * - Cost: 200 XNA (burned)
8
+ * - Requires asset's owner token (ASSET!)
9
+ * - Can lock asset (make it non-reissuable)
10
+ * - Can update IPFS metadata
11
+ * - Owner token must be returned
12
+ */
13
+
14
+ const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
15
+ const { OutputFormatter, AssetNameParser } = require('../utils');
16
+ const {
17
+ AssetNotFoundError,
18
+ AssetNotReissuableError,
19
+ OwnerTokenNotFoundError,
20
+ MaxSupplyExceededError
21
+ } = require('../errors');
22
+ const { IpfsValidator } = require('../validators');
23
+ const { ASSET_LIMITS } = require('../constants');
24
+
25
+ class ReissueBuilder extends BaseAssetTransactionBuilder {
26
+ /**
27
+ * Validate reissue parameters
28
+ * @param {object} params - Reissue parameters
29
+ * @throws {Error} If validation fails
30
+ */
31
+ validateParams(params) {
32
+ // Validate required parameters
33
+ if (!params.assetName) {
34
+ throw new Error('assetName is required');
35
+ }
36
+
37
+ if (params.quantity === undefined || params.quantity === null) {
38
+ throw new Error('quantity is required (amount to mint)');
39
+ }
40
+
41
+ if (params.quantity <= 0) {
42
+ throw new Error('quantity must be greater than 0');
43
+ }
44
+
45
+ // Validate new IPFS hash if provided
46
+ if (params.newIpfs) {
47
+ IpfsValidator.validate(params.newIpfs);
48
+ }
49
+
50
+ return true;
51
+ }
52
+
53
+ /**
54
+ * Build reissue 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
+ reissuable,
65
+ newIpfs
66
+ } = this.params;
67
+
68
+ // 2. Get asset data to verify it exists and is reissuable
69
+ const assetData = await this.getAssetData(assetName);
70
+ if (!assetData) {
71
+ throw new AssetNotFoundError(
72
+ `Asset ${assetName} does not exist on the blockchain`,
73
+ assetName
74
+ );
75
+ }
76
+
77
+ // 3. Check if asset is reissuable
78
+ if (!assetData.reissuable) {
79
+ throw new AssetNotReissuableError(
80
+ `Asset ${assetName} is not reissuable. The supply has been locked.`,
81
+ assetName
82
+ );
83
+ }
84
+
85
+ // 4. Check if reissuing would exceed max supply
86
+ const currentSupply = assetData.amount || 0;
87
+ const additionalAmount = quantity;
88
+ const newTotalSupply = currentSupply + additionalAmount;
89
+
90
+ if (newTotalSupply > ASSET_LIMITS.MAX_QUANTITY) {
91
+ throw new MaxSupplyExceededError(
92
+ `Reissuing ${additionalAmount} would exceed maximum supply. ` +
93
+ `Current: ${currentSupply}, Additional: ${additionalAmount}, ` +
94
+ `Max: ${ASSET_LIMITS.MAX_QUANTITY}`,
95
+ assetName,
96
+ currentSupply,
97
+ additionalAmount,
98
+ ASSET_LIMITS.MAX_QUANTITY
99
+ );
100
+ }
101
+
102
+ // 5. Get addresses
103
+ const addresses = await this._getAddresses();
104
+ const toAddress = await this.getToAddress();
105
+ const changeAddress = await this.getChangeAddress();
106
+
107
+ // 6. Find owner token (CRITICAL: must have this)
108
+ const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
109
+ let ownerTokenUTXO;
110
+ try {
111
+ ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
112
+ ownerTokenName,
113
+ addresses
114
+ );
115
+ } catch (error) {
116
+ if (error instanceof OwnerTokenNotFoundError) {
117
+ throw new OwnerTokenNotFoundError(
118
+ `You must own the asset's owner token (${ownerTokenName}) to reissue it. ` +
119
+ `The owner token proves you have the right to mint more supply.`,
120
+ ownerTokenName
121
+ );
122
+ }
123
+ throw error;
124
+ }
125
+
126
+ // 7. Get burn information
127
+ const burnInfo = this.burnManager.getReissueBurn();
128
+
129
+ // 8. Estimate fee
130
+ // Inputs: XNA UTXOs + owner token UTXO
131
+ // Outputs: burn + change + owner token return + reissue operation
132
+ const estimatedFee = await this.estimateFee(2, 4);
133
+
134
+ // 9. Calculate total XNA needed
135
+ const totalXNANeeded = burnInfo.amount + estimatedFee;
136
+
137
+ // 10. Select XNA UTXOs
138
+ const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
139
+ const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
140
+ const totalXNAInput = utxoSelection.totalXNA;
141
+
142
+ // 11. Recalculate fee with actual input count
143
+ const actualInputCount = baseCurrencyUTXOs.length + 1; // +1 for owner token
144
+ const actualFee = await this.estimateFee(actualInputCount, 4);
145
+
146
+ // 12. Verify we have enough XNA
147
+ const totalRequired = burnInfo.amount + actualFee;
148
+ if (totalXNAInput < totalRequired) {
149
+ const additionalNeeded = totalRequired - totalXNAInput + 0.001;
150
+ const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
151
+ baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
152
+ }
153
+
154
+ // 13. Calculate XNA change
155
+ const finalTotalInput = baseCurrencyUTXOs.reduce(
156
+ (sum, utxo) => sum + utxo.satoshis / 100000000,
157
+ 0
158
+ );
159
+ const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
160
+
161
+ // 14. Build inputs (XNA + owner token)
162
+ const inputs = [];
163
+
164
+ // Add XNA inputs
165
+ baseCurrencyUTXOs.forEach(utxo => {
166
+ inputs.push({
167
+ txid: utxo.txid,
168
+ vout: utxo.outputIndex,
169
+ address: utxo.address,
170
+ satoshis: utxo.satoshis
171
+ });
172
+ });
173
+
174
+ // Add owner token input
175
+ inputs.push({
176
+ txid: ownerTokenUTXO.txid,
177
+ vout: ownerTokenUTXO.outputIndex,
178
+ address: ownerTokenUTXO.address,
179
+ assetName: ownerTokenUTXO.assetName,
180
+ satoshis: ownerTokenUTXO.satoshis
181
+ });
182
+
183
+ // 15. Build outputs (ORDER CRITICAL!)
184
+ const outputs = {};
185
+
186
+ // First: Burn output
187
+ outputs[burnInfo.address] = burnInfo.amount;
188
+
189
+ // Second: XNA change (if any)
190
+ if (xnaChange > 0.00000001) {
191
+ outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
192
+ }
193
+
194
+ // Third: Owner token return (CRITICAL - must return or lost forever!)
195
+ // Support optional ownerChangeAddress for transferring ownership
196
+ const ownerReturnAddress = this.params.ownerChangeAddress || changeAddress;
197
+ const ownerTokenReturn = this.ownerTokenManager.createOwnerTokenReturnOutput(
198
+ ownerTokenName,
199
+ ownerReturnAddress
200
+ );
201
+ Object.assign(outputs, ownerTokenReturn);
202
+
203
+ // Last: Reissue operation
204
+ const units = assetData.units || 0;
205
+ const reissueOutput = OutputFormatter.formatReissueOutput({
206
+ asset_name: assetName,
207
+ asset_quantity: this.toSatoshis(quantity, units),
208
+ reissuable: reissuable !== undefined ? reissuable : undefined,
209
+ new_ipfs: newIpfs || undefined
210
+ });
211
+
212
+ outputs[toAddress] = reissueOutput;
213
+
214
+ // 16. Order outputs (protocol requirement)
215
+ const orderedOutputs = this.outputOrderer.order(outputs);
216
+
217
+ // 17. Validate owner token is returned (safety check)
218
+ this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
219
+
220
+ // 18. Create raw transaction
221
+ const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
222
+
223
+ // 19. Format and return result
224
+ const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
225
+
226
+ return this.formatResult(
227
+ rawTx,
228
+ allUTXOs,
229
+ inputs,
230
+ orderedOutputs,
231
+ actualFee,
232
+ burnInfo.amount,
233
+ {
234
+ assetName,
235
+ ownerTokenUsed: ownerTokenName,
236
+ quantityMinted: quantity,
237
+ newTotalSupply,
238
+ previousSupply: currentSupply,
239
+ reissuableLocked: reissuable === false,
240
+ operationType: 'REISSUE'
241
+ }
242
+ );
243
+ }
244
+ }
245
+
246
+ module.exports = ReissueBuilder;
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Reissue Restricted Builder
3
+ * Builds transactions for reissuing RESTRICTED assets
4
+ *
5
+ * Reissue Restricted:
6
+ * - Mints additional supply of restricted asset
7
+ * - Cost: 200 XNA (burned)
8
+ * - Requires asset's owner token ($ASSET!)
9
+ * - Can update verifier string
10
+ * - Can lock asset (make it non-reissuable)
11
+ * - Can update IPFS metadata
12
+ * - Owner token must be returned
13
+ */
14
+
15
+ const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
16
+ const { OutputFormatter, AssetNameParser } = require('../utils');
17
+ const {
18
+ AssetNotFoundError,
19
+ AssetNotReissuableError,
20
+ OwnerTokenNotFoundError,
21
+ MaxSupplyExceededError
22
+ } = require('../errors');
23
+ const { IpfsValidator, VerifierValidator } = require('../validators');
24
+ const { ASSET_LIMITS } = require('../constants');
25
+
26
+ class ReissueRestrictedBuilder extends BaseAssetTransactionBuilder {
27
+ /**
28
+ * Validate reissue restricted parameters
29
+ * @param {object} params - Reissue parameters
30
+ * @throws {Error} If validation fails
31
+ */
32
+ validateParams(params) {
33
+ // Validate required parameters
34
+ if (!params.assetName) {
35
+ throw new Error('assetName is required');
36
+ }
37
+
38
+ // Validate asset name is restricted
39
+ this.validateAssetName(params.assetName, 'RESTRICTED');
40
+
41
+ if (params.quantity === undefined || params.quantity === null) {
42
+ throw new Error('quantity is required (amount to mint)');
43
+ }
44
+
45
+ if (params.quantity <= 0) {
46
+ throw new Error('quantity must be greater than 0');
47
+ }
48
+
49
+ // Validate verifier string if changing
50
+ if (params.changeVerifier && params.newVerifier) {
51
+ VerifierValidator.validate(params.newVerifier);
52
+ }
53
+
54
+ // Validate new IPFS hash if provided
55
+ if (params.newIpfs) {
56
+ IpfsValidator.validate(params.newIpfs);
57
+ }
58
+
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * Build reissue restricted transaction
64
+ * @returns {Promise<object>} Transaction result
65
+ */
66
+ async build() {
67
+ // 1. Validate parameters
68
+ await this.validateParams(this.params);
69
+
70
+ const {
71
+ assetName,
72
+ quantity,
73
+ changeVerifier = false,
74
+ newVerifier,
75
+ reissuable,
76
+ newIpfs
77
+ } = this.params;
78
+
79
+ // 2. Get asset data to verify it exists and is reissuable
80
+ const assetData = await this.getAssetData(assetName);
81
+ if (!assetData) {
82
+ throw new AssetNotFoundError(
83
+ `Asset ${assetName} does not exist on the blockchain`,
84
+ assetName
85
+ );
86
+ }
87
+
88
+ // 3. Check if asset is reissuable
89
+ if (!assetData.reissuable) {
90
+ throw new AssetNotReissuableError(
91
+ `Asset ${assetName} is not reissuable. The supply has been locked.`,
92
+ assetName
93
+ );
94
+ }
95
+
96
+ // 4. Check if reissuing would exceed max supply
97
+ const currentSupply = assetData.amount || 0;
98
+ const additionalAmount = quantity;
99
+ const newTotalSupply = currentSupply + additionalAmount;
100
+
101
+ if (newTotalSupply > ASSET_LIMITS.MAX_QUANTITY) {
102
+ throw new MaxSupplyExceededError(
103
+ `Reissuing ${additionalAmount} would exceed maximum supply. ` +
104
+ `Current: ${currentSupply}, Additional: ${additionalAmount}, ` +
105
+ `Max: ${ASSET_LIMITS.MAX_QUANTITY}`,
106
+ assetName,
107
+ currentSupply,
108
+ additionalAmount,
109
+ ASSET_LIMITS.MAX_QUANTITY
110
+ );
111
+ }
112
+
113
+ // 5. Get addresses
114
+ const addresses = await this._getAddresses();
115
+ const toAddress = await this.getToAddress();
116
+ const changeAddress = await this.getChangeAddress();
117
+
118
+ // 6. Find owner token (CRITICAL: must have this)
119
+ const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
120
+ let ownerTokenUTXO;
121
+ try {
122
+ ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
123
+ ownerTokenName,
124
+ addresses
125
+ );
126
+ } catch (error) {
127
+ if (error instanceof OwnerTokenNotFoundError) {
128
+ throw new OwnerTokenNotFoundError(
129
+ `You must own the asset's owner token (${ownerTokenName}) to reissue it. ` +
130
+ `The owner token proves you have the right to mint more supply and manage the asset.`,
131
+ ownerTokenName
132
+ );
133
+ }
134
+ throw error;
135
+ }
136
+
137
+ // 7. Get burn information
138
+ const burnInfo = this.burnManager.getReissueBurn();
139
+
140
+ // 8. Estimate fee
141
+ const estimatedFee = await this.estimateFee(2, 4);
142
+
143
+ // 9. Calculate total XNA needed
144
+ const totalXNANeeded = burnInfo.amount + estimatedFee;
145
+
146
+ // 10. Select XNA UTXOs
147
+ const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
148
+ const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
149
+ const totalXNAInput = utxoSelection.totalXNA;
150
+
151
+ // 11. Recalculate fee with actual input count
152
+ const actualInputCount = baseCurrencyUTXOs.length + 1; // +1 for owner token
153
+ const actualFee = await this.estimateFee(actualInputCount, 4);
154
+
155
+ // 12. Verify we have enough XNA
156
+ const totalRequired = burnInfo.amount + actualFee;
157
+ if (totalXNAInput < totalRequired) {
158
+ const additionalNeeded = totalRequired - totalXNAInput + 0.001;
159
+ const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
160
+ baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
161
+ }
162
+
163
+ // 13. Calculate XNA change
164
+ const finalTotalInput = baseCurrencyUTXOs.reduce(
165
+ (sum, utxo) => sum + utxo.satoshis / 100000000,
166
+ 0
167
+ );
168
+ const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
169
+
170
+ // 14. Build inputs (XNA + owner token)
171
+ const inputs = [];
172
+
173
+ // Add XNA inputs
174
+ baseCurrencyUTXOs.forEach(utxo => {
175
+ inputs.push({
176
+ txid: utxo.txid,
177
+ vout: utxo.outputIndex,
178
+ address: utxo.address,
179
+ satoshis: utxo.satoshis
180
+ });
181
+ });
182
+
183
+ // Add owner token input
184
+ inputs.push({
185
+ txid: ownerTokenUTXO.txid,
186
+ vout: ownerTokenUTXO.outputIndex,
187
+ address: ownerTokenUTXO.address,
188
+ assetName: ownerTokenUTXO.assetName,
189
+ satoshis: ownerTokenUTXO.satoshis
190
+ });
191
+
192
+ // 15. Build outputs (ORDER CRITICAL!)
193
+ const outputs = {};
194
+
195
+ // First: Burn output
196
+ outputs[burnInfo.address] = burnInfo.amount;
197
+
198
+ // Second: XNA change (if any)
199
+ if (xnaChange > 0.00000001) {
200
+ outputs[changeAddress] = parseFloat(xnaChange.toFixed(8));
201
+ }
202
+
203
+ // Third: Owner token return (CRITICAL - must return or lost forever!)
204
+ const ownerReturnAddress = this.params.ownerChangeAddress || changeAddress;
205
+ const ownerTokenReturn = this.ownerTokenManager.createOwnerTokenReturnOutput(
206
+ ownerTokenName,
207
+ ownerReturnAddress
208
+ );
209
+ Object.assign(outputs, ownerTokenReturn);
210
+
211
+ // Last: Reissue restricted operation
212
+ const units = assetData.units || 0;
213
+ const reissueRestrictedOutput = OutputFormatter.formatReissueRestrictedOutput({
214
+ asset_name: assetName,
215
+ asset_quantity: this.toSatoshis(quantity, units),
216
+ change_verifier: changeVerifier,
217
+ new_verifier: changeVerifier ? newVerifier : undefined,
218
+ reissuable: reissuable !== undefined ? reissuable : undefined,
219
+ new_ipfs: newIpfs || undefined
220
+ });
221
+
222
+ outputs[toAddress] = reissueRestrictedOutput;
223
+
224
+ // 16. Order outputs (protocol requirement)
225
+ const orderedOutputs = this.outputOrderer.order(outputs);
226
+
227
+ // 17. Validate owner token is returned (safety check)
228
+ this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
229
+
230
+ // 18. Create raw transaction
231
+ const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
232
+
233
+ // 19. Format and return result
234
+ const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
235
+
236
+ // Extract qualifiers from new verifier if changed
237
+ const requiredQualifiers = changeVerifier && newVerifier
238
+ ? VerifierValidator.extractQualifiers(newVerifier)
239
+ : null;
240
+
241
+ return this.formatResult(
242
+ rawTx,
243
+ allUTXOs,
244
+ inputs,
245
+ orderedOutputs,
246
+ actualFee,
247
+ burnInfo.amount,
248
+ {
249
+ assetName,
250
+ ownerTokenUsed: ownerTokenName,
251
+ quantityMinted: quantity,
252
+ newTotalSupply,
253
+ previousSupply: currentSupply,
254
+ verifierChanged: changeVerifier,
255
+ newVerifier: changeVerifier ? newVerifier : undefined,
256
+ requiredQualifiers,
257
+ reissuableLocked: reissuable === false,
258
+ operationType: 'REISSUE_RESTRICTED'
259
+ }
260
+ );
261
+ }
262
+ }
263
+
264
+ module.exports = ReissueRestrictedBuilder;