UNPKG

@openzeppelin/contracts-ui-builder-adapter-stellar

Version:
219 lines (187 loc) 8.29 kB
import { nativeToScVal, xdr } from '@stellar/stellar-sdk'; import type { FunctionParameter } from '@openzeppelin/contracts-ui-builder-types'; import { isPlainObject, logger } from '@openzeppelin/contracts-ui-builder-utils'; import { convertStellarTypeToScValType } from '../../utils/formatting'; import { parseGenericType } from './generic-parser'; const SYSTEM_LOG_TAG = 'StructParser'; /** * Determines if a field value needs parsing through parseStellarInput. * Returns false for already-processed values (like Uint8Array for bytes). */ function needsParsing(value: unknown, fieldType: string): boolean { // Skip parsing for already-processed Uint8Array values for bytes types if ((fieldType === 'Bytes' || fieldType.startsWith('BytesN<')) && value instanceof Uint8Array) { return false; } // For Vec types, check if it's already an array of properly typed values if (fieldType.startsWith('Vec<')) { if (!Array.isArray(value)) { return true; // Needs parsing if not an array } // Extract inner type to check array elements const innerTypeMatch = fieldType.match(/Vec<(.+)>$/); if (innerTypeMatch) { // If all elements are already properly typed, skip parsing // For now, let's always parse Vec types to ensure validation return true; } } // For Map types, always parse to convert UI MapEntry format to Stellar SDK format if (fieldType.startsWith('Map<')) { return true; // Map data needs conversion from UI format to Stellar SDK format } // For primitive types, parse if value is string (form input) if (typeof value === 'string') { return true; } // For complex objects, they might need recursive parsing if (isPlainObject(value)) { return true; } // For other cases, skip parsing (assume already processed) return false; } /** * Converts a struct object to ScVal using Laboratory pattern with schema-based type resolution. * * @param structObj - The plain object representing the struct * @param parameterType - The Stellar parameter type (for error messages) * @param paramSchema - Parameter schema with struct field definitions (required) * @param parseInnerValue - Function to recursively parse inner values * @returns ScVal ready for contract calls * @throws Error if schema information is missing for any field */ export function convertStructToScVal( structObj: Record<string, unknown>, parameterType: string, paramSchema: FunctionParameter | undefined, parseInnerValue: (val: unknown, type: string) => unknown ): xdr.ScVal { // Laboratory-style struct conversion with proper type hint handling // Struct conversion using Laboratory pattern with schema-based type resolution // See: laboratory/src/helpers/sorobanUtils.ts convertObjectToScVal const convertedValue: Record<string, unknown> = {}; const typeHints: Record<string, unknown> = {}; // For structs with Vec/Map fields, ensure proper parsing but use nativeToScVal approach // Laboratory pattern: preserve raw values, provide type hints as arrays // Type hints format: [keyType, valueType] where keyType is always "symbol" for (const [fieldName, fieldValue] of Object.entries(structObj)) { // Use schema information for accurate type mapping let fieldType: string | undefined; if (paramSchema?.components) { const fieldSchema = paramSchema.components.find( (comp: FunctionParameter) => comp.name === fieldName ); fieldType = fieldSchema?.type; } if (fieldType) { // Parse the field value using the correct type to ensure validation and conversion // Only parse if the value appears to be raw form input (strings/primitives) // Skip parsing for already-processed values (like Uint8Array for bytes) let parsedValue: unknown; if (needsParsing(fieldValue, fieldType)) { parsedValue = parseInnerValue(fieldValue, fieldType); } else { parsedValue = fieldValue; } // Handle Map fields specially - convert from SDK format to plain object with type hints if (fieldType.startsWith('Map<') && Array.isArray(parsedValue)) { // Extract key and value types const mapTypeMatch = fieldType.match(/Map<(.+),\s*(.+)>$/); const mapKeyType = mapTypeMatch ? mapTypeMatch[1] : 'ScSymbol'; const mapValueType = mapTypeMatch ? mapTypeMatch[2] : 'Bytes'; // parsedValue is in Stellar SDK format: [{0: {value, type}, 1: {value, type}}, ...] // Convert to plain object for nativeToScVal const mapObject: Record<string, unknown> = {}; const mapTypeHints: Record<string, string[]> = {}; ( parsedValue as Array<{ 0: { value: unknown; type: string }; 1: { value: unknown; type: string }; }> ).forEach((entry) => { // Parse the key and value const processedKey = parseInnerValue(entry[0].value, entry[0].type || mapKeyType); const processedVal = parseInnerValue(entry[1].value, entry[1].type || mapValueType); const keyString = typeof processedKey === 'string' ? processedKey : String(processedKey); mapObject[keyString] = processedVal; // Set type hints for each entry using the actual types const keyScValType = convertStellarTypeToScValType(entry[0].type || mapKeyType); const valueScValType = convertStellarTypeToScValType(entry[1].type || mapValueType); // Laboratory pattern: [keyType, valueType] for each map entry mapTypeHints[keyString] = [ Array.isArray(keyScValType) ? keyScValType[0] : keyScValType === 'map-special' ? 'symbol' : keyScValType, Array.isArray(valueScValType) ? valueScValType[0] : valueScValType === 'map-special' ? 'bytes' : valueScValType, ]; }); convertedValue[fieldName] = mapObject; // Provide nested type hints for Map field (Laboratory pattern) typeHints[fieldName] = ['symbol', mapTypeHints]; } else { convertedValue[fieldName] = parsedValue; // Use exact type from schema for non-Map fields const scValType = convertStellarTypeToScValType(fieldType); if (scValType !== 'map-special') { typeHints[fieldName] = ['symbol', Array.isArray(scValType) ? scValType[0] : scValType]; } } } else { // Schema is required for struct field type resolution throw new Error( `Missing schema information for struct field "${fieldName}" in struct type "${parameterType}". ` + `Schema-based type resolution is required for accurate ScVal conversion.` ); } } logger.debug(SYSTEM_LOG_TAG, 'convertStructToScVal final values:', { parameterType, convertedValue, typeHints, }); const scVal = nativeToScVal(convertedValue, { type: typeHints }); logger.debug(SYSTEM_LOG_TAG, 'convertStructToScVal generated ScVal:', { parameterType, scValType: scVal.switch().name, scValValue: scVal.value(), }); return scVal; } /** * Checks if the given value and type represent a struct that can be handled by this parser. * * @param value - The value to check * @param parameterType - The Stellar parameter type * @returns True if this is a struct type */ export function isStructType(value: unknown, parameterType: string): boolean { // Check if this is a struct type (object that doesn't match enum pattern or generic pattern) // Exclude special object types like Uint8Array, Date, etc. if (!isPlainObject(value)) { return false; } // Skip if it's a generic type const genericInfo = parseGenericType(parameterType); if (genericInfo) { return false; } // Skip if it looks like an enum (has 'tag', 'enum', or 'values' properties) const obj = value as Record<string, unknown>; if ('tag' in obj || 'enum' in obj || 'values' in obj) { return false; } // Check if it's a plain object with simple constructor return ( !(value instanceof Uint8Array) && !(value instanceof Date) && typeof value.constructor === 'function' && value.constructor === Object ); }