@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,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTXO Selector
|
|
3
|
+
* Selects appropriate UTXOs for asset transactions
|
|
4
|
+
*
|
|
5
|
+
* Handles selection of:
|
|
6
|
+
* - Base currency (XNA) UTXOs for fees and burns
|
|
7
|
+
* - Asset UTXOs for transfers and operations
|
|
8
|
+
* - Mempool filtering to prevent double-spending
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { InsufficientFundsError } = require('../errors');
|
|
12
|
+
|
|
13
|
+
class UTXOSelector {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Function} rpc - RPC function to call Neurai node
|
|
16
|
+
*/
|
|
17
|
+
constructor(rpc) {
|
|
18
|
+
if (!rpc || typeof rpc !== 'function') {
|
|
19
|
+
throw new Error('RPC function is required');
|
|
20
|
+
}
|
|
21
|
+
this.rpc = rpc;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get all UTXOs for addresses
|
|
26
|
+
* @param {string[]} addresses - Array of wallet addresses
|
|
27
|
+
* @param {string|null} assetName - Filter by asset name (null for XNA)
|
|
28
|
+
* @returns {Promise<Array>} Array of UTXOs
|
|
29
|
+
*/
|
|
30
|
+
async getUTXOs(addresses, assetName = null) {
|
|
31
|
+
if (!Array.isArray(addresses) || addresses.length === 0) {
|
|
32
|
+
throw new Error('Addresses array is required');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const params = assetName
|
|
37
|
+
? [{ addresses, assetName }]
|
|
38
|
+
: [{ addresses }];
|
|
39
|
+
|
|
40
|
+
const utxos = await this.rpc('getaddressutxos', params);
|
|
41
|
+
|
|
42
|
+
// Filter for specific asset or XNA
|
|
43
|
+
if (assetName) {
|
|
44
|
+
return utxos.filter(utxo => utxo.assetName === assetName);
|
|
45
|
+
} else {
|
|
46
|
+
// XNA UTXOs don't have assetName or have assetName === 'XNA'
|
|
47
|
+
return utxos.filter(utxo => !utxo.assetName || utxo.assetName === 'XNA');
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new Error(`Failed to get UTXOs: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get mempool entries for addresses
|
|
56
|
+
* Used to filter out UTXOs that are already being spent
|
|
57
|
+
* @param {string[]} addresses - Array of wallet addresses
|
|
58
|
+
* @returns {Promise<Array>} Array of mempool entries
|
|
59
|
+
*/
|
|
60
|
+
async getMempoolEntries(addresses) {
|
|
61
|
+
if (!Array.isArray(addresses) || addresses.length === 0) {
|
|
62
|
+
throw new Error('Addresses array is required');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const mempool = await this.rpc('getaddressmempool', [{ addresses }]);
|
|
67
|
+
return mempool || [];
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// If RPC method not available, return empty array
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Filter out UTXOs that are being spent in mempool
|
|
76
|
+
* @param {Array} utxos - Array of UTXOs
|
|
77
|
+
* @param {Array} mempoolEntries - Array of mempool entries
|
|
78
|
+
* @returns {Array} Filtered UTXOs
|
|
79
|
+
*/
|
|
80
|
+
filterMempoolSpentUTXOs(utxos, mempoolEntries) {
|
|
81
|
+
if (mempoolEntries.length === 0) {
|
|
82
|
+
return utxos;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return utxos.filter(utxo => {
|
|
86
|
+
// Check if this UTXO is being spent in mempool
|
|
87
|
+
const isSpent = mempoolEntries.some(entry => {
|
|
88
|
+
return entry.prevtxid === utxo.txid && entry.prevout === utxo.outputIndex;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return !isSpent;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Select UTXOs for base currency (XNA)
|
|
97
|
+
* Uses greedy algorithm: selects UTXOs until sum >= required amount
|
|
98
|
+
*
|
|
99
|
+
* @param {string[]} addresses - Wallet addresses
|
|
100
|
+
* @param {number} requiredAmount - Required amount in XNA
|
|
101
|
+
* @param {number} buffer - Safety buffer percentage (default: 0.1 = 10%)
|
|
102
|
+
* @returns {Promise<object>} { utxos, totalAmount }
|
|
103
|
+
* @throws {InsufficientFundsError} If not enough funds
|
|
104
|
+
*/
|
|
105
|
+
async selectBaseCurrencyUTXOs(addresses, requiredAmount, buffer = 0.1) {
|
|
106
|
+
// Get all XNA UTXOs
|
|
107
|
+
const allUTXOs = await this.getUTXOs(addresses, null);
|
|
108
|
+
|
|
109
|
+
// Get mempool and filter
|
|
110
|
+
const mempool = await this.getMempoolEntries(addresses);
|
|
111
|
+
const availableUTXOs = this.filterMempoolSpentUTXOs(allUTXOs, mempool);
|
|
112
|
+
|
|
113
|
+
// Sort by value (largest first) for efficiency
|
|
114
|
+
const sortedUTXOs = availableUTXOs.sort((a, b) => b.satoshis - a.satoshis);
|
|
115
|
+
|
|
116
|
+
// Add buffer to required amount
|
|
117
|
+
const requiredWithBuffer = requiredAmount * (1 + buffer);
|
|
118
|
+
|
|
119
|
+
// Select UTXOs greedily
|
|
120
|
+
const selected = [];
|
|
121
|
+
let totalSatoshis = 0;
|
|
122
|
+
const requiredSatoshis = Math.ceil(requiredWithBuffer * 100000000); // Convert XNA to satoshis
|
|
123
|
+
|
|
124
|
+
for (const utxo of sortedUTXOs) {
|
|
125
|
+
selected.push(utxo);
|
|
126
|
+
totalSatoshis += utxo.satoshis;
|
|
127
|
+
|
|
128
|
+
if (totalSatoshis >= requiredSatoshis) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if we have enough
|
|
134
|
+
if (totalSatoshis < requiredSatoshis) {
|
|
135
|
+
const available = totalSatoshis / 100000000;
|
|
136
|
+
throw new InsufficientFundsError(
|
|
137
|
+
`Insufficient XNA balance. Required: ${requiredAmount.toFixed(8)} XNA (+ ${(buffer * 100).toFixed(0)}% buffer), ` +
|
|
138
|
+
`Available: ${available.toFixed(8)} XNA`,
|
|
139
|
+
requiredAmount,
|
|
140
|
+
available
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
utxos: selected,
|
|
146
|
+
totalAmount: totalSatoshis / 100000000
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Select UTXOs for asset transfer
|
|
152
|
+
* @param {string[]} addresses - Wallet addresses
|
|
153
|
+
* @param {string} assetName - Asset name
|
|
154
|
+
* @param {number} requiredAmount - Required amount (in asset units)
|
|
155
|
+
* @returns {Promise<object>} { utxos, totalAmount }
|
|
156
|
+
* @throws {InsufficientFundsError} If not enough asset balance
|
|
157
|
+
*/
|
|
158
|
+
async selectAssetUTXOs(addresses, assetName, requiredAmount) {
|
|
159
|
+
if (!assetName) {
|
|
160
|
+
throw new Error('Asset name is required');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Get all asset UTXOs
|
|
164
|
+
const allUTXOs = await this.getUTXOs(addresses, assetName);
|
|
165
|
+
|
|
166
|
+
// Get mempool and filter
|
|
167
|
+
const mempool = await this.getMempoolEntries(addresses);
|
|
168
|
+
const availableUTXOs = this.filterMempoolSpentUTXOs(allUTXOs, mempool);
|
|
169
|
+
|
|
170
|
+
// Sort by value (largest first)
|
|
171
|
+
const sortedUTXOs = availableUTXOs.sort((a, b) => b.satoshis - a.satoshis);
|
|
172
|
+
|
|
173
|
+
// Select UTXOs greedily
|
|
174
|
+
const selected = [];
|
|
175
|
+
let totalSatoshis = 0;
|
|
176
|
+
const requiredSatoshis = Math.ceil(requiredAmount * 100000000); // Assuming 8 decimals
|
|
177
|
+
|
|
178
|
+
for (const utxo of sortedUTXOs) {
|
|
179
|
+
selected.push(utxo);
|
|
180
|
+
totalSatoshis += utxo.satoshis;
|
|
181
|
+
|
|
182
|
+
if (totalSatoshis >= requiredSatoshis) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check if we have enough
|
|
188
|
+
if (totalSatoshis < requiredSatoshis) {
|
|
189
|
+
const available = totalSatoshis / 100000000;
|
|
190
|
+
throw new InsufficientFundsError(
|
|
191
|
+
`Insufficient ${assetName} balance. Required: ${requiredAmount}, Available: ${available}`,
|
|
192
|
+
requiredAmount,
|
|
193
|
+
available
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
utxos: selected,
|
|
199
|
+
totalAmount: totalSatoshis / 100000000
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Select UTXOs for a transaction requiring both XNA and assets
|
|
205
|
+
* @param {string[]} addresses - Wallet addresses
|
|
206
|
+
* @param {number} xnaAmount - Required XNA amount
|
|
207
|
+
* @param {string|null} assetName - Asset name (null if not needed)
|
|
208
|
+
* @param {number} assetAmount - Required asset amount
|
|
209
|
+
* @returns {Promise<object>} { xnaUTXOs, assetUTXOs, totalXNA, totalAsset }
|
|
210
|
+
*/
|
|
211
|
+
async selectMixedUTXOs(addresses, xnaAmount, assetName = null, assetAmount = 0) {
|
|
212
|
+
const result = {
|
|
213
|
+
xnaUTXOs: [],
|
|
214
|
+
assetUTXOs: [],
|
|
215
|
+
totalXNA: 0,
|
|
216
|
+
totalAsset: 0
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Select XNA UTXOs if needed
|
|
220
|
+
if (xnaAmount > 0) {
|
|
221
|
+
const xnaSelection = await this.selectBaseCurrencyUTXOs(addresses, xnaAmount);
|
|
222
|
+
result.xnaUTXOs = xnaSelection.utxos;
|
|
223
|
+
result.totalXNA = xnaSelection.totalAmount;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Select asset UTXOs if needed
|
|
227
|
+
if (assetName && assetAmount > 0) {
|
|
228
|
+
const assetSelection = await this.selectAssetUTXOs(addresses, assetName, assetAmount);
|
|
229
|
+
result.assetUTXOs = assetSelection.utxos;
|
|
230
|
+
result.totalAsset = assetSelection.totalAmount;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get total balance for an asset
|
|
238
|
+
* @param {string[]} addresses - Wallet addresses
|
|
239
|
+
* @param {string|null} assetName - Asset name (null for XNA)
|
|
240
|
+
* @returns {Promise<number>} Total balance
|
|
241
|
+
*/
|
|
242
|
+
async getBalance(addresses, assetName = null) {
|
|
243
|
+
const utxos = await this.getUTXOs(addresses, assetName);
|
|
244
|
+
const mempool = await this.getMempoolEntries(addresses);
|
|
245
|
+
const availableUTXOs = this.filterMempoolSpentUTXOs(utxos, mempool);
|
|
246
|
+
|
|
247
|
+
const totalSatoshis = availableUTXOs.reduce((sum, utxo) => sum + utxo.satoshis, 0);
|
|
248
|
+
return totalSatoshis / 100000000;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Estimate transaction size in bytes
|
|
253
|
+
* Used for fee calculation
|
|
254
|
+
*
|
|
255
|
+
* @param {number} inputCount - Number of inputs
|
|
256
|
+
* @param {number} outputCount - Number of outputs
|
|
257
|
+
* @returns {number} Estimated size in bytes
|
|
258
|
+
*/
|
|
259
|
+
estimateTransactionSize(inputCount, outputCount) {
|
|
260
|
+
// Rough estimation:
|
|
261
|
+
// - Each input: ~180 bytes
|
|
262
|
+
// - Each output: ~34 bytes
|
|
263
|
+
// - Transaction overhead: ~10 bytes
|
|
264
|
+
const inputSize = inputCount * 180;
|
|
265
|
+
const outputSize = outputCount * 34;
|
|
266
|
+
const overhead = 10;
|
|
267
|
+
|
|
268
|
+
return inputSize + outputSize + overhead;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Estimate fee for a transaction
|
|
273
|
+
* @param {number} inputCount - Number of inputs
|
|
274
|
+
* @param {number} outputCount - Number of outputs
|
|
275
|
+
* @param {number} feeRate - Fee rate in XNA per KB (default: 0.015)
|
|
276
|
+
* @returns {number} Estimated fee in XNA
|
|
277
|
+
*/
|
|
278
|
+
estimateFee(inputCount, outputCount, feeRate = 0.015) {
|
|
279
|
+
const sizeBytes = this.estimateTransactionSize(inputCount, outputCount);
|
|
280
|
+
const sizeKB = sizeBytes / 1000;
|
|
281
|
+
const fee = sizeKB * feeRate;
|
|
282
|
+
|
|
283
|
+
// Round up to 8 decimals
|
|
284
|
+
return Math.ceil(fee * 100000000) / 100000000;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get fee rate from network
|
|
289
|
+
* @param {number} confirmationTarget - Target confirmations (default: 20)
|
|
290
|
+
* @returns {Promise<number>} Fee rate in XNA per KB
|
|
291
|
+
*/
|
|
292
|
+
async getFeeRate(confirmationTarget = 20) {
|
|
293
|
+
try {
|
|
294
|
+
const result = await this.rpc('estimatesmartfee', [confirmationTarget]);
|
|
295
|
+
|
|
296
|
+
if (result && result.feerate && result.feerate > 0) {
|
|
297
|
+
return result.feerate;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Fallback to default
|
|
301
|
+
return 0.015;
|
|
302
|
+
} catch (error) {
|
|
303
|
+
// Fallback to default if estimation fails
|
|
304
|
+
return 0.015;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = UTXOSelector;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managers Module
|
|
3
|
+
* Exports all manager classes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const BurnManager = require('./BurnManager');
|
|
7
|
+
const OwnerTokenManager = require('./OwnerTokenManager');
|
|
8
|
+
const UTXOSelector = require('./UTXOSelector');
|
|
9
|
+
const OutputOrderer = require('./OutputOrderer');
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
BurnManager,
|
|
13
|
+
OwnerTokenManager,
|
|
14
|
+
UTXOSelector,
|
|
15
|
+
OutputOrderer
|
|
16
|
+
};
|