@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.
- package/README.md +522 -0
- package/examples/01-create-root-asset.js +71 -0
- package/examples/02-create-sub-asset.js +79 -0
- package/examples/03-create-nfts.js +140 -0
- package/examples/04-reissue-asset.js +164 -0
- package/examples/05-create-qualifier-and-tag.js +209 -0
- package/examples/06-create-restricted-asset.js +223 -0
- package/examples/07-freeze-and-unfreeze.js +292 -0
- package/examples/08-query-assets.js +332 -0
- package/examples/09-wallet-integration.js +320 -0
- package/examples/README.md +319 -0
- package/package.json +43 -0
- package/src/NeuraiAssets.js +468 -0
- package/src/builders/BaseAssetTransactionBuilder.js +303 -0
- package/src/builders/FreezeAddressBuilder.js +271 -0
- package/src/builders/IssueQualifierBuilder.js +251 -0
- package/src/builders/IssueRestrictedBuilder.js +187 -0
- package/src/builders/IssueRootBuilder.js +173 -0
- package/src/builders/IssueSubBuilder.js +237 -0
- package/src/builders/IssueUniqueBuilder.js +255 -0
- package/src/builders/ReissueBuilder.js +246 -0
- package/src/builders/ReissueRestrictedBuilder.js +264 -0
- package/src/builders/TagAddressBuilder.js +243 -0
- package/src/builders/index.js +38 -0
- package/src/constants/assetTypes.js +23 -0
- package/src/constants/burnAddresses.js +65 -0
- package/src/constants/fees.js +61 -0
- package/src/constants/index.js +44 -0
- package/src/constants/networks.js +112 -0
- package/src/errors/AssetErrors.js +135 -0
- package/src/errors/ValidationErrors.js +87 -0
- package/src/errors/index.js +56 -0
- package/src/index.js +68 -0
- package/src/managers/BurnManager.js +222 -0
- package/src/managers/OutputOrderer.js +289 -0
- package/src/managers/OwnerTokenManager.js +265 -0
- package/src/managers/UTXOSelector.js +309 -0
- package/src/managers/index.js +16 -0
- package/src/queries/AssetQueries.js +447 -0
- package/src/queries/index.js +10 -0
- package/src/utils/amountConverter.js +115 -0
- package/src/utils/assetNameParser.js +203 -0
- package/src/utils/index.js +16 -0
- package/src/utils/networkDetector.js +144 -0
- package/src/utils/outputFormatter.js +292 -0
- package/src/validators/amountValidator.js +149 -0
- package/src/validators/assetNameValidator.js +296 -0
- package/src/validators/index.js +16 -0
- package/src/validators/ipfsValidator.js +101 -0
- package/src/validators/verifierValidator.js +146 -0
- package/tests/README.md +126 -0
- package/tests/integration/assetLifecycle.test.js +244 -0
- package/tests/mocks/rpcMock.js +156 -0
- package/tests/unit/NeuraiAssets.test.js +217 -0
- package/tests/unit/utils/amountConverter.test.js +171 -0
- package/tests/unit/utils/assetNameParser.test.js +203 -0
- package/tests/unit/validators/amountValidator.test.js +143 -0
- 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;
|