@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,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;
|