UNPKG

@silvana-one/nft

Version:
581 lines 22.7 kB
import { Field, Poseidon, Struct, IndexedMerkleMap, PublicKey, UInt64, } from "o1js"; import { fieldFromString } from "../interfaces/index.js"; import { Text } from "./text.js"; import { MinaAddress } from "./address.js"; import { MetadataTree } from "./tree.js"; export { Metadata, MetadataMap, MetadataFieldTypeValues, MetadataValue, ColorPlugin, }; /** * The height of the metadata Merkle tree. */ const METADATA_HEIGHT = 20; /** * A specialized IndexedMerkleMap for storing metadata. */ class MetadataMap extends IndexedMerkleMap(METADATA_HEIGHT) { } /** * Represents a metadata value with its type and associated data. */ class MetadataValue extends Struct({ value: Field, type: Field, length: Field, height: Field, }) { /** * Creates a new MetadataValue instance. * @param params - The parameters including value and type. * @returns A new MetadataValue. */ static new(params) { const { value, type } = params; let valueField; let length = Field(0); let height = Field(0); switch (type) { case "string": if (!(value instanceof Field)) throw new Error(`Invalid value type`); valueField = value; break; case "text": case "image": case "url": if (!(value instanceof Text)) throw new Error(`Invalid value type`); valueField = value.root; length = Field(value.size); height = Field(value.height); break; case "field": if (!(value instanceof Field)) throw new Error(`Invalid value type`); valueField = value; break; case "number": if (!(value instanceof UInt64)) throw new Error(`Invalid value type`); valueField = value.value; break; case "address": if (!(value instanceof PublicKey)) throw new Error(`Invalid value type`); const address = new MinaAddress(value); valueField = address.hash; length = Field(2); height = Field(0); break; case "map": if (!(value instanceof Metadata)) throw new Error(`Invalid value type`); valueField = value.map.root; length = Field(value.map.length); height = Field(value.map.height); break; case "tree": if (!(value instanceof MetadataTree)) throw new Error(`Invalid value type`); valueField = value.root; length = Field(value.values.length); height = Field(value.height); break; default: throw new Error(`Unknown value type`); } return new MetadataValue({ value: valueField, type: Field(MetadataFieldTypeValues[type].code), length, height, }); } /** * Computes the Poseidon hash of the metadata value. * @returns The hash as a Field. */ hash() { return Poseidon.hash(MetadataValue.toFields(this)); } } /** * Abstract class for creating custom metadata plugins. */ class MetadataPlugin { } /** * A plugin for handling color metadata. */ class ColorPlugin extends MetadataPlugin { constructor() { super(...arguments); /** * The name of the plugin. */ this.name = "color"; } /** * Converts a color name or value into its numeric representation. * @param value - The color value (name, string, or number). * @returns The numeric representation of the color. */ getColor(value) { if (typeof value === "string" && ["blue", "red", "green", "yellow", "purple", "orange", "pink"].includes(value)) { const colors = { blue: 0x0000ff, red: 0xff0000, green: 0x00ff00, yellow: 0xffff00, purple: 0x800080, orange: 0xffa500, pink: 0xffc0cb, }; return colors[value]; } else if (typeof value === "number") { return value; } else if (typeof value === "string") { try { // parse hex color like #0000ff return parseInt(value.slice(1), 16); } catch (e) { throw new Error("Invalid color value"); } } throw new Error("Invalid color value"); } /** * Retrieves the trait representation of the color value. * @param params - The parameters including key, type, and value. * @returns An object containing the key, value, and canonical representation. */ getTrait(params) { const { key, value } = params; const color = this.getColor(value); return { key: fieldFromString(key), value: new MetadataValue({ value: Field(color), type: Field(10), length: Field(0), height: Field(0), }), canonicalRepresentation: color, }; } /** * Converts the color value to a JSON string. * @param value - The color value. * @returns The JSON string representation. */ toJSON(value) { return this.getColor(value).toString(16); } /** * Parses the color value from a JSON string or object. * @param value - The JSON value. * @returns The numeric representation of the color. */ fromJSON(value) { if (typeof value !== "string") throw new Error("Invalid color value"); return this.getColor(parseInt(value, 16)); } } /** * Represents the metadata for an NFT, including traits and associated data. */ class Metadata { /** * Creates a new Metadata instance. * @param params - The parameters for the metadata, including name, image, description, banner, and plugins. */ constructor(params) { /** * Object containing the traits of the NFT. */ this.traits = {}; const { name, description, image, banner, plugins } = params; this.plugins = plugins ?? []; this.map = new MetadataMap(); this.addTrait({ key: "name", type: "string", value: name, }); this.addTrait({ key: "image", type: "image", value: image, }); if (description) { this.addTrait({ key: "description", type: "text", value: description, }); } if (banner) { this.addTrait({ key: "banner", type: "image", value: banner, }); } this.name = name; this.image = image; this.banner = banner; this.description = description; } /** * Adds a trait to the metadata. * @param params - The parameters including key, type, value, and isPrivate. * @returns An object containing the key and the metadata value. */ addTrait(params) { const { key, type, value, isPrivate = false } = params; let keyField = fieldFromString(key); let metadataValue; let canonicalRepresentation = value; if (type in MetadataFieldTypeValues) { let valueObject; switch (type) { case "string": if (typeof value !== "string") throw new Error(`Invalid trait value type`); valueObject = fieldFromString(value); break; case "text": case "image": case "url": if (typeof value !== "string") throw new Error(`Invalid trait value type`); valueObject = new Text(value); break; case "field": if (!(value instanceof Field || typeof value === "bigint" || typeof value === "number" || typeof value === "string")) throw new Error(`Invalid trait value type`); valueObject = Field(value); break; case "number": if (!(value instanceof UInt64 || typeof value === "bigint" || typeof value === "number" || typeof value === "string")) throw new Error(`Invalid trait value type`); valueObject = UInt64.from(value); break; case "address": if (!(value instanceof PublicKey || typeof value === "string")) throw new Error(`Invalid trait value type`); valueObject = typeof value === "string" ? PublicKey.fromBase58(value) : value; break; case "map": if (!(value instanceof Metadata)) throw new Error(`Invalid trait value type`); valueObject = value; break; case "tree": if (!(value instanceof MetadataTree)) throw new Error(`Invalid trait value type`); valueObject = value; break; default: throw new Error(`Unknown trait value type - internal error`); } metadataValue = MetadataValue.new({ value: valueObject, type }); } else { const index = this.plugins.findIndex((plugin) => plugin.name === type); if (index !== -1) { const plugin = this.plugins[index]; const pluginTrait = plugin.getTrait({ key, type, value, isPrivate }); metadataValue = pluginTrait.value; keyField = pluginTrait.key; canonicalRepresentation = pluginTrait.canonicalRepresentation; } else throw new Error(`Unknown trait type`); } this.map.set(keyField, metadataValue.hash()); this.traits[key] = { type, value: canonicalRepresentation, isPrivate }; return { key: keyField, value: metadataValue }; } /** * Converts the metadata to a JSON representation. * @param includePrivateTraits - Whether to include private traits. * @returns The JSON representation of the metadata. */ toJSON(includePrivateTraits = false) { return { name: this.name, description: this.description, image: this.image, banner: this.banner, metadataRoot: this.map.root.toJSON(), traits: Object.entries(this.traits) .filter(([_, { isPrivate }]) => includePrivateTraits || !isPrivate) .map(([key, { type, value, isPrivate }]) => { let jsonValue; switch (type) { case "string": case "text": case "image": case "url": if (typeof value !== "string") throw new Error(`Invalid trait value type`); jsonValue = value; break; case "field": if (!(value instanceof Field)) throw new Error(`Invalid trait value type`); jsonValue = value.toJSON(); break; case "number": if (!(value instanceof UInt64)) throw new Error(`Invalid trait value type`); jsonValue = value.toJSON(); break; case "address": if (!(value instanceof PublicKey)) throw new Error(`Invalid trait value type`); jsonValue = value.toBase58(); break; case "map": if (!(value instanceof Metadata)) throw new Error(`Invalid trait value type`); jsonValue = value.toJSON(includePrivateTraits); break; case "tree": if (!(value instanceof MetadataTree)) throw new Error(`Invalid trait value type`); jsonValue = value.toJSON(); break; default: const plugin = this.plugins.find((plugin) => plugin.name === type); if (!plugin) throw new Error(`Unknown trait type`); jsonValue = plugin.toJSON(value); } return { key, type, ...(isPrivate ? { isPrivate } : {}), value: jsonValue, }; }), }; } /** * Constructs a Metadata instance from JSON data. * @param params - The parameters including json data, checkRoot flag, and plugins. * @returns A new Metadata instance. */ static fromJSON(params) { const { json, checkRoot = false, plugins } = params; const { name, description, image, banner, metadataRoot, traits = [], } = json; if (!name) throw new Error(`Metadata name is required`); if (typeof name !== "string") throw new Error(`Invalid metadata name`); if (!image || typeof image !== "string") throw new Error(`Invalid metadata image`); if (description && typeof description !== "string") throw new Error(`Invalid metadata description`); if (banner && typeof banner !== "string") throw new Error(`Invalid metadata banner`); if (!metadataRoot || typeof metadataRoot !== "string") throw new Error(`Invalid metadata root`); if (!traits || !Array.isArray(traits)) throw new Error(`Metadata traits are required`); for (const { key, type, value, isPrivate } of traits) { if (!key || typeof key !== "string") throw new Error(`Invalid trait key`); if (!type || typeof type !== "string") throw new Error(`Invalid trait type`); if (!value || (typeof value !== "string" && typeof value !== "object")) throw new Error(`Invalid trait value`); if (isPrivate && typeof isPrivate !== "boolean") throw new Error(`Invalid trait isPrivate`); } const metadata = new Metadata({ name, description, image, banner, plugins, }); for (const { key, type, value, isPrivate } of traits) { let valueField; switch (type) { case "string": case "text": case "image": case "url": if (typeof value !== "string") throw new Error(`Invalid trait value type`); valueField = value; break; case "field": if (typeof value !== "string") throw new Error(`Invalid trait value type`); valueField = Field.fromJSON(value); break; case "number": if (typeof value !== "string") throw new Error(`Invalid trait value type`); valueField = UInt64.fromJSON(value); break; case "address": if (typeof value !== "string") throw new Error(`Invalid trait value type`); valueField = PublicKey.fromBase58(value); break; case "map": if (typeof value !== "object") throw new Error(`Invalid trait value type`); valueField = Metadata.fromJSON({ json: value, checkRoot, }); break; case "tree": if (typeof value !== "object") throw new Error(`Invalid trait value type`); valueField = MetadataTree.fromJSON(value); break; default: const plugin = metadata.plugins.find((plugin) => plugin.name === type); if (!plugin) throw new Error(`Unknown trait type`); valueField = plugin.fromJSON(value); } metadata.addTrait({ key, type, value: valueField, isPrivate: isPrivate ?? false, }); } if (checkRoot === true && metadata.map.root.toJSON() !== metadataRoot) { throw new Error(`Invalid metadata root:${JSON.stringify({ params, root: metadata.map.root.toJSON(), checkRoot, metadata: metadata.toJSON(true), }, null, 2)}`); } return metadata; } /** * Constructs a Metadata instance from OpenAPI JSON data (without calculated root). * @param params - The parameters including json data, checkRoot flag, and plugins. * @returns A new Metadata instance. */ static fromOpenApiJSON(params) { const { json, plugins } = params; const { name, description, image, banner, traits = [] } = json; if (!name) throw new Error(`Metadata name is required`); if (typeof name !== "string") throw new Error(`Invalid metadata name`); if (!image || typeof image !== "string") throw new Error(`Invalid metadata image`); if (description && typeof description !== "string") throw new Error(`Invalid metadata description`); if (banner && typeof banner !== "string") throw new Error(`Invalid metadata banner`); if (!traits || !Array.isArray(traits)) throw new Error(`Metadata traits are required`); for (const { key, type, value, isPrivate } of traits) { if (!key || typeof key !== "string") throw new Error(`Invalid trait key`); if (!type || typeof type !== "string") throw new Error(`Invalid trait type`); if (!value || (typeof value !== "string" && typeof value !== "object")) throw new Error(`Invalid trait value`); if (isPrivate && typeof isPrivate !== "boolean") throw new Error(`Invalid trait isPrivate`); } const metadata = new Metadata({ name, description, image, banner, plugins, }); for (const { key, type, value, isPrivate } of traits) { let valueField; switch (type) { case "string": case "text": case "image": case "url": if (typeof value !== "string") throw new Error(`Invalid trait value type`); valueField = value; break; case "field": if (typeof value !== "string") throw new Error(`Invalid trait value type`); valueField = Field.fromJSON(value); break; case "number": if (typeof value !== "string") throw new Error(`Invalid trait value type`); valueField = UInt64.fromJSON(value); break; case "address": if (typeof value !== "string") throw new Error(`Invalid trait value type`); valueField = PublicKey.fromBase58(value); break; case "map": if (typeof value !== "object") throw new Error(`Invalid trait value type`); valueField = Metadata.fromOpenApiJSON({ json: value, }); break; case "tree": if (typeof value !== "object") throw new Error(`Invalid trait value type`); valueField = MetadataTree.fromJSON(value); break; default: const plugin = metadata.plugins.find((plugin) => plugin.name === type); if (!plugin) throw new Error(`Unknown trait type`); valueField = plugin.fromJSON(value); } metadata.addTrait({ key, type, value: valueField, isPrivate: isPrivate ?? false, }); } return metadata; } } /** * Mapping of metadata field types to their code values and associated types. */ const MetadataFieldTypeValues = { string: { code: 1n, inputType: "string", storedType: Field }, // Field text: { code: 2n, inputType: "string", storedType: Text }, // Text image: { code: 3n, inputType: "string", storedType: Text }, // Text url: { code: 4n, inputType: "string", storedType: Text }, // Text field: { code: 5n, inputType: Field, storedType: Field }, // Field map: { code: 6n, inputType: Metadata, storedType: Metadata }, // Metadata tree: { code: 7n, inputType: MetadataTree, storedType: MetadataTree }, // MetadataTree number: { code: 8n, inputType: UInt64, storedType: UInt64 }, // UInt64 address: { code: 9n, inputType: PublicKey, storedType: MinaAddress }, // MinaAddress }; //# sourceMappingURL=metadata.js.map