@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,149 @@
1
+ /**
2
+ * Amount Validator
3
+ * Validates asset quantities and units
4
+ */
5
+
6
+ const { ASSET_LIMITS } = require('../constants');
7
+ const { InvalidAmountError, InvalidUnitsError } = require('../errors');
8
+
9
+ class AmountValidator {
10
+ /**
11
+ * Validate asset quantity
12
+ * @param {number} quantity - Asset quantity
13
+ * @param {number} units - Decimal places (0-8)
14
+ */
15
+ static validate(quantity, units = 0) {
16
+ // Validate quantity is a number
17
+ if (typeof quantity !== 'number' || isNaN(quantity)) {
18
+ throw new InvalidAmountError('Quantity must be a valid number', quantity);
19
+ }
20
+
21
+ // Validate quantity is positive
22
+ if (quantity <= 0) {
23
+ throw new InvalidAmountError('Quantity must be greater than 0', quantity);
24
+ }
25
+
26
+ // Validate quantity is within limits
27
+ if (quantity < ASSET_LIMITS.MIN_QUANTITY) {
28
+ throw new InvalidAmountError(
29
+ `Quantity must be at least ${ASSET_LIMITS.MIN_QUANTITY}`,
30
+ quantity
31
+ );
32
+ }
33
+
34
+ if (quantity > ASSET_LIMITS.MAX_QUANTITY) {
35
+ throw new InvalidAmountError(
36
+ `Quantity cannot exceed ${ASSET_LIMITS.MAX_QUANTITY}`,
37
+ quantity
38
+ );
39
+ }
40
+
41
+ // Validate units
42
+ this.validateUnits(units);
43
+
44
+ // Validate quantity doesn't have more decimals than units allow
45
+ const decimalPlaces = this.getDecimalPlaces(quantity);
46
+ if (decimalPlaces > units) {
47
+ throw new InvalidAmountError(
48
+ `Quantity has ${decimalPlaces} decimal places but units is ${units}`,
49
+ quantity
50
+ );
51
+ }
52
+
53
+ return true;
54
+ }
55
+
56
+ /**
57
+ * Validate units (decimal places)
58
+ * @param {number} units - Decimal places (0-8)
59
+ */
60
+ static validateUnits(units) {
61
+ if (typeof units !== 'number' || isNaN(units)) {
62
+ throw new InvalidUnitsError('Units must be a valid number', units);
63
+ }
64
+
65
+ if (!Number.isInteger(units)) {
66
+ throw new InvalidUnitsError('Units must be an integer', units);
67
+ }
68
+
69
+ if (units < ASSET_LIMITS.MIN_UNITS || units > ASSET_LIMITS.MAX_UNITS) {
70
+ throw new InvalidUnitsError(
71
+ `Units must be between ${ASSET_LIMITS.MIN_UNITS} and ${ASSET_LIMITS.MAX_UNITS}`,
72
+ units
73
+ );
74
+ }
75
+
76
+ return true;
77
+ }
78
+
79
+ /**
80
+ * Validate qualifier quantity (1-10 only)
81
+ * @param {number} quantity - Qualifier quantity
82
+ */
83
+ static validateQualifierQuantity(quantity) {
84
+ if (typeof quantity !== 'number' || isNaN(quantity)) {
85
+ throw new InvalidAmountError('Qualifier quantity must be a valid number', quantity);
86
+ }
87
+
88
+ if (!Number.isInteger(quantity)) {
89
+ throw new InvalidAmountError('Qualifier quantity must be an integer', quantity);
90
+ }
91
+
92
+ if (quantity < ASSET_LIMITS.QUALIFIER_MIN_QUANTITY || quantity > ASSET_LIMITS.QUALIFIER_MAX_QUANTITY) {
93
+ throw new InvalidAmountError(
94
+ `Qualifier quantity must be between ${ASSET_LIMITS.QUALIFIER_MIN_QUANTITY} and ${ASSET_LIMITS.QUALIFIER_MAX_QUANTITY}`,
95
+ quantity
96
+ );
97
+ }
98
+
99
+ return true;
100
+ }
101
+
102
+ /**
103
+ * Validate owner token quantity (always 1)
104
+ * @param {number} quantity - Owner token quantity
105
+ */
106
+ static validateOwnerTokenQuantity(quantity) {
107
+ if (quantity !== ASSET_LIMITS.OWNER_TOKEN_QUANTITY) {
108
+ throw new InvalidAmountError(
109
+ `Owner token quantity must be exactly ${ASSET_LIMITS.OWNER_TOKEN_QUANTITY}`,
110
+ quantity
111
+ );
112
+ }
113
+
114
+ return true;
115
+ }
116
+
117
+ /**
118
+ * Get number of decimal places in a number
119
+ * @param {number} num - Number to check
120
+ * @returns {number} Number of decimal places
121
+ */
122
+ static getDecimalPlaces(num) {
123
+ const match = ('' + num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
124
+ if (!match) return 0;
125
+ return Math.max(
126
+ 0,
127
+ (match[1] ? match[1].length : 0) - (match[2] ? +match[2] : 0)
128
+ );
129
+ }
130
+
131
+ /**
132
+ * Validate that sum of amounts doesn't exceed max
133
+ * @param {number} current - Current amount
134
+ * @param {number} additional - Additional amount
135
+ * @returns {boolean} True if sum is valid
136
+ */
137
+ static validateSum(current, additional) {
138
+ if (current + additional > ASSET_LIMITS.MAX_QUANTITY) {
139
+ throw new InvalidAmountError(
140
+ `Sum of current (${current}) and additional (${additional}) exceeds maximum ${ASSET_LIMITS.MAX_QUANTITY}`,
141
+ current + additional
142
+ );
143
+ }
144
+
145
+ return true;
146
+ }
147
+ }
148
+
149
+ module.exports = AmountValidator;
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Asset Name Validator
3
+ * Validates asset names according to Neurai protocol rules
4
+ */
5
+
6
+ const { ASSET_NAME_RULES } = require('../constants');
7
+ const { InvalidAssetNameError } = require('../errors');
8
+
9
+ class AssetNameValidator {
10
+ /**
11
+ * Validate ROOT asset name
12
+ * Rules: 3-30 uppercase characters, A-Z, 0-9, underscore, period
13
+ * Cannot start with period, A, or Z
14
+ * Cannot be reserved names
15
+ */
16
+ static validateRoot(name) {
17
+ if (!name || typeof name !== 'string') {
18
+ throw new InvalidAssetNameError('Asset name must be a non-empty string', name);
19
+ }
20
+
21
+ // Length check
22
+ if (name.length < ASSET_NAME_RULES.ROOT.minLength || name.length > ASSET_NAME_RULES.ROOT.maxLength) {
23
+ throw new InvalidAssetNameError(
24
+ `ROOT asset name must be ${ASSET_NAME_RULES.ROOT.minLength}-${ASSET_NAME_RULES.ROOT.maxLength} characters`,
25
+ name
26
+ );
27
+ }
28
+
29
+ // Uppercase check
30
+ if (name !== name.toUpperCase()) {
31
+ throw new InvalidAssetNameError('Asset name must be uppercase', name);
32
+ }
33
+
34
+ // Starting character check
35
+ if (ASSET_NAME_RULES.ROOT.cannotStartWith.some(char => name.startsWith(char))) {
36
+ throw new InvalidAssetNameError(
37
+ `Asset name cannot start with: ${ASSET_NAME_RULES.ROOT.cannotStartWith.join(', ')}`,
38
+ name
39
+ );
40
+ }
41
+
42
+ // Pattern check
43
+ if (!ASSET_NAME_RULES.ROOT.pattern.test(name)) {
44
+ throw new InvalidAssetNameError(
45
+ 'Asset name can only contain A-Z, 0-9, underscore, and period',
46
+ name
47
+ );
48
+ }
49
+
50
+ // Reserved names check
51
+ if (ASSET_NAME_RULES.ROOT.reserved.includes(name)) {
52
+ throw new InvalidAssetNameError(`${name} is a reserved asset name`, name);
53
+ }
54
+
55
+ return true;
56
+ }
57
+
58
+ /**
59
+ * Validate SUB asset name
60
+ * Format: ROOT/SUBNAME
61
+ */
62
+ static validateSub(name) {
63
+ if (!name || typeof name !== 'string') {
64
+ throw new InvalidAssetNameError('SUB asset name must be a non-empty string', name);
65
+ }
66
+
67
+ const parts = name.split(ASSET_NAME_RULES.SUB.separator);
68
+ if (parts.length !== 2) {
69
+ throw new InvalidAssetNameError(
70
+ `SUB asset must be in ${ASSET_NAME_RULES.SUB.separator} format (ROOT/SUBNAME)`,
71
+ name
72
+ );
73
+ }
74
+
75
+ const [rootName, subName] = parts;
76
+
77
+ // Validate root part
78
+ this.validateRoot(rootName);
79
+
80
+ // Validate sub part
81
+ if (subName.length < ASSET_NAME_RULES.SUB.minLength || subName.length > ASSET_NAME_RULES.SUB.maxLength) {
82
+ throw new InvalidAssetNameError(
83
+ `SUB asset name must be ${ASSET_NAME_RULES.SUB.minLength}-${ASSET_NAME_RULES.SUB.maxLength} characters`,
84
+ name
85
+ );
86
+ }
87
+
88
+ if (subName !== subName.toUpperCase()) {
89
+ throw new InvalidAssetNameError('SUB asset name must be uppercase', name);
90
+ }
91
+
92
+ if (!ASSET_NAME_RULES.SUB.pattern.test(subName)) {
93
+ throw new InvalidAssetNameError(
94
+ 'SUB asset name can only contain A-Z, 0-9, underscore, and period',
95
+ name
96
+ );
97
+ }
98
+
99
+ return true;
100
+ }
101
+
102
+ /**
103
+ * Validate UNIQUE asset name
104
+ * Format: ROOT#TAG
105
+ */
106
+ static validateUnique(name) {
107
+ if (!name || typeof name !== 'string') {
108
+ throw new InvalidAssetNameError('UNIQUE asset name must be a non-empty string', name);
109
+ }
110
+
111
+ const parts = name.split(ASSET_NAME_RULES.UNIQUE.separator);
112
+ if (parts.length !== 2) {
113
+ throw new InvalidAssetNameError(
114
+ `UNIQUE asset must be in ROOT${ASSET_NAME_RULES.UNIQUE.separator}TAG format`,
115
+ name
116
+ );
117
+ }
118
+
119
+ const [rootName, tag] = parts;
120
+
121
+ // Validate root part
122
+ this.validateRoot(rootName);
123
+
124
+ // Validate tag
125
+ if (tag.length < ASSET_NAME_RULES.UNIQUE.minLength || tag.length > ASSET_NAME_RULES.UNIQUE.maxLength) {
126
+ throw new InvalidAssetNameError(
127
+ `UNIQUE tag must be ${ASSET_NAME_RULES.UNIQUE.minLength}-${ASSET_NAME_RULES.UNIQUE.maxLength} characters`,
128
+ name
129
+ );
130
+ }
131
+
132
+ if (tag !== tag.toUpperCase()) {
133
+ throw new InvalidAssetNameError('UNIQUE tag must be uppercase', name);
134
+ }
135
+
136
+ if (!ASSET_NAME_RULES.UNIQUE.pattern.test(tag)) {
137
+ throw new InvalidAssetNameError(
138
+ 'UNIQUE tag can only contain A-Z, 0-9, underscore, and period',
139
+ name
140
+ );
141
+ }
142
+
143
+ return true;
144
+ }
145
+
146
+ /**
147
+ * Validate QUALIFIER asset name
148
+ * Format: #NAME or #ROOT/SUB
149
+ */
150
+ static validateQualifier(name) {
151
+ if (!name || typeof name !== 'string') {
152
+ throw new InvalidAssetNameError('QUALIFIER asset name must be a non-empty string', name);
153
+ }
154
+
155
+ if (!name.startsWith(ASSET_NAME_RULES.QUALIFIER.prefix)) {
156
+ throw new InvalidAssetNameError(
157
+ `QUALIFIER asset must start with ${ASSET_NAME_RULES.QUALIFIER.prefix}`,
158
+ name
159
+ );
160
+ }
161
+
162
+ const withoutPrefix = name.substring(1);
163
+
164
+ if (withoutPrefix.includes(ASSET_NAME_RULES.QUALIFIER.separator)) {
165
+ // Sub-qualifier: #ROOT/SUB
166
+ const parts = withoutPrefix.split(ASSET_NAME_RULES.QUALIFIER.separator);
167
+ if (parts.length !== 2) {
168
+ throw new InvalidAssetNameError(
169
+ 'SUB_QUALIFIER must be in #ROOT/SUB format',
170
+ name
171
+ );
172
+ }
173
+
174
+ // Validate each part as a qualifier name (without the #)
175
+ parts.forEach(part => {
176
+ if (part.length < ASSET_NAME_RULES.QUALIFIER.minLength || part.length > ASSET_NAME_RULES.QUALIFIER.maxLength) {
177
+ throw new InvalidAssetNameError(
178
+ `QUALIFIER name must be ${ASSET_NAME_RULES.QUALIFIER.minLength}-${ASSET_NAME_RULES.QUALIFIER.maxLength} characters`,
179
+ name
180
+ );
181
+ }
182
+
183
+ if (part !== part.toUpperCase()) {
184
+ throw new InvalidAssetNameError('QUALIFIER name must be uppercase', name);
185
+ }
186
+
187
+ if (!ASSET_NAME_RULES.QUALIFIER.pattern.test(part)) {
188
+ throw new InvalidAssetNameError(
189
+ 'QUALIFIER name can only contain A-Z, 0-9, and underscore',
190
+ name
191
+ );
192
+ }
193
+ });
194
+ } else {
195
+ // Root qualifier: #NAME
196
+ if (withoutPrefix.length < ASSET_NAME_RULES.QUALIFIER.minLength ||
197
+ withoutPrefix.length > ASSET_NAME_RULES.QUALIFIER.maxLength) {
198
+ throw new InvalidAssetNameError(
199
+ `QUALIFIER name must be ${ASSET_NAME_RULES.QUALIFIER.minLength}-${ASSET_NAME_RULES.QUALIFIER.maxLength} characters`,
200
+ name
201
+ );
202
+ }
203
+
204
+ if (withoutPrefix !== withoutPrefix.toUpperCase()) {
205
+ throw new InvalidAssetNameError('QUALIFIER name must be uppercase', name);
206
+ }
207
+
208
+ if (!ASSET_NAME_RULES.QUALIFIER.pattern.test(withoutPrefix)) {
209
+ throw new InvalidAssetNameError(
210
+ 'QUALIFIER name can only contain A-Z, 0-9, and underscore',
211
+ name
212
+ );
213
+ }
214
+ }
215
+
216
+ return true;
217
+ }
218
+
219
+ /**
220
+ * Validate RESTRICTED asset name
221
+ * Format: $NAME
222
+ */
223
+ static validateRestricted(name) {
224
+ if (!name || typeof name !== 'string') {
225
+ throw new InvalidAssetNameError('RESTRICTED asset name must be a non-empty string', name);
226
+ }
227
+
228
+ if (!name.startsWith(ASSET_NAME_RULES.RESTRICTED.prefix)) {
229
+ throw new InvalidAssetNameError(
230
+ `RESTRICTED asset must start with ${ASSET_NAME_RULES.RESTRICTED.prefix}`,
231
+ name
232
+ );
233
+ }
234
+
235
+ const withoutPrefix = name.substring(1);
236
+
237
+ // Validate the part after $ as a ROOT asset name
238
+ this.validateRoot(withoutPrefix);
239
+
240
+ return true;
241
+ }
242
+
243
+ /**
244
+ * Validate owner token name
245
+ * Format: ASSETNAME!
246
+ */
247
+ static validateOwnerToken(name) {
248
+ if (!name || typeof name !== 'string') {
249
+ throw new InvalidAssetNameError('Owner token name must be a non-empty string', name);
250
+ }
251
+
252
+ if (!name.endsWith('!')) {
253
+ throw new InvalidAssetNameError('Owner token must end with !', name);
254
+ }
255
+
256
+ const assetName = name.substring(0, name.length - 1);
257
+
258
+ // Validate the asset name part (could be ROOT or RESTRICTED)
259
+ if (assetName.startsWith('$')) {
260
+ this.validateRestricted(assetName);
261
+ } else {
262
+ this.validateRoot(assetName);
263
+ }
264
+
265
+ return true;
266
+ }
267
+
268
+ /**
269
+ * Auto-detect asset type and validate
270
+ * @param {string} name - Asset name
271
+ * @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'OWNER')
272
+ */
273
+ static validateAndDetectType(name) {
274
+ if (name.endsWith('!')) {
275
+ this.validateOwnerToken(name);
276
+ return 'OWNER';
277
+ } else if (name.startsWith('#')) {
278
+ this.validateQualifier(name);
279
+ return name.includes('/') ? 'SUB_QUALIFIER' : 'QUALIFIER';
280
+ } else if (name.startsWith('$')) {
281
+ this.validateRestricted(name);
282
+ return 'RESTRICTED';
283
+ } else if (name.includes('#')) {
284
+ this.validateUnique(name);
285
+ return 'UNIQUE';
286
+ } else if (name.includes('/')) {
287
+ this.validateSub(name);
288
+ return 'SUB';
289
+ } else {
290
+ this.validateRoot(name);
291
+ return 'ROOT';
292
+ }
293
+ }
294
+ }
295
+
296
+ module.exports = AssetNameValidator;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Validators Module
3
+ * Exports all validator classes
4
+ */
5
+
6
+ const AssetNameValidator = require('./assetNameValidator');
7
+ const AmountValidator = require('./amountValidator');
8
+ const VerifierValidator = require('./verifierValidator');
9
+ const IpfsValidator = require('./ipfsValidator');
10
+
11
+ module.exports = {
12
+ AssetNameValidator,
13
+ AmountValidator,
14
+ VerifierValidator,
15
+ IpfsValidator
16
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * IPFS Hash Validator
3
+ * Validates IPFS CID formats
4
+ */
5
+
6
+ const { InvalidIPFSHashError } = require('../errors');
7
+
8
+ class IpfsValidator {
9
+ /**
10
+ * Validate IPFS hash format
11
+ * Accepts:
12
+ * - CIDv0: Qm... (46 characters, base58)
13
+ * - CIDv1: bafy... or bafk... (various lengths, base32)
14
+ * - Neurai TXID: 64 hex characters (for on-chain metadata)
15
+ *
16
+ * @param {string} hash - IPFS hash or TXID
17
+ * @returns {boolean} True if valid
18
+ */
19
+ static validate(hash) {
20
+ if (!hash || typeof hash !== 'string') {
21
+ throw new InvalidIPFSHashError('IPFS hash must be a non-empty string', hash);
22
+ }
23
+
24
+ const trimmed = hash.trim();
25
+
26
+ // Check maximum length (Neurai allows up to 40 bytes in protocol)
27
+ if (trimmed.length > 80) {
28
+ throw new InvalidIPFSHashError('IPFS hash too long (max 80 characters)', hash);
29
+ }
30
+
31
+ // Check if it's a valid format
32
+ const isCIDv0 = this.isCIDv0(trimmed);
33
+ const isCIDv1 = this.isCIDv1(trimmed);
34
+ const isTXID = this.isTXID(trimmed);
35
+
36
+ if (!isCIDv0 && !isCIDv1 && !isTXID) {
37
+ throw new InvalidIPFSHashError(
38
+ 'Invalid IPFS hash format. Must be CIDv0 (Qm...), CIDv1 (bafy...), or TXID (64 hex chars)',
39
+ hash
40
+ );
41
+ }
42
+
43
+ return true;
44
+ }
45
+
46
+ /**
47
+ * Check if hash is CIDv0 format
48
+ * @param {string} hash - Hash to check
49
+ * @returns {boolean} True if CIDv0
50
+ */
51
+ static isCIDv0(hash) {
52
+ // CIDv0: Starts with "Qm", 46 characters, base58
53
+ if (!hash.startsWith('Qm')) return false;
54
+ if (hash.length !== 46) return false;
55
+
56
+ // Base58 characters: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
57
+ const base58Pattern = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/;
58
+ return base58Pattern.test(hash);
59
+ }
60
+
61
+ /**
62
+ * Check if hash is CIDv1 format
63
+ * @param {string} hash - Hash to check
64
+ * @returns {boolean} True if CIDv1
65
+ */
66
+ static isCIDv1(hash) {
67
+ // CIDv1: Starts with "bafy" or "bafk" (base32), various lengths
68
+ if (!hash.startsWith('bafy') && !hash.startsWith('bafk')) return false;
69
+
70
+ // Base32 characters: a-z, 2-7
71
+ const base32Pattern = /^[a-z2-7]+$/;
72
+ return base32Pattern.test(hash);
73
+ }
74
+
75
+ /**
76
+ * Check if hash is a transaction ID
77
+ * @param {string} hash - Hash to check
78
+ * @returns {boolean} True if TXID
79
+ */
80
+ static isTXID(hash) {
81
+ // TXID: 64 hexadecimal characters
82
+ if (hash.length !== 64) return false;
83
+
84
+ const hexPattern = /^[0-9a-fA-F]+$/;
85
+ return hexPattern.test(hash);
86
+ }
87
+
88
+ /**
89
+ * Get IPFS hash type
90
+ * @param {string} hash - IPFS hash
91
+ * @returns {string} Type ('CIDv0', 'CIDv1', 'TXID', or 'UNKNOWN')
92
+ */
93
+ static getHashType(hash) {
94
+ if (this.isCIDv0(hash)) return 'CIDv0';
95
+ if (this.isCIDv1(hash)) return 'CIDv1';
96
+ if (this.isTXID(hash)) return 'TXID';
97
+ return 'UNKNOWN';
98
+ }
99
+ }
100
+
101
+ module.exports = IpfsValidator;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Verifier String Validator
3
+ * Validates verifier strings for restricted assets
4
+ */
5
+
6
+ const { InvalidVerifierStringError } = require('../errors');
7
+ const AssetNameValidator = require('./assetNameValidator');
8
+
9
+ class VerifierValidator {
10
+ /**
11
+ * Validate verifier string syntax
12
+ * Verifier syntax: #TAG, !#TAG, &, |, (, )
13
+ * Examples:
14
+ * "#KYC"
15
+ * "#KYC & #ACCREDITED"
16
+ * "#KYC | #INSTITUTION"
17
+ * "(#KYC & #ACCREDITED) | #INSTITUTION"
18
+ * "!#BANNED"
19
+ *
20
+ * @param {string} verifierString - Verifier string to validate
21
+ * @returns {boolean} True if valid
22
+ */
23
+ static validate(verifierString) {
24
+ if (!verifierString || typeof verifierString !== 'string') {
25
+ throw new InvalidVerifierStringError(
26
+ 'Verifier string must be a non-empty string',
27
+ verifierString
28
+ );
29
+ }
30
+
31
+ // Trim whitespace
32
+ const trimmed = verifierString.trim();
33
+ if (trimmed.length === 0) {
34
+ throw new InvalidVerifierStringError(
35
+ 'Verifier string cannot be empty',
36
+ verifierString
37
+ );
38
+ }
39
+
40
+ // Check for valid characters only
41
+ // Valid: #, A-Z, 0-9, _, &, |, !, (, ), space, /
42
+ const validPattern = /^[#A-Z0-9_&|()\s!/]+$/;
43
+ if (!validPattern.test(trimmed)) {
44
+ throw new InvalidVerifierStringError(
45
+ 'Verifier contains invalid characters. Valid: #, A-Z, 0-9, _, &, |, !, (, ), space, /',
46
+ verifierString
47
+ );
48
+ }
49
+
50
+ // Extract all qualifiers (tokens starting with # or !#)
51
+ const qualifierMatches = trimmed.match(/!?#[A-Z0-9_/]+/g) || [];
52
+
53
+ if (qualifierMatches.length === 0) {
54
+ throw new InvalidVerifierStringError(
55
+ 'Verifier must contain at least one qualifier (#TAG)',
56
+ verifierString
57
+ );
58
+ }
59
+
60
+ // Validate each qualifier
61
+ for (const match of qualifierMatches) {
62
+ const qualifier = match.startsWith('!') ? match.substring(1) : match;
63
+
64
+ try {
65
+ AssetNameValidator.validateQualifier(qualifier);
66
+ } catch (e) {
67
+ throw new InvalidVerifierStringError(
68
+ `Invalid qualifier in verifier: ${qualifier} - ${e.message}`,
69
+ verifierString
70
+ );
71
+ }
72
+ }
73
+
74
+ // Check balanced parentheses
75
+ let depth = 0;
76
+ for (const char of trimmed) {
77
+ if (char === '(') depth++;
78
+ if (char === ')') depth--;
79
+ if (depth < 0) {
80
+ throw new InvalidVerifierStringError(
81
+ 'Unbalanced parentheses in verifier string',
82
+ verifierString
83
+ );
84
+ }
85
+ }
86
+ if (depth !== 0) {
87
+ throw new InvalidVerifierStringError(
88
+ 'Unbalanced parentheses in verifier string',
89
+ verifierString
90
+ );
91
+ }
92
+
93
+ // Check for valid operators placement
94
+ // & and | must be between qualifiers, not at start/end
95
+ const operatorPattern = /(&|\|)/g;
96
+ const operators = trimmed.match(operatorPattern);
97
+ if (operators) {
98
+ // Check operators are not at start or end
99
+ if (trimmed.trim().match(/^(&|\|)/) || trimmed.trim().match(/(&|\|)$/)) {
100
+ throw new InvalidVerifierStringError(
101
+ 'Operators & or | cannot be at start or end of verifier',
102
+ verifierString
103
+ );
104
+ }
105
+
106
+ // Check no consecutive operators
107
+ if (trimmed.match(/(&|\|)\s*(&|\|)/)) {
108
+ throw new InvalidVerifierStringError(
109
+ 'Consecutive operators are not allowed',
110
+ verifierString
111
+ );
112
+ }
113
+ }
114
+
115
+ return true;
116
+ }
117
+
118
+ /**
119
+ * Extract all qualifiers from verifier string
120
+ * @param {string} verifierString - Verifier string
121
+ * @returns {string[]} Array of qualifier names (including #)
122
+ */
123
+ static extractQualifiers(verifierString) {
124
+ this.validate(verifierString);
125
+
126
+ const qualifierMatches = verifierString.match(/!?#[A-Z0-9_/]+/g) || [];
127
+
128
+ // Remove ! prefix and deduplicate
129
+ const qualifiers = [...new Set(qualifierMatches.map(q => q.replace('!', '')))];
130
+
131
+ return qualifiers;
132
+ }
133
+
134
+ /**
135
+ * Check if verifier string uses a specific qualifier
136
+ * @param {string} verifierString - Verifier string
137
+ * @param {string} qualifierName - Qualifier to check (with #)
138
+ * @returns {boolean} True if qualifier is used
139
+ */
140
+ static usesQualifier(verifierString, qualifierName) {
141
+ const qualifiers = this.extractQualifiers(verifierString);
142
+ return qualifiers.includes(qualifierName);
143
+ }
144
+ }
145
+
146
+ module.exports = VerifierValidator;