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