js-moi-manifest
Version:
Module to encode and decode MOI Logic Engine input and output.
335 lines (289 loc) • 13.7 kB
text/typescript
import { builtInLogEventSchema, bytesToHex, deepCopy, ErrorCode, ErrorUtils, hexToBytes, trimHexPrefix } from "js-moi-utils";
import { Depolorizer, documentEncode, Schema as PoloSchema } from "js-polo";
import { LogicManifest } from "../types/manifest";
import { Exception } from "../types/response";
import { ElementDescriptor } from "./element-descriptor";
import { JsonManifestCoder } from "./manifest-coder/json-manifest-coder";
import { ManifestCoderFormat } from "./manifest-coder/serialization-format";
import { YamlManifestCoder } from "./manifest-coder/yaml-manifest-coder";
import { Schema } from "./schema";
/**
* ManifestCoder is a class that provides encoding and decoding functionality
* for Logic Interface.It allows encoding manifests and arguments, as well as
* decoding output, exceptions and logic states based on both a predefined and
* runtime schema.
*
* @class
*/
export class ManifestCoder {
private readonly elementDescriptor: ElementDescriptor;
/**
* Creates an instance of ManifestCoder.
*
* @param {LogicManifest.Manifest} manifest - The logic manifest.
* @constructor
*/
constructor(manifest: LogicManifest.Manifest);
/**
* Creates an instance of ManifestCoder.
*/
constructor(manifest: LogicManifest.Manifest) {
this.elementDescriptor = new ElementDescriptor(manifest.elements);
}
private get schema(): Schema {
return new Schema(this.elementDescriptor.getElements(), this.elementDescriptor.getClassDefs());
}
/**
* Parses the calldata arguments based on the provided POLO Schema.
* The calldata arguments is recursively processed and transformed according to the schema.
*
* @private
* @param {PoloSchema} schema - The schema definition for the calldata.
* @param {*} arg - The calldata argument to parse.
* @param {boolean} [updateType=true] - Indicates whether to update the schema type during parsing.
* @returns {*} The parsed calldata argument.
*/
private parseCalldata(schema: PoloSchema, arg: any, updateType: boolean = true): any {
const parsableKinds = ["bytes", "array", "map", "struct"];
const reconstructSchema = (schema: PoloSchema): PoloSchema => {
Object.keys(schema.fields).forEach(key => {
if (schema.fields[key].kind === "struct") {
schema.fields[key].kind = "document";
}
});
return schema;
}
const parseArray = (schema: PoloSchema, arg: any[]) => {
return arg.map((value: any, index: number) =>
this.parseCalldata(schema, value, arg.length - 1 === index)
);
}
const parseMap = (schema: PoloSchema, arg: Map<any, any>) => {
const map = new Map()
const entries = Array.from(arg.entries());
// Loop through the entries of the Map
entries.forEach((entry: [any, any], index: number) => {
const [key, value] = entry;
map.set(
this.parseCalldata(
schema.fields.keys,
key,
entries.length - 1 === index
),
this.parseCalldata(
schema.fields.values,
value,
entries.length - 1 === index
)
);
});
return map;
}
const parseStruct = (schema: PoloSchema, arg: any, updateType: boolean) => {
Object.keys(arg).forEach(key => {
arg[key] = this.parseCalldata(schema.fields[key], arg[key], false)
});
const doc = documentEncode(arg, reconstructSchema(deepCopy(schema)))
if (updateType) {
schema.kind = "document";
delete schema.fields;
}
return doc.getData();
}
switch (schema.kind) {
case "string":
return trimHexPrefix(arg);
case "bytes":
if (typeof arg === "string") {
return hexToBytes(arg);
}
break;
case "array":
if (parsableKinds.includes(schema.fields.values.kind)) {
return parseArray(schema.fields.values, arg);
}
break;
case "map":
if ((parsableKinds.includes(schema.fields.keys.kind) ||
parsableKinds.includes(schema.fields.values.kind))) {
return parseMap(schema, arg);
}
break;
case "struct":
return parseStruct(schema, arg, updateType);
default:
break;
}
return arg;
}
/**
* Encodes the arguments for a specified routine into a hexadecimal string.
*
* @param routine - The name of the routine for which the arguments are being encoded.
* @param args - The arguments to be encoded, passed as a variadic parameter.
* @returns A hexadecimal string representing the encoded arguments.
*/
public encodeArguments(routine: string, ...args: any[]): string {
const element = this.elementDescriptor.getRoutineElement(routine).data as LogicManifest.Routine
const schema = this.schema.parseFields(element.accepts ?? []);
const calldata = Object.values(element.accepts).reduce((acc, field: LogicManifest.TypeField) => {
acc[field.label] = this.parseCalldata(schema.fields[field.label], args[field.slot]);
return acc;
}, {});
return "0x" + bytesToHex((documentEncode(calldata, schema).bytes()))
}
/**
* Decodes the provided calldata into the expected arguments for a given routine.
*
* @template T - The type of the decoded arguments.
* @param {string} routine - The name of the routine whose arguments are to be decoded.
* @param {string} calldata - The calldata to decode.
* @returns {T | null} - The decoded arguments as an object of type T, or null if the routine accepts no arguments.
*/
public decodeArguments<T>(routine: string, calldata: string): T | null {
const element = this.elementDescriptor.getRoutineElement(routine).data as LogicManifest.Routine
if (element && element.accepts.length === 0) {
return null
}
const schema = this.schema.parseFields(element.accepts ?? []);
const decodedCalldata = new Depolorizer(hexToBytes(calldata)).depolorize(schema);
return element.accepts.map((field: LogicManifest.TypeField) => decodedCalldata[field.label]) as T;
}
/**
* Decodes the output of a routine.
*
* @template T - The type to which the output should be decoded.
* @param {string} routine - The name of the routine whose output is to be decoded.
* @param {string} output - The output string to decode.
* @returns {T | null} - The decoded output as type T, or null if the output is invalid or the routine has no return schema.
*/
public decodeOutput<T>(routine: string, output: string): T | null {
const element = this.elementDescriptor.getRoutineElement(routine).data as LogicManifest.Routine
if (output && output != "0x" && element.returns && element.returns.length) {
const schema = this.schema.parseFields(element.returns);
return new Depolorizer(hexToBytes(output)).depolorize(schema) as T
}
return null
}
/**
* Decodes a log data from an event emitted in a logic.
*
* @param {string} event - The name of the event.
* @param {string} logData - The log data to decode, represented as a hexadecimal string prefixed with "0x".
* @returns {T | null} The decoded event log data, or null if the log data is empty.
*/
public decodeEventOutput<T>(event: string, logData: string): T | null;
/**
* Decodes a log data from an event emitted in a logic.
*
* @param {string} logData - The log data to decode, represented as a hexadecimal string prefixed with "0x".
* @returns {T | null} The decoded event log data, or null if the log data is empty.
*/
public decodeEventOutput(event: "builtin.Log", logData: string): { value: string } | null;
/**
* Decodes a log data from an event emitted in a logic.
*
* @param {string} event - The name of the event.
* @param {string} logData - The POLO encoded log data to be decoded.
* @returns {T | null} The decoded event log data, or null if the log data is empty.
*/
public decodeEventOutput<T>(event: string, logData: string): T | null {
if (event === "builtin.Log") {
return new Depolorizer(hexToBytes(logData)).depolorize(builtInLogEventSchema) as T;
}
const element = this.elementDescriptor.getEventElement(event);
if (element == null) {
throw new Error(`Event ${event} not found in manifest`);
}
if (logData && logData !== "0x") {
const element = this.elementDescriptor.getEventElement(event);
const schema = this.schema.parseFields(element.data.fields);
return new Depolorizer(hexToBytes(logData)).depolorize(schema) as T;
}
return null
}
/**
* Decodes an exception thrown by a logic routine call.
* The exception data is decoded using the predefined exception schema.
* Returns the decoded exception object, or null if the error is empty.
*
* @param {string} error - The error data to decode, represented as a
hexadecimal string prefixed with "0x".
* @returns {Exception | null} The decoded exception object, or null if
the error is empty.
*/
public static decodeException(error: string): Exception | null {
if (error && error !== "0x") {
const decodedError = hexToBytes(error)
const depolorizer = new Depolorizer(decodedError)
return depolorizer.depolorize(Schema.PISA_EXCEPTION_SCHEMA) as Exception
}
return null
}
/**
* Encodes a manifest into a hexadecimal string.
*
* This function supports encoding both JSON and YAML manifest formats.
* If the input manifest is an object, it is assumed to be a JSON manifest and
* is encoded using the `JsonManifestCoder`. If the input manifest is a string,
* it is assumed to be a YAML manifest and is encoded using the `YamlManifestCoder`.
*
* @param manifest - The manifest to encode. It can be either a string (YAML) or an object (JSON).
* @returns The encoded manifest as a hexadecimal string prefixed with "0x".
* @throws Will throw an error if the manifest type is unsupported.
*/
public static encodeManifest(manifest: string | LogicManifest.Manifest): string {
if (typeof manifest === "object" && manifest !== null) {
const serializer = new JsonManifestCoder();
return "0x" + bytesToHex(serializer.encode(manifest));
}
if (typeof manifest === "string") {
const serializer = new YamlManifestCoder();
return "0x" + bytesToHex(serializer.encode(manifest));
}
ErrorUtils.throwError("Unsupported manifest type", ErrorCode.UNSUPPORTED_OPERATION);
}
/**
* Decodes a given manifest in either JSON format.
*
* @param manifest - The manifest data to decode, provided as a string or Uint8Array.
* @param format - The format of the manifest in JSON string.
*
* @returns Returns a JSON manifest object.
*
* @throws Throws an error if the format is unsupported.
*/
public static decodeManifest(manifest: string | Uint8Array, format: ManifestCoderFormat.JSON): LogicManifest.Manifest;
/**
* Decodes a given manifest in either YAML format.
*
* @param {string | Uint8Array} manifest The manifest data to decode, provided as a string or Uint8Array.
* @param {ManifestCoderFormat} format The format of the manifest in YAML string.
*
* @returns {string} Returns a YAML string representation.
*
* @throws {Error} Throws an error if the format is unsupported.
*/
public static decodeManifest(manifest: string | Uint8Array, format: ManifestCoderFormat.YAML): string;
/**
* Decodes a given manifest in either JSON or YAML format.
*
* @param {string | Uint8Array} manifest - The manifest data to decode, provided as a string or Uint8Array.
* @param {ManifestCoderFormat} format - The format of the manifest, either JSON or YAML.
*
* @returns {LogicManifest.Manifest | string} - Returns a `LogicManifest.Manifest` object if JSON format is used or a string representation if YAML format is used.
*
* @throws {Error} - Throws an error if the format is unsupported.
*/
public static decodeManifest(manifest: string | Uint8Array, format: ManifestCoderFormat): LogicManifest.Manifest | string {
if (format === ManifestCoderFormat.JSON) {
const serializer = new JsonManifestCoder();
return serializer.decode(manifest);
}
if (format === ManifestCoderFormat.YAML) {
const serializer = new YamlManifestCoder();
return serializer.decode(manifest);
}
ErrorUtils.throwError("Unsupported manifest format", ErrorCode.UNSUPPORTED_OPERATION);
}
}