import { ErrorCode, ErrorUtils } from "js-moi-utils"; import { Schema as PoloSchema } from "js-polo"; import { LogicManifest } from "../types/manifest"; const ARRAY_MATCHER_REGEX = /^\[(\d*)\]/; const primitiveTypes = ["null", "bool", "bytes", "identifier", "string", "u64", "u256", "i64", "i256", "bigint"]; export const isPrimitiveType = (type: string): boolean => { return primitiveTypes.includes(type); }; export const isArray = (type: string): boolean => { return ARRAY_MATCHER_REGEX.test(type); }; export const isMap = (type: string): boolean => { return type.startsWith("map"); }; export const isClass = (type: string, classDefs: Map): boolean => { return classDefs.has(type); }; /** * Schema is a class that provides schema parsing functionality for encoding and * decoding manifest, arguments, logic states and other data based on * a predefined schema. It supports parsing fields and generating a schema for * decoding purposes. * * @class */ export class Schema { private elements: Map; private classDefs: Map; constructor(elements: Map, classDefs: Map) { this.elements = elements; this.classDefs = classDefs; } public static PISA_ENGINE_SCHEMA = { kind: "struct", fields: { kind: { kind: "string", }, flags: { kind: "array", fields: { values: { kind: "string", }, }, }, version: { kind: "string" } }, }; public static PISA_DEPS_SCHEMA = { kind: "array", fields: { values: { kind: "integer", }, }, }; public static PISA_TYPE_FIELD_SCHEMA = { kind: "array", fields: { values: { kind: "struct", fields: { slot: { kind: "integer", }, label: { kind: "string", }, type: { kind: "string", }, }, }, }, }; public static PISA_METHOD_FIELD_SCHEMA = { kind: "array", fields: { values: { kind: "struct", fields: { ptr: { kind: "integer", }, code: { kind: "integer", }, }, }, }, }; public static PISA_INSTRUCTIONS_SCHEMA = { kind: "struct", fields: { bin: { kind: "bytes", }, hex: { kind: "string", }, asm: { kind: "array", fields: { values: { kind: "string", }, }, }, }, }; public static PISA_STATE_SCHEMA = { kind: "struct", fields: { mode: { kind: "string", }, fields: { ...Schema.PISA_TYPE_FIELD_SCHEMA, }, }, }; public static PISA_LITERAL_SCHEMA = { kind: "struct", fields: { type: { kind: "string", }, value: { kind: "string", }, }, }; public static PISA_TYPEDEF_SCHEMA = { kind: "string", fields: {}, }; public static PISA_CLASS_SCHEMA = { kind: "struct", fields: { name: { kind: "string", }, fields: { ...Schema.PISA_TYPE_FIELD_SCHEMA, }, methods: { ...Schema.PISA_METHOD_FIELD_SCHEMA, }, }, }; public static PISA_ROUTINE_SCHEMA = { kind: "struct", fields: { name: { kind: "string", }, mode: { kind: "string", }, kind: { kind: "string", }, accepts: { ...Schema.PISA_TYPE_FIELD_SCHEMA, }, returns: { ...Schema.PISA_TYPE_FIELD_SCHEMA, }, executes: { ...Schema.PISA_INSTRUCTIONS_SCHEMA, }, catches: { kind: "array", fields: { values: { kind: "string", }, }, }, }, }; public static PISA_METHOD_SCHEMA = { kind: "struct", fields: { name: { kind: "string", }, class: { kind: "string", }, mutable: { kind: "bool", }, accepts: { ...Schema.PISA_TYPE_FIELD_SCHEMA, }, returns: { ...Schema.PISA_TYPE_FIELD_SCHEMA, }, executes: { ...Schema.PISA_INSTRUCTIONS_SCHEMA, }, catches: { kind: "array", fields: { values: { kind: "string", }, }, }, }, }; public static PISA_EXTERNAL_ROUTINE_SCHEMA = { kind: "struct", fields: { name: { kind: "string", }, accepts: { ...Schema.PISA_TYPE_FIELD_SCHEMA, }, returns: { ...Schema.PISA_TYPE_FIELD_SCHEMA, }, } } public static PISA_EXTERN_SCHEMA = { kind: "struct", fields: { name: { kind: "string", }, logic: { ...Schema.PISA_STATE_SCHEMA, }, actor: { ...Schema.PISA_STATE_SCHEMA, }, endpoint: { kind: "array", fields: { values: { ...Schema.PISA_EXTERNAL_ROUTINE_SCHEMA } } } } } public static PISA_EVENT_SCHEMA = { kind: "struct", fields: { name: { kind: "string", }, topics: { kind: "integer", }, fields: { ...Schema.PISA_TYPE_FIELD_SCHEMA, } } } public static PISA_EXCEPTION_SCHEMA = { kind: "struct", fields: { class: { kind: "string", }, error: { kind: "string", }, revert: { kind: "bool", }, trace: { kind: "array", fields: { values: { kind: "string", }, }, }, }, }; public static PISA_RESULT_SCHEMA = { kind: "struct", fields: { outputs: { kind: "bytes", }, error: { kind: "bytes", }, }, }; public static PISA_ASSET_SCHEMA = { kind: "struct", fields: { engine: { kind: "string" } } } /** * Extracts the array data type from the provided data type string. * * @param {string} dataType - The array data type, eg:"[]u64". * @returns {string} The extracted array data type. * @throws {Error} If the array type is invalid or unsupported. */ public static extractArrayDataType(dataType: string): string { if (!isArray(dataType)) { ErrorUtils.throwError("Invalid array type: The provided data type is not an array.", ErrorCode.INVALID_ARGUMENT); } const type = dataType.replace(ARRAY_MATCHER_REGEX, ""); if (type === "") { ErrorUtils.throwError("Failed to extract array type: The array type could not be determined.", ErrorCode.INVALID_ARGUMENT); } return type; } /** * Extracts the key and value data types from the provided map data type string. * * @param {string} dataType - The map data type. eg:"map[u64]string". * @returns The extracted key and value data types as a tuple. * @throws {Error} If the map data type is invalid or unsupported. */ public static extractMapDataType(dataType: string): [string, string] { let brackets: string[] = []; let startIndex: number = 0; let endIndex: number = 0; for (let i = 0; i < dataType.length; i++) { if (dataType.charAt(i) == "[") { if (startIndex === 0) { startIndex = i + 1; } brackets.push("["); } else if (dataType.charAt(i) == "]") { brackets.pop(); if (brackets.length === 0) { endIndex = i; break; } } } const key = dataType.slice(startIndex, endIndex); const value = dataType.replace("map[" + key + "]", ""); if (!key || !value) { ErrorUtils.throwError( "Failed to extract map type: The key or value type of the map could not be determined.", ErrorCode.INVALID_ARGUMENT ); } return [key, value]; } /** * Converts the primitive data type to a standardized representation. * * @param {string} type - The primitive data type. * @returns {string} The converted data type. * @throws {Error} If the data type is unsupported. */ public static convertPrimitiveDataType(type: string): string { switch (type) { case "null": return "null"; case "bool": return "bool"; case "bytes": case "identifier": return "bytes"; case "string": return "string"; case "u64": case "u256": case "i64": case "i256": case "bigint": return "integer"; default: ErrorUtils.throwError("Unsupported data type!", ErrorCode.UNSUPPORTED_OPERATION); } } /** * Parses the fields of a class data type and generates the schema for the class. * * @param {string} className - The name of the class. * @returns {object} The schema for the class. */ public static parseClassFields(className: string, classDef: Map, elements: Map): PoloSchema { const ptr = classDef.get(className); if (ptr === undefined) { ErrorUtils.throwError(`Invalid class name: ${className}`, ErrorCode.INVALID_ARGUMENT); } const element = elements.get(ptr); if (!element) { ErrorUtils.throwError(`Element not found for class: ${className}`, ErrorCode.INVALID_ARGUMENT); } const schema: { kind: string; fields: Record } = { kind: "struct", fields: {}, }; element.data = element.data as LogicManifest.Class; Object.values(element.data.fields!).forEach((field) => { schema.fields[field.label] = Schema.parseDataType(field.type, classDef, elements); }); return schema; } /** * Parses a data type and generates the corresponding schema based on the * data type. The parsing is performed recursively to handle nested data types, * such as arrays, maps and class. * * @param {string} type - The data type string. * @returns {object} The schema generated based on the data type. * @throws {Error} If the data type is unsupported. */ public static parseDataType(type: string, classDef: Map, elements: Map): PoloSchema { switch (true) { case isPrimitiveType(type): return { kind: Schema.convertPrimitiveDataType(type), }; case isArray(type): const values = Schema.extractArrayDataType(type); return { kind: "array", fields: { values: Schema.parseDataType(values, classDef, elements), }, }; case isMap(type): const [key, value] = Schema.extractMapDataType(type); return { kind: "map", fields: { keys: Schema.parseDataType(key, classDef, elements), values: Schema.parseDataType(value, classDef, elements), }, }; case isClass(type, classDef): return this.parseClassFields(type, classDef, elements); default: ErrorUtils.throwError(`Unsupported data type: ${type}!`, ErrorCode.UNSUPPORTED_OPERATION); } } /** * Parses an array of fields and generates the schema based on the fields. * * @param {LogicManifest.TypeField[]} fields - The array of fields. * @returns {PoloSchema} The generated schema based on the fields. * @throws {Error} If the fields are invalid or contain unsupported data types. */ public parseFields(fields: LogicManifest.TypeField[]): PoloSchema { const schema: { kind: string; fields: Record } = { kind: "struct", fields: {}, }; if (!Array.isArray(fields)) { ErrorUtils.throwError("Invalid fields: Fields must be an array.", ErrorCode.INVALID_ARGUMENT); } fields.forEach((field) => { if (!field || !(field.label && field.type)) { ErrorUtils.throwError("Invalid field: Each field must have a label and a type.", ErrorCode.INVALID_ARGUMENT); } schema.fields[field.label] = Schema.parseDataType(field.type, this.classDefs, this.elements); }); return schema; } }