diff --git a/README.md b/README.md index d49a19e..86d3438 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This package includes all sorts of tooling for the Hedera NFT ecosystem, including: -1. **Token metadata validation:** Verify your metadata against the [token metadata JSON schema V2](https://github.com/hashgraph/hedera-improvement-proposal/blob/main/HIP/hip-412.md) for NFTs, which returns errors and warnings against the standard. +1. **Token metadata validation:** Verify your metadata against the [token metadata JSON schema V2](https://github.com/hashgraph/hedera-improvement-proposal/blob/main/HIP/hip-412.md) for NFTs, which returns errors and warnings against the standard. You can also define your own token metadata standard and add it to the package to use this schema for validation. 2. **Local metadata validator:** Verify a local folder containing multiple JSON metadata files against the standard before publishing the NFT collection on the Hedera network. 3. **Risk score calculation:** Calculate a risk score for an NFT collection from the token information or by passing a token ID of an NFT on the Hedera testnet or mainnet. 4. **Rarity score calculation:** Calculate the rarity scores for a local folder containing multiple JSON metadata files for an NFT collection. @@ -39,13 +39,13 @@ Install the package: npm i -s @hashgraph/nft-utilities ``` -Import the package into your project. You can import the `validator` function and the default schema version for token metadata with `defaultVersion`. +Import the package into your project. You can import the `Validator` class and the default schema version for token metadata with `defaultVersion`. ```js -const { validator, defaultVersion } = require('@hashgraph/nft-utilities'); +const { Validator, defaultVersion } = require('@hashgraph/nft-utilities'); ``` -You can use the `validator` like below. +You can use the `Validator` like below. 1. The first parameter is the JSON object you want to verify against a JSON schema 2. The second parameter is the version of the token metadata JSON schema against which you want to validate your metadata instance. The default value is `2.0.0` (V2). In the future, new functionality might be added, releasing new version numbers. @@ -58,7 +58,8 @@ const metadata = { }; const version = '2.0.0'; -const issues = validator(metadata, version); +const validator = new Validator(); +const issues = validator.validate(metadata, version); ``` ### Interface @@ -111,6 +112,49 @@ See: **[/examples/token-metadata-validator](https://github.com/hashgraph/hedera- ### Add custom schema versions +#### Method 1: Use Validator constructor to pass custom schemas + +The easiest approach to adding new schemas is using the constructor of the `Validator` class. It accepts an array of JSON objects, each containing a JSON schema and tag for the schema. The tag is used to refer to the schema when validating metadata instances. + +Therefore, each tag needs to be unqiue. The following tags can't be used as they are already occupied: + +- `1.0.0` -> Refers to token metadata JSON schema V1 (HIP10) +- `2.0.0` -> Refers to token metadata JSON schema V2 (HIP412) + +You can add your custom schema like this: + +```js +const { Validator } = require('@hashgraph/nft-utilities'); + +// Define your schema +const customSchema = { + "title": "Token Metadata", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Identifies the asset to which this token represents." + } + }, + "required": ["name"] +} + +// Create Validator instance with custom schema labeled "custom-v1" +const validator = new Validator([{ schemaObject: customSchema, tag: "custom-v1" }]); + +// Verify metadata against custom schema +const results = validator.validate(metadataInstance, "custom-v1"); +console.log(results); +``` + +**Examples:** See: [/examples/token-metadata-calculation](https://github.com/hashgraph/hedera-nft-utilities/tree/main/examples/token-metadata-calculation/custom-schema-valid-metadata.js) + + +#### Method 2: Rebuilding package + +> ⚠️ Warning: **This approach requires you to rebuild the package.** + You can add custom JSON schemas to the `/schemas` folder. You can then add the version to the `schemaMap` in `/schema/index.js` using the following code: @@ -128,16 +172,16 @@ When you've added your schema to the map, you can validate against your schema v ### Add custom validation rules -Set custom validation rules by importing new validators from the `/validators` folder into the `index.js` file. You can then add them to the `validator()` function. Stick to the `issues` format of errors and warnings (see section "Issues format" for the detailed description). +Set custom validation rules by importing new validators from the `/validators` folder into the `index.js` file. You can then add them to the `validate()` function. Stick to the `issues` format of errors and warnings (see section "Issues format" for the detailed description). ```js const { myCustomValidator, schemaValidator } = require("./validators"); -const validator = (instance, schemaVersion = defaultVersion) => { +const validate = (instance, schemaVersion = defaultSchemaVersion) => { let errors = []; let warnings = []; - const schema = getSchema(schemaVersion) + const schema = this.getSchema(schemaVersion) // When errors against the schema are found, you don't want to continue verifying the NFT // Warnings don't matter because they only contain "additional property" warnings that don't break the other validators @@ -186,7 +230,7 @@ The `localValidation` expects an absolute path to your metadata files to verify localValidation("/Users/projects/nft/files"); ``` -This package uses the `validator` function explained in the [previous section](#token-metadata-validator). +This package uses the `Validator` class explained in the [previous section](#token-metadata-validator). ### Interface diff --git a/examples/token-metadata-validator/custom-schema-valid-metadata.js b/examples/token-metadata-validator/custom-schema-valid-metadata.js new file mode 100644 index 0000000..dbdda69 --- /dev/null +++ b/examples/token-metadata-validator/custom-schema-valid-metadata.js @@ -0,0 +1,72 @@ +/*- + * + * Hedera NFT Utilities + * + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +const { Validator, defaultVersion } = require('../..'); + +function main() { + // Define your JSON schema + const customSchema = { + "title": "Token Metadata", + "type": "object", + "additionalProperties": false, + "properties": { + "version": { + "type": "string", + "description": "Semantic version for the metadata JSON format." + }, + "name": { + "type": "string", + "description": "Identifies the asset to which this token represents." + } + }, + "required": [ + "version", + "name" + ] + } + + // Create Validator instance with custom schema + const validator = new Validator([{ schemaObject: customSchema, tag: "custom-v1" }]); + + // Define metadata + const metadataInstance = { + "version": "v3.0.0", + "name": "HANGRY BARBOON #2343", + "image": "ipfs://QmaHVnnp7qAmGADa3tQfWVNxxZDRmTL5r6jKrAo16mSd5y/2343.png" + } + + // Verify metadata against custom schema + const results = validator.validate(metadataInstance, "custom-v1"); + console.log(results); + + /* Output: + { + errors: [], + warnings: [ + { + type: 'schema', + msg: "is not allowed to have the additional property 'image'", + path: 'instance' + } + ] + } + */ +} + +main(); \ No newline at end of file diff --git a/examples/token-metadata-validator/invalid-errors-metadata.js b/examples/token-metadata-validator/invalid-errors-metadata.js index 717a575..c9a9209 100644 --- a/examples/token-metadata-validator/invalid-errors-metadata.js +++ b/examples/token-metadata-validator/invalid-errors-metadata.js @@ -17,7 +17,7 @@ * limitations under the License. * */ -const { validator, defaultVersion } = require('../..'); +const { Validator, defaultVersion } = require('../..'); function main() { const metadataInstance = { @@ -37,7 +37,8 @@ function main() { ] } - const results = validator(metadataInstance, defaultVersion); + const validator = new Validator(); + const results = validator.validate(metadataInstance, defaultVersion); console.log(results); /* Output: diff --git a/examples/token-metadata-validator/invalid-warnings-metadata.js b/examples/token-metadata-validator/invalid-warnings-metadata.js index 5ebe177..8847d64 100644 --- a/examples/token-metadata-validator/invalid-warnings-metadata.js +++ b/examples/token-metadata-validator/invalid-warnings-metadata.js @@ -17,7 +17,7 @@ * limitations under the License. * */ -const { validator, defaultVersion } = require('../..'); +const { Validator, defaultVersion } = require('../..'); function main() { const metadataInstance = { @@ -38,7 +38,8 @@ function main() { "myAdditionalProperty": "Additional properties should be included in properties" } - const results = validator(metadataInstance, defaultVersion); + const validator = new Validator(); + const results = validator.validate(metadataInstance, defaultVersion); console.log(results); /* Output: diff --git a/examples/token-metadata-validator/valid-metadata.js b/examples/token-metadata-validator/valid-metadata.js index ca1195b..137d65b 100644 --- a/examples/token-metadata-validator/valid-metadata.js +++ b/examples/token-metadata-validator/valid-metadata.js @@ -17,7 +17,7 @@ * limitations under the License. * */ -const { validator, defaultVersion } = require('../..'); +const { Validator, defaultVersion } = require('../..'); function main() { const metadataInstance = { @@ -37,7 +37,8 @@ function main() { ] } - const results = validator(metadataInstance); // by default: verifies metadata against HIP412@2.0.0 + const validator = new Validator(); + const results = validator.validate(metadataInstance); // by default: verifies metadata against HIP412@2.0.0 console.log(results); /* Output: diff --git a/index.js b/index.js index 4892ada..70eb4a3 100644 --- a/index.js +++ b/index.js @@ -17,7 +17,7 @@ * limitations under the License. * */ -const { validator, defaultVersion } = require('./validator'); +const { Validator, defaultSchemaVersion } = require('./validator'); const { localValidation } = require('./local-validation'); const { defaultWeights, defaultRiskLevels, @@ -27,8 +27,8 @@ const { calculateRarity } = require('./rarity'); module.exports = { // validation - validator, - defaultVersion, + Validator, + defaultSchemaVersion, // local validation localValidation, diff --git a/local-validation/index.js b/local-validation/index.js index 3c4ef71..90f4216 100644 --- a/local-validation/index.js +++ b/local-validation/index.js @@ -17,14 +17,15 @@ * limitations under the License. * */ -const { validator } = require('../validator/index'); +const { Validator } = require('../validator/index'); const { readFiles, getJSONFilesForDir } = require('../helpers/files'); const validateFiles = (files) => { let validationResults = {}; + const validator = new Validator(); files.forEach(file => { - const result = validator(file.filedata); + const result = validator.validate(file.filedata); validationResults[file.filename] = result; }); diff --git a/package.json b/package.json index 12b6de2..2c3d997 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hashgraph/nft-utilities", - "version": "1.2.1", + "version": "2.0.0", "description": "NFT Utilities for Hedera Hashgraph", "main": "index.js", "scripts": { diff --git a/tests/validator/validator.test.js b/tests/validator/validator.test.js index 28f745b..036b8fe 100644 --- a/tests/validator/validator.test.js +++ b/tests/validator/validator.test.js @@ -17,18 +17,18 @@ * limitations under the License. * */ -const { validator } = require('../../validator/index'); -const { defaultVersion } = require('../../validator/schemas'); +const { Validator, defaultSchemaVersion } = require('../../validator/index'); const validMetadata = require('./data/valid-HIP412.json'); describe("Validator function tests", () => { describe("Schema version tests", () => { test("it should not return errors for a valid metadata JSON using default schema", () => { // Arrange + const validator = new Validator(); let metadata = JSON.parse(JSON.stringify(validMetadata)); // Act - const schemaProblems = validator(metadata, defaultVersion); + const schemaProblems = validator.validate(metadata, defaultSchemaVersion); // Assert expect(Array.isArray(schemaProblems.errors)).toBe(true); @@ -39,10 +39,11 @@ describe("Validator function tests", () => { test("it should not return errors for a valid metadata JSON using schema version v1.0.0", () => { // Arrange + const validator = new Validator(); let metadata = JSON.parse(JSON.stringify(validMetadata)); // Act - const schemaProblems = validator(metadata, "1.0.0"); + const schemaProblems = validator.validate(metadata, "1.0.0"); // Assert expect(schemaProblems.warnings.length).toBe(0); @@ -51,10 +52,11 @@ describe("Validator function tests", () => { test("it should not return errors for a valid metadata JSON not passing a schema version (using default version)", () => { // Arrange + const validator = new Validator(); let metadata = JSON.parse(JSON.stringify(validMetadata)); // Act - const schemaProblems = validator(metadata); + const schemaProblems = validator.validate(metadata); // Assert expect(schemaProblems.warnings.length).toBe(0); @@ -65,6 +67,7 @@ describe("Validator function tests", () => { describe("Validator errors", () => { test("it should only return schema errors when the metadata contains schema errors and also other types of errors like attribute and localization", () => { // Arrange + const validator = new Validator(); let metadataCopy = JSON.parse(JSON.stringify(validMetadata)); let metadata = { // missing name, image, and type for HIP412@1.0.0 @@ -78,7 +81,7 @@ describe("Validator function tests", () => { } // Act - const validationResults = validator(metadata, defaultVersion); + const validationResults = validator.validate(metadata, defaultSchemaVersion); // Assert expect(validationResults.errors.length).toBe(3); @@ -89,6 +92,7 @@ describe("Validator function tests", () => { test("it should return all types of errors when there are no schema errors", () => { // Arrange + const validator = new Validator(); let metadataCopy = JSON.parse(JSON.stringify(validMetadata)); let metadata = { name: "myname", @@ -104,7 +108,7 @@ describe("Validator function tests", () => { } // Act - const validationResults = validator(metadata, defaultVersion); + const validationResults = validator.validate(metadata, defaultSchemaVersion); // Assert expect(validationResults.errors.length).toBe(1); @@ -113,6 +117,7 @@ describe("Validator function tests", () => { test("it should return all types of errors even when there are additional property warnings and no schema errors", () => { // Arrange + const validator = new Validator(); let metadataCopy = JSON.parse(JSON.stringify(validMetadata)); let metadata = { name: "myname", @@ -128,7 +133,7 @@ describe("Validator function tests", () => { } // Act - const validationResults = validator(metadata, defaultVersion); + const validationResults = validator.validate(metadata, defaultSchemaVersion); // Assert expect(validationResults.warnings.length).toBe(1); diff --git a/validator/index.js b/validator/index.js index afb0b8a..ba654c3 100644 --- a/validator/index.js +++ b/validator/index.js @@ -17,37 +17,77 @@ * limitations under the License. * */ -const { schemaValidator, attributesValidator, localizationValidator, SHA256Validator } = require("./validators"); -const { getSchema, defaultVersion } = require("./schemas"); +const { + schemaValidator, + attributesValidator, + localizationValidator, + SHA256Validator, +} = require("./validators"); -/** - * Validate a metadata object against a schema. This function validates the instance respectively against - * the schema validator, attributes validator, localization validator, and SHA256 validator. - * - * If the schema validator (jsonschema) returns errors, it's not possible to run the other validators because properties might be missing. - * The other validators are executed when the schema validator only returns "additional property" erros which don't affect the execution of other validators. - * - * @param {Object} instance - The JSON object to validate against a schema - * @param {string} schemaVersion [schemaVersion = defaultVersion = 2.0.0] - The metadata schema version against which we want to validate our {instance} - * @returns {Array} - Contains no, one, or multiple error objects that describe errors for the validated {instance} - */ -const validator = (instance, schemaVersion = defaultVersion) => { +const token_metadata_1_0_0 = require("./schemas/HIP10@1.0.0.json"); +const token_metadata_2_0_0 = require("./schemas/HIP412@2.0.0.json"); +const defaultSchemaVersion = "2.0.0"; + +class Validator { + /** + * @param {Array} schemas + * @param {string} schemas.tag - The tag of the schema + * @param {Object} schemas.schemaObject - The schema object + */ + constructor(schemas = []) { + this.schemaMap = new Map(); + this.schemaMap.set("1.0.0", token_metadata_1_0_0); + this.schemaMap.set("2.0.0", token_metadata_2_0_0); + + schemas.forEach((schema) => { + this.schemaMap.set(schema.tag, schema.schemaObject); + }); + } + + /** + * Retrieves correct schema for the requested HIP412 metadata schema version. + * If the version doesn't exist, it will return the default schema version "2.0.0". + * + * @param {string} version - The schema version to load. + * @return {Object} - Returns a json schema JSON object. + */ + getSchema(version) { + const validVersion = this.schemaMap.has(version); + if (validVersion) { + return this.schemaMap.get(version); + } + + return this.schemaMap.get(defaultSchemaVersion); + } + + /** + * Validate a metadata object against a schema. This function validates the instance respectively against + * the schema validator, attributes validator, localization validator, and SHA256 validator. + * + * If the schema validator (jsonschema) returns errors, it's not possible to run the other validators because properties might be missing. + * The other validators are executed when the schema validator only returns "additional property" erros which don't affect the execution of other validators. + * + * @param {Object} instance - The JSON object to validate against a schema + * @param {string} schemaVersion [schemaVersion = defaultSchemaVersion = 2.0.0] - The metadata schema version against which we want to validate our {instance} + * @returns {Array} - Contains no, one, or multiple error objects that describe errors for the validated {instance} + */ + validate(instance, schemaVersion = defaultSchemaVersion) { let errors = []; let warnings = []; - const schema = getSchema(schemaVersion) + const schema = this.getSchema(schemaVersion); // When errors against the schema are found, you don't want to continue verifying the NFT // Warnings don't matter because they only contain "additional property" warnings const schemaProblems = schemaValidator(instance, schema); warnings.push(...schemaProblems.warnings); if (schemaProblems.errors.length > 0) { - errors.push(...schemaProblems.errors); + errors.push(...schemaProblems.errors); - return { - errors, - warnings - } + return { + errors, + warnings, + }; } const attributeErrors = attributesValidator(instance); @@ -60,12 +100,13 @@ const validator = (instance, schemaVersion = defaultVersion) => { errors.push(...SHA256Errors); return { - errors, - warnings + errors, + warnings, }; + } } module.exports = { - validator, - defaultVersion + Validator, + defaultSchemaVersion };