/* This file is part of web3.js. web3.js is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. web3.js is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ import { FormatterError } from '@theqrl/web3-errors'; import { Bytes, DataFormat, FMT_BYTES, FMT_NUMBER, FormatType } from '@theqrl/web3-types'; import { isNullish, isObject, JsonSchema, utils, ValidationSchemaInput, } from '@theqrl/web3-validator'; import { bytesToUint8Array, bytesToHex, numberToHex, toBigInt } from './converters.js'; import { mergeDeep } from './objects.js'; import { padLeft } from './string_manipulation.js'; import { uint8ArrayConcat } from './uint8array.js'; const { parseBaseType } = utils; export const isDataFormat = (dataFormat: unknown): dataFormat is DataFormat => typeof dataFormat === 'object' && !isNullish(dataFormat) && 'number' in dataFormat && 'bytes' in dataFormat; /** * Finds the schema that corresponds to a specific data path within a larger JSON schema. * It works by iterating over the dataPath array and traversing the JSON schema one step at a time until it reaches the end of the path. * * @param schema - represents a JSON schema, which is an object that describes the structure of JSON data * @param dataPath - represents an array of strings that specifies the path to the data within the JSON schema * @param oneOfPath - represents an optional array of two-element tuples that specifies the "oneOf" option to choose, if the schema has oneOf and the data path can match multiple subschemas * @returns the JSON schema that matches the data path * */ const findSchemaByDataPath = ( schema: JsonSchema, dataPath: string[], oneOfPath: [string, number][] = [], ): JsonSchema | undefined => { let result: JsonSchema = { ...schema } as JsonSchema; let previousDataPath: string | undefined; for (const dataPart of dataPath) { if (result.oneOf && previousDataPath) { const prev = previousDataPath ?? ''; const path = oneOfPath.find((element: [string, number]) => prev === element[0]); if (path && path[0] === previousDataPath) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access result = result.oneOf[path[1]]; } } if (!result.properties && !result.items) { return undefined; } if (result.properties) { result = (result.properties as Record)[dataPart]; } else if (result.items && (result.items as JsonSchema).properties) { const node = (result.items as JsonSchema).properties as Record; if (!node) { return undefined; } result = node[dataPart]; } else if (result.items && isObject(result.items)) { result = result.items; } else if (result.items && Array.isArray(result.items)) { result = result.items[parseInt(dataPart, 10)]; } if (result && dataPart) previousDataPath = dataPart; } return result; }; /** * Converts a value depending on the format * @param value - value to convert * @param qrlType - The type of the value to be parsed * @param format - The format to be converted to * @returns - The value converted to the specified format */ export const convertScalarValue = (value: unknown, qrlType: string, format: DataFormat) => { try { const { baseType, baseTypeSize } = parseBaseType(qrlType); if (baseType === 'int' || baseType === 'uint') { switch (format.number) { case FMT_NUMBER.NUMBER: return Number(toBigInt(value)); case FMT_NUMBER.HEX: return numberToHex(toBigInt(value)); case FMT_NUMBER.STR: return toBigInt(value).toString(); case FMT_NUMBER.BIGINT: return toBigInt(value); default: throw new FormatterError(`Invalid format: ${String(format.number)}`); } } if (baseType === 'bytes') { let paddedValue; if (baseTypeSize) { if (typeof value === 'string') paddedValue = padLeft(value, baseTypeSize * 2); else if (value instanceof Uint8Array) { paddedValue = uint8ArrayConcat( new Uint8Array(baseTypeSize - value.length), value, ); } } else { paddedValue = value; } switch (format.bytes) { case FMT_BYTES.HEX: return bytesToHex(bytesToUint8Array(paddedValue as Bytes)); case FMT_BYTES.UINT8ARRAY: return bytesToUint8Array(paddedValue as Bytes); default: throw new FormatterError(`Invalid format: ${String(format.bytes)}`); } } } catch (error) { // If someone didn't use `eth` keyword we can return original value // as the scope of this code is formatting not validation return value; } return value; }; /** * Converts the data to the specified format * @param data - data to convert * @param schema - The JSON schema that describes the structure of the data * @param dataPath - A string array that specifies the path to the data within the JSON schema * @param format - The format to be converted to * @param oneOfPath - An optional array of two-element tuples that specifies the "oneOf" option to choose, if the schema has oneOf and the data path can match multiple subschemas * @returns - The data converted to the specified format */ export const convert = ( data: Record | unknown[] | unknown, schema: JsonSchema, dataPath: string[], format: DataFormat, oneOfPath: [string, number][] = [], ) => { // If it's a scalar value if (!isObject(data) && !Array.isArray(data)) { return convertScalarValue(data, schema?.format as string, format); } const object = data as Record; for (const [key, value] of Object.entries(object)) { dataPath.push(key); const schemaProp = findSchemaByDataPath(schema, dataPath, oneOfPath); // If value is a scaler value if (isNullish(schemaProp)) { delete object[key]; dataPath.pop(); continue; } // If value is an object, recurse into it if (isObject(value)) { convert(value, schema, dataPath, format); dataPath.pop(); continue; } // If value is an array if (Array.isArray(value)) { let _schemaProp = schemaProp; // TODO This is a naive approach to solving the issue of // a schema using oneOf. This chunk of code was intended to handle // BlockSchema.transactions // TODO BlockSchema.transactions are not being formatted if (schemaProp?.oneOf !== undefined) { // The following code is basically saying: // if the schema specifies oneOf, then we are to loop // over each possible schema and check if they type of the schema // matches the type of value[0], and if so we use the oneOfSchemaProp // as the schema for formatting // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call schemaProp.oneOf.forEach((oneOfSchemaProp: JsonSchema, index: number) => { if ( !Array.isArray(schemaProp?.items) && ((typeof value[0] === 'object' && (oneOfSchemaProp?.items as JsonSchema)?.type === 'object') || (typeof value[0] === 'string' && (oneOfSchemaProp?.items as JsonSchema)?.type !== 'object')) ) { _schemaProp = oneOfSchemaProp; oneOfPath.push([key, index]); } }); } if (isNullish(_schemaProp?.items)) { // Can not find schema for array item, delete that item delete object[key]; dataPath.pop(); continue; } // If schema for array items is a single type if (isObject(_schemaProp.items) && !isNullish(_schemaProp.items.format)) { for (let i = 0; i < value.length; i += 1) { (object[key] as unknown[])[i] = convertScalarValue( value[i], // eslint-disable-next-line @typescript-eslint/no-unsafe-argument _schemaProp?.items?.format, format, ); } dataPath.pop(); continue; } // If schema for array items is an object if (!Array.isArray(_schemaProp?.items) && _schemaProp?.items?.type === 'object') { for (const arrObject of value) { convert( arrObject as Record | unknown[], schema, dataPath, format, oneOfPath, ); } dataPath.pop(); continue; } // If schema for array is a tuple if (Array.isArray(_schemaProp?.items)) { for (let i = 0; i < value.length; i += 1) { (object[key] as unknown[])[i] = convertScalarValue( value[i], _schemaProp.items[i].format as string, format, ); } dataPath.pop(); continue; } } object[key] = convertScalarValue(value, schemaProp.format as string, format); dataPath.pop(); } return object; }; export const format = < DataType extends Record | unknown[] | unknown, ReturnType extends DataFormat, >( schema: ValidationSchemaInput | JsonSchema, data: DataType, returnFormat: ReturnType, ): FormatType => { let dataToParse: Record | unknown[] | unknown; if (isObject(data)) { dataToParse = mergeDeep({}, data); } else if (Array.isArray(data)) { dataToParse = [...data]; } else { dataToParse = data; } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const jsonSchema: JsonSchema = isObject(schema) ? schema : utils.qrlAbiToJsonSchema(schema); if (!jsonSchema.properties && !jsonSchema.items && !jsonSchema.format) { throw new FormatterError('Invalid json schema for formatting'); } return convert(dataToParse, jsonSchema, [], returnFormat) as FormatType< typeof data, ReturnType >; };