@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,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset Queries
|
|
3
|
+
* Wrapper methods for querying asset information from the blockchain
|
|
4
|
+
*
|
|
5
|
+
* Provides convenient access to all asset-related RPC query methods:
|
|
6
|
+
* - Asset metadata (getassetdata)
|
|
7
|
+
* - Asset listings (listassets, listmyassets)
|
|
8
|
+
* - Holder information (listaddressesbyasset)
|
|
9
|
+
* - Balance queries (listassetbalancesbyaddress)
|
|
10
|
+
* - Qualifier checks (checkaddresstag, listtagsforaddress)
|
|
11
|
+
* - Restriction checks (checkaddressrestriction, checkglobalrestriction)
|
|
12
|
+
* - Verifier validation (isvalidverifierstring)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { AssetNotFoundError, InvalidAddressError } = require('../errors');
|
|
16
|
+
|
|
17
|
+
class AssetQueries {
|
|
18
|
+
/**
|
|
19
|
+
* @param {Function} rpc - RPC function to call Neurai node
|
|
20
|
+
*/
|
|
21
|
+
constructor(rpc) {
|
|
22
|
+
if (!rpc || typeof rpc !== 'function') {
|
|
23
|
+
throw new Error('RPC function is required');
|
|
24
|
+
}
|
|
25
|
+
this.rpc = rpc;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get asset metadata
|
|
30
|
+
* @param {string} assetName - Asset name
|
|
31
|
+
* @returns {Promise<object>} Asset data
|
|
32
|
+
* @throws {AssetNotFoundError} If asset doesn't exist
|
|
33
|
+
*/
|
|
34
|
+
async getAssetData(assetName) {
|
|
35
|
+
if (!assetName) {
|
|
36
|
+
throw new Error('Asset name is required');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const assetData = await this.rpc('getassetdata', [assetName]);
|
|
41
|
+
|
|
42
|
+
if (!assetData) {
|
|
43
|
+
throw new AssetNotFoundError(
|
|
44
|
+
`Asset ${assetName} not found on blockchain`,
|
|
45
|
+
assetName
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return assetData;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof AssetNotFoundError) {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// RPC error - likely asset doesn't exist
|
|
56
|
+
if (error.message && error.message.includes('not found')) {
|
|
57
|
+
throw new AssetNotFoundError(
|
|
58
|
+
`Asset ${assetName} not found on blockchain`,
|
|
59
|
+
assetName
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw new Error(`Failed to get asset data: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* List all assets on the blockchain
|
|
69
|
+
* @param {string} filter - Filter pattern (e.g., 'MY*' for all assets starting with MY)
|
|
70
|
+
* @param {boolean} verbose - Include detailed information
|
|
71
|
+
* @param {number} count - Maximum number to return
|
|
72
|
+
* @param {number} start - Starting index (for pagination)
|
|
73
|
+
* @returns {Promise<Array|object>} Array of asset names or detailed objects
|
|
74
|
+
*/
|
|
75
|
+
async listAssets(filter = '*', verbose = false, count = 100, start = 0) {
|
|
76
|
+
try {
|
|
77
|
+
const assets = await this.rpc('listassets', [filter, verbose, count, start]);
|
|
78
|
+
return assets || [];
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(`Failed to list assets: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* List assets owned by wallet
|
|
86
|
+
* @param {string} assetName - Filter by asset name (default: '*' for all)
|
|
87
|
+
* @param {boolean} verbose - Include detailed information
|
|
88
|
+
* @param {number} count - Maximum number to return
|
|
89
|
+
* @param {number} start - Starting index (for pagination)
|
|
90
|
+
* @param {number} confs - Minimum confirmations (default: 1)
|
|
91
|
+
* @returns {Promise<object>} Object with asset names as keys and amounts as values
|
|
92
|
+
*/
|
|
93
|
+
async listMyAssets(assetName = '*', verbose = false, count = 100, start = 0, confs = 1) {
|
|
94
|
+
try {
|
|
95
|
+
const myAssets = await this.rpc('listmyassets', [assetName, verbose, count, start, confs]);
|
|
96
|
+
return myAssets || {};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw new Error(`Failed to list my assets: ${error.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* List all addresses holding a specific asset
|
|
104
|
+
* @param {string} assetName - Asset name
|
|
105
|
+
* @param {boolean} onlyCount - Return only count instead of full list
|
|
106
|
+
* @param {number} count - Maximum number to return
|
|
107
|
+
* @param {number} start - Starting index (for pagination)
|
|
108
|
+
* @returns {Promise<Array|number>} Array of {address, amount} or count
|
|
109
|
+
*/
|
|
110
|
+
async listAddressesByAsset(assetName, onlyCount = false, count = 100, start = 0) {
|
|
111
|
+
if (!assetName) {
|
|
112
|
+
throw new Error('Asset name is required');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const result = await this.rpc('listaddressesbyasset', [assetName, onlyCount, count, start]);
|
|
117
|
+
return result || (onlyCount ? 0 : []);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error.message && error.message.includes('not found')) {
|
|
120
|
+
throw new AssetNotFoundError(
|
|
121
|
+
`Asset ${assetName} not found on blockchain`,
|
|
122
|
+
assetName
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Failed to list addresses by asset: ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* List asset balances for a specific address
|
|
131
|
+
* @param {string} address - Address to query
|
|
132
|
+
* @param {boolean} onlyTotal - Return only count instead of full list
|
|
133
|
+
* @param {number} count - Maximum number to return
|
|
134
|
+
* @param {number} start - Starting index (for pagination)
|
|
135
|
+
* @returns {Promise<Array|number>} Array of {asset, amount} or count
|
|
136
|
+
*/
|
|
137
|
+
async listAssetBalancesByAddress(address, onlyTotal = false, count = 100, start = 0) {
|
|
138
|
+
if (!address) {
|
|
139
|
+
throw new Error('Address is required');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const result = await this.rpc('listassetbalancesbyaddress', [address, onlyTotal, count, start]);
|
|
144
|
+
return result || (onlyTotal ? 0 : []);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw new Error(`Failed to list asset balances by address: ${error.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if an address has a specific qualifier tag
|
|
152
|
+
* @param {string} address - Address to check
|
|
153
|
+
* @param {string} qualifierName - Qualifier name (e.g., '#KYC_VERIFIED')
|
|
154
|
+
* @returns {Promise<boolean>} True if address has the tag
|
|
155
|
+
*/
|
|
156
|
+
async checkAddressTag(address, qualifierName) {
|
|
157
|
+
if (!address) {
|
|
158
|
+
throw new Error('Address is required');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!qualifierName) {
|
|
162
|
+
throw new Error('Qualifier name is required');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const result = await this.rpc('checkaddresstag', [address, qualifierName]);
|
|
167
|
+
return result === true || result === 1;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
// If tag doesn't exist or address doesn't have it, return false
|
|
170
|
+
if (error.message && (error.message.includes('not found') || error.message.includes('does not have'))) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
throw new Error(`Failed to check address tag: ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* List all qualifiers assigned to an address
|
|
179
|
+
* @param {string} address - Address to query
|
|
180
|
+
* @returns {Promise<Array>} Array of qualifier names
|
|
181
|
+
*/
|
|
182
|
+
async listTagsForAddress(address) {
|
|
183
|
+
if (!address) {
|
|
184
|
+
throw new Error('Address is required');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const tags = await this.rpc('listtagsforaddress', [address]);
|
|
189
|
+
return tags || [];
|
|
190
|
+
} catch (error) {
|
|
191
|
+
// If no tags found, return empty array
|
|
192
|
+
if (error.message && error.message.includes('not found')) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
throw new Error(`Failed to list tags for address: ${error.message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* List all addresses with a specific qualifier tag
|
|
201
|
+
* @param {string} qualifierName - Qualifier name
|
|
202
|
+
* @returns {Promise<Array>} Array of addresses
|
|
203
|
+
*/
|
|
204
|
+
async listAddressesForTag(qualifierName) {
|
|
205
|
+
if (!qualifierName) {
|
|
206
|
+
throw new Error('Qualifier name is required');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const addresses = await this.rpc('listaddressesfortag', [qualifierName]);
|
|
211
|
+
return addresses || [];
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error.message && error.message.includes('not found')) {
|
|
214
|
+
throw new AssetNotFoundError(
|
|
215
|
+
`Qualifier ${qualifierName} not found on blockchain`,
|
|
216
|
+
qualifierName
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
throw new Error(`Failed to list addresses for tag: ${error.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if an address can hold a restricted asset
|
|
225
|
+
* @param {string} address - Address to check
|
|
226
|
+
* @param {string} restrictedAssetName - Restricted asset name (e.g., '$SECURITY')
|
|
227
|
+
* @returns {Promise<boolean>} True if address meets verifier requirements
|
|
228
|
+
*/
|
|
229
|
+
async checkAddressRestriction(address, restrictedAssetName) {
|
|
230
|
+
if (!address) {
|
|
231
|
+
throw new Error('Address is required');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!restrictedAssetName) {
|
|
235
|
+
throw new Error('Restricted asset name is required');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const result = await this.rpc('checkaddressrestriction', [address, restrictedAssetName]);
|
|
240
|
+
return result === true || result === 1;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
// If address doesn't meet requirements, return false
|
|
243
|
+
if (error.message && (error.message.includes('not found') || error.message.includes('does not meet'))) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
throw new Error(`Failed to check address restriction: ${error.message}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Check if an address is frozen for a restricted asset
|
|
252
|
+
* @param {string} address - Address to check
|
|
253
|
+
* @param {string} restrictedAssetName - Restricted asset name
|
|
254
|
+
* @returns {Promise<boolean>} True if address is frozen
|
|
255
|
+
*/
|
|
256
|
+
async isAddressFrozen(address, restrictedAssetName) {
|
|
257
|
+
if (!address) {
|
|
258
|
+
throw new Error('Address is required');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!restrictedAssetName) {
|
|
262
|
+
throw new Error('Restricted asset name is required');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const result = await this.rpc('checkaddressrestriction', [address, restrictedAssetName]);
|
|
267
|
+
// If result has frozen property, check it
|
|
268
|
+
if (typeof result === 'object' && result.frozen !== undefined) {
|
|
269
|
+
return result.frozen === true || result.frozen === 1;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
throw new Error(`Failed to check if address is frozen: ${error.message}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Check if an asset is globally frozen
|
|
279
|
+
* @param {string} restrictedAssetName - Restricted asset name
|
|
280
|
+
* @returns {Promise<boolean>} True if asset is globally frozen
|
|
281
|
+
*/
|
|
282
|
+
async checkGlobalRestriction(restrictedAssetName) {
|
|
283
|
+
if (!restrictedAssetName) {
|
|
284
|
+
throw new Error('Restricted asset name is required');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const result = await this.rpc('checkglobalrestriction', [restrictedAssetName]);
|
|
289
|
+
return result === true || result === 1;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (error.message && error.message.includes('not found')) {
|
|
292
|
+
throw new AssetNotFoundError(
|
|
293
|
+
`Restricted asset ${restrictedAssetName} not found on blockchain`,
|
|
294
|
+
restrictedAssetName
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
throw new Error(`Failed to check global restriction: ${error.message}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get verifier string for a restricted asset
|
|
303
|
+
* @param {string} restrictedAssetName - Restricted asset name
|
|
304
|
+
* @returns {Promise<string>} Verifier string
|
|
305
|
+
*/
|
|
306
|
+
async getVerifierString(restrictedAssetName) {
|
|
307
|
+
if (!restrictedAssetName) {
|
|
308
|
+
throw new Error('Restricted asset name is required');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const result = await this.rpc('getverifierstring', [restrictedAssetName]);
|
|
313
|
+
return result || '';
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (error.message && error.message.includes('not found')) {
|
|
316
|
+
throw new AssetNotFoundError(
|
|
317
|
+
`Restricted asset ${restrictedAssetName} not found on blockchain`,
|
|
318
|
+
restrictedAssetName
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
throw new Error(`Failed to get verifier string: ${error.message}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Validate verifier string syntax
|
|
327
|
+
* @param {string} verifierString - Verifier string to validate
|
|
328
|
+
* @returns {Promise<boolean>} True if valid
|
|
329
|
+
*/
|
|
330
|
+
async isValidVerifierString(verifierString) {
|
|
331
|
+
if (!verifierString) {
|
|
332
|
+
throw new Error('Verifier string is required');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const result = await this.rpc('isvalidverifierstring', [verifierString]);
|
|
337
|
+
return result === true || result === 1;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
// If validation fails, return false
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get snapshot of asset ownership at a specific block
|
|
346
|
+
* @param {string} assetName - Asset name
|
|
347
|
+
* @param {number} blockHeight - Block height for snapshot
|
|
348
|
+
* @returns {Promise<object>} Snapshot request result
|
|
349
|
+
*/
|
|
350
|
+
async getSnapshotRequest(assetName, blockHeight) {
|
|
351
|
+
if (!assetName) {
|
|
352
|
+
throw new Error('Asset name is required');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!blockHeight || typeof blockHeight !== 'number') {
|
|
356
|
+
throw new Error('Block height must be a number');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const result = await this.rpc('getsnapshotrequest', [assetName, blockHeight]);
|
|
361
|
+
return result;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
throw new Error(`Failed to get snapshot request: ${error.message}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Cancel a snapshot request
|
|
369
|
+
* @param {string} assetName - Asset name
|
|
370
|
+
* @param {number} blockHeight - Block height of snapshot to cancel
|
|
371
|
+
* @returns {Promise<boolean>} True if cancelled successfully
|
|
372
|
+
*/
|
|
373
|
+
async cancelSnapshotRequest(assetName, blockHeight) {
|
|
374
|
+
if (!assetName) {
|
|
375
|
+
throw new Error('Asset name is required');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!blockHeight || typeof blockHeight !== 'number') {
|
|
379
|
+
throw new Error('Block height must be a number');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const result = await this.rpc('cancelsnapshotrequest', [assetName, blockHeight]);
|
|
384
|
+
return result === true || result === 1;
|
|
385
|
+
} catch (error) {
|
|
386
|
+
throw new Error(`Failed to cancel snapshot request: ${error.message}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get total count of assets on blockchain
|
|
392
|
+
* @returns {Promise<number>} Total asset count
|
|
393
|
+
*/
|
|
394
|
+
async getAssetCount() {
|
|
395
|
+
try {
|
|
396
|
+
// List all assets with count only
|
|
397
|
+
const assets = await this.listAssets('*', false, 1, 0);
|
|
398
|
+
return Array.isArray(assets) ? assets.length : 0;
|
|
399
|
+
} catch (error) {
|
|
400
|
+
throw new Error(`Failed to get asset count: ${error.message}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Check if asset exists
|
|
406
|
+
* @param {string} assetName - Asset name
|
|
407
|
+
* @returns {Promise<boolean>} True if asset exists
|
|
408
|
+
*/
|
|
409
|
+
async assetExists(assetName) {
|
|
410
|
+
try {
|
|
411
|
+
await this.getAssetData(assetName);
|
|
412
|
+
return true;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
if (error instanceof AssetNotFoundError) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get asset type from name
|
|
423
|
+
* @param {string} assetName - Asset name
|
|
424
|
+
* @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'OWNER')
|
|
425
|
+
*/
|
|
426
|
+
getAssetType(assetName) {
|
|
427
|
+
if (!assetName) {
|
|
428
|
+
throw new Error('Asset name is required');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (assetName.endsWith('!')) {
|
|
432
|
+
return 'OWNER';
|
|
433
|
+
} else if (assetName.startsWith('#')) {
|
|
434
|
+
return assetName.includes('/') ? 'SUB_QUALIFIER' : 'QUALIFIER';
|
|
435
|
+
} else if (assetName.startsWith('$')) {
|
|
436
|
+
return 'RESTRICTED';
|
|
437
|
+
} else if (assetName.includes('#')) {
|
|
438
|
+
return 'UNIQUE';
|
|
439
|
+
} else if (assetName.includes('/')) {
|
|
440
|
+
return 'SUB';
|
|
441
|
+
} else {
|
|
442
|
+
return 'ROOT';
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
module.exports = AssetQueries;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Amount Converter
|
|
3
|
+
* Converts between user amounts and satoshis (protocol internal format)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class AmountConverter {
|
|
7
|
+
/**
|
|
8
|
+
* Convert user amount to satoshis
|
|
9
|
+
* @param {number} amount - User-friendly amount (e.g., 1.5)
|
|
10
|
+
* @param {number} units - Decimal places (0-8)
|
|
11
|
+
* @returns {number} Amount in satoshis
|
|
12
|
+
*/
|
|
13
|
+
static toSatoshis(amount, units) {
|
|
14
|
+
if (typeof amount !== 'number' || isNaN(amount)) {
|
|
15
|
+
throw new Error('Amount must be a valid number');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof units !== 'number' || isNaN(units) || units < 0 || units > 8) {
|
|
19
|
+
throw new Error('Units must be a number between 0 and 8');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Calculate multiplier
|
|
23
|
+
const multiplier = Math.pow(10, units);
|
|
24
|
+
|
|
25
|
+
// Convert to satoshis and round to avoid floating point issues
|
|
26
|
+
const satoshis = Math.round(amount * multiplier);
|
|
27
|
+
|
|
28
|
+
return satoshis;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Convert satoshis to user amount
|
|
33
|
+
* @param {number} satoshis - Amount in satoshis
|
|
34
|
+
* @param {number} units - Decimal places (0-8)
|
|
35
|
+
* @returns {number} User-friendly amount
|
|
36
|
+
*/
|
|
37
|
+
static fromSatoshis(satoshis, units) {
|
|
38
|
+
if (typeof satoshis !== 'number' || isNaN(satoshis)) {
|
|
39
|
+
throw new Error('Satoshis must be a valid number');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (typeof units !== 'number' || isNaN(units) || units < 0 || units > 8) {
|
|
43
|
+
throw new Error('Units must be a number between 0 and 8');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Calculate divisor
|
|
47
|
+
const divisor = Math.pow(10, units);
|
|
48
|
+
|
|
49
|
+
// Convert to user amount
|
|
50
|
+
const amount = satoshis / divisor;
|
|
51
|
+
|
|
52
|
+
return amount;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format amount with proper decimal places
|
|
57
|
+
* @param {number} amount - Amount to format
|
|
58
|
+
* @param {number} units - Decimal places
|
|
59
|
+
* @returns {string} Formatted amount
|
|
60
|
+
*/
|
|
61
|
+
static format(amount, units) {
|
|
62
|
+
if (units === 0) {
|
|
63
|
+
return amount.toString();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return amount.toFixed(units);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse formatted amount string
|
|
71
|
+
* @param {string} formattedAmount - Formatted amount string
|
|
72
|
+
* @returns {number} Parsed amount
|
|
73
|
+
*/
|
|
74
|
+
static parse(formattedAmount) {
|
|
75
|
+
const num = parseFloat(formattedAmount);
|
|
76
|
+
if (isNaN(num)) {
|
|
77
|
+
throw new Error('Invalid number format');
|
|
78
|
+
}
|
|
79
|
+
return num;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get decimal places from amount
|
|
84
|
+
* @param {number} amount - Amount to check
|
|
85
|
+
* @returns {number} Number of decimal places
|
|
86
|
+
*/
|
|
87
|
+
static getDecimalPlaces(amount) {
|
|
88
|
+
const match = ('' + amount).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
|
|
89
|
+
if (!match) return 0;
|
|
90
|
+
return Math.max(
|
|
91
|
+
0,
|
|
92
|
+
(match[1] ? match[1].length : 0) - (match[2] ? +match[2] : 0)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Adjust amount to proper units
|
|
98
|
+
* If amount has more decimals than units allow, round it
|
|
99
|
+
* @param {number} amount - Amount to adjust
|
|
100
|
+
* @param {number} units - Target decimal places
|
|
101
|
+
* @returns {number} Adjusted amount
|
|
102
|
+
*/
|
|
103
|
+
static adjustToUnits(amount, units) {
|
|
104
|
+
const decimalPlaces = this.getDecimalPlaces(amount);
|
|
105
|
+
if (decimalPlaces <= units) {
|
|
106
|
+
return amount;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Round to units decimal places
|
|
110
|
+
const multiplier = Math.pow(10, units);
|
|
111
|
+
return Math.round(amount * multiplier) / multiplier;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = AmountConverter;
|