@openzeppelin/contracts-ui-builder-adapter-stellar
Version:
Stellar Adapter for Contracts UI Builder
167 lines (144 loc) • 6.32 kB
text/typescript
import { nativeToScVal, xdr } from '@stellar/stellar-sdk';
import { isEnumValue, type FunctionParameter } from '@openzeppelin/contracts-ui-builder-types';
import { convertStellarTypeToScValType } from '../../utils/formatting';
import { convertEnumToScVal } from '../../utils/input-parsing';
import { parseGenericType } from './generic-parser';
import { parsePrimitive } from './primitive-parser';
import { convertStructToScVal, isStructType } from './struct-parser';
import type { SorobanEnumValue } from './types';
/**
* Converts a value to ScVal with comprehensive generic type support.
* This should be used in the transaction execution instead of calling nativeToScVal directly.
*
* @param value - The parsed value from parseStellarInput
* @param parameterType - The Stellar parameter type
* @param paramSchema - Optional parameter schema with struct field definitions
* @param parseInnerValue - Function to recursively parse inner values (defaults to parseStellarInput)
* @returns ScVal ready for contract calls
*/
export function valueToScVal(
value: unknown,
parameterType: string,
paramSchema?: FunctionParameter,
parseInnerValue?: (val: unknown, type: string) => unknown
): xdr.ScVal {
// Default parseInnerValue to a basic pass-through function if not provided
// This handles the common case where callers don't need recursive parsing
const parseValue = parseInnerValue || ((val: unknown) => val);
const genericInfo = parseGenericType(parameterType);
if (!genericInfo) {
// Check if this is an enum object (has 'tag' or 'enum' property)
if (isEnumValue(value) || (typeof value === 'object' && value !== null && 'enum' in value)) {
return convertEnumToScVal(value as SorobanEnumValue);
}
// Check if this is a struct type
if (isStructType(value, parameterType)) {
return convertStructToScVal(
value as Record<string, unknown>,
parameterType,
paramSchema,
parseValue
);
}
// Non-generic types - use existing logic
if (parameterType === 'Bool' || parameterType === 'Bytes') {
return nativeToScVal(value);
}
const scValType = convertStellarTypeToScValType(parameterType);
const typeHint = Array.isArray(scValType) ? scValType[0] : scValType;
return nativeToScVal(value, { type: typeHint });
}
const { baseType, parameters } = genericInfo;
switch (baseType) {
case 'Vec': {
// Handle Vec<T> types
const innerType = parameters[0];
if (Array.isArray(value)) {
const convertedElements = value.map((element) => valueToScVal(element, innerType));
return nativeToScVal(convertedElements);
}
return nativeToScVal(value);
}
case 'Map': {
// Handle Map<K,V> types in Stellar SDK format
if (Array.isArray(value)) {
// Expect Stellar SDK format: [{ 0: {value, type}, 1: {value, type} }, ...]
const convertedValue: Record<string, unknown> = {};
const typeHints: Record<string, string[]> = {};
value.forEach(
(entry: { 0: { value: string; type: string }; 1: { value: string; type: string } }) => {
if (
typeof entry !== 'object' ||
entry === null ||
!entry[0] ||
!entry[1] ||
typeof entry[0].value === 'undefined' ||
typeof entry[1].value === 'undefined'
) {
throw new Error('Invalid Stellar SDK map format in valueToScVal');
}
// Process key and value through parsePrimitive for bytes conversion
// This ensures bytes strings are converted to Uint8Array before nativeToScVal
let processedKey: unknown = entry[0].value;
let processedValue: unknown = entry[1].value;
// Handle bytes conversion for keys
const keyPrimitive = parsePrimitive(entry[0].value, entry[0].type);
if (keyPrimitive !== null) {
processedKey = keyPrimitive;
}
// Handle bytes conversion for values
const valuePrimitive = parsePrimitive(entry[1].value, entry[1].type);
if (valuePrimitive !== null) {
processedValue = valuePrimitive;
}
// Use the processed key as the object key (convert to string if needed)
const keyString =
typeof processedKey === 'string' ? processedKey : String(processedKey);
convertedValue[keyString] = processedValue;
const keyScValType = convertStellarTypeToScValType(entry[0].type);
const valueScValType = convertStellarTypeToScValType(entry[1].type);
typeHints[keyString] = [
Array.isArray(keyScValType) ? keyScValType[0] : keyScValType,
Array.isArray(valueScValType) ? valueScValType[0] : valueScValType,
];
}
);
return nativeToScVal(convertedValue, { type: typeHints });
}
return nativeToScVal(value);
}
case 'Option': {
// Handle Option<T> types
const innerType = parameters[0];
if (value === null || value === undefined) {
return nativeToScVal(null); // None variant
} else {
// Some variant - convert the inner value
return valueToScVal(value, innerType);
}
}
case 'Result': {
// Handle Result<T,E> types
const okType = parameters[0];
const errType = parameters[1];
// Result types typically come as {ok: value} or {err: error}
if (typeof value === 'object' && value !== null) {
const resultObj = value as Record<string, unknown>;
if ('ok' in resultObj) {
const okScVal = valueToScVal(resultObj.ok, okType);
return nativeToScVal({ ok: okScVal });
} else if ('err' in resultObj) {
const errScVal = valueToScVal(resultObj.err, errType);
return nativeToScVal({ err: errScVal });
}
}
return nativeToScVal(value);
}
default: {
// Unknown generic type - fallback to basic conversion
const scValType = convertStellarTypeToScValType(parameterType);
const typeHint = Array.isArray(scValType) ? scValType[0] : scValType;
return nativeToScVal(value, { type: typeHint });
}
}
}