@openzeppelin/ui-builder-adapter-stellar 0.15.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,12 +4,18 @@ import { isEnumValue, type FunctionParameter } from '@openzeppelin/ui-builder-ty
4
4
 
5
5
  import { convertStellarTypeToScValType } from '../../utils/formatting';
6
6
  import { convertEnumToScVal } from '../../utils/input-parsing';
7
- import { isBytesNType } from '../../utils/type-detection';
7
+ import { isPrimitiveParamType } from '../../utils/stellar-types';
8
+ import { isBytesNType, isLikelyEnumType } from '../../utils/type-detection';
9
+ import { compareScValsByXdr } from '../../utils/xdr-ordering';
8
10
  import { parseGenericType } from './generic-parser';
9
11
  import { parsePrimitive } from './primitive-parser';
10
12
  import { convertStructToScVal, isStructType } from './struct-parser';
11
13
  import type { SorobanEnumValue } from './types';
12
14
 
15
+ // FunctionParameter already includes enumMetadata in its type definition (from @openzeppelin/ui-builder-types)
16
+ // No need for a separate type wrapper
17
+ type EnumAwareFunctionParameter = FunctionParameter;
18
+
13
19
  /**
14
20
  * Converts a value to ScVal with comprehensive generic type support.
15
21
  * This should be used in the transaction execution instead of calling nativeToScVal directly.
@@ -31,19 +37,152 @@ export function valueToScVal(
31
37
  const parseValue = parseInnerValue || ((val: unknown) => val);
32
38
  const genericInfo = parseGenericType(parameterType);
33
39
 
40
+ // Helper: detect SorobanArgumentValue wrapper { type, value }
41
+ const isTypedWrapper = (v: unknown): v is { type: string; value: unknown } =>
42
+ !!v &&
43
+ typeof v === 'object' &&
44
+ 'type' in (v as Record<string, unknown>) &&
45
+ 'value' in (v as Record<string, unknown>);
46
+
47
+ const enumMetadata = (paramSchema as EnumAwareFunctionParameter | undefined)?.enumMetadata;
48
+ const possibleEnumValue =
49
+ typeof value === 'string' && (enumMetadata || isLikelyEnumType(parameterType))
50
+ ? { tag: value }
51
+ : value;
52
+
34
53
  if (!genericInfo) {
54
+ // Integer-only enums (discriminant enums) → encode as u32 (matches Lab behavior)
55
+ if (enumMetadata && enumMetadata.variants.every((v) => v.type === 'integer')) {
56
+ // Derive numeric discriminant from name, tag, or numeric input
57
+ let numericValue: number | undefined;
58
+ if (typeof value === 'string') {
59
+ const byName = enumMetadata.variants.find((v) => v.name === value);
60
+ numericValue = byName?.value ?? Number(value);
61
+ } else if (typeof value === 'number') {
62
+ numericValue = value;
63
+ } else if (isEnumValue(value)) {
64
+ const byTag = enumMetadata.variants.find((v) => v.name === value.tag);
65
+ numericValue = byTag?.value;
66
+ }
67
+ if (numericValue === undefined || Number.isNaN(numericValue)) {
68
+ const validNames = enumMetadata.variants.map((v) => v.name).join(', ');
69
+ throw new Error(
70
+ `Invalid integer enum value for ${parameterType}: ${String(value)}. Expected one of: ${validNames}`
71
+ );
72
+ }
73
+ return nativeToScVal(numericValue, { type: 'u32' });
74
+ }
75
+ // If a typed wrapper is provided, convert directly using the wrapped type/value
76
+ if (isTypedWrapper(possibleEnumValue)) {
77
+ const wrapped = possibleEnumValue;
78
+ const parsed = parsePrimitive(wrapped.value, wrapped.type);
79
+ const finalVal = parsed !== null ? parsed : wrapped.value;
80
+ const scValType = convertStellarTypeToScValType(wrapped.type);
81
+ const typeHint = Array.isArray(scValType) ? scValType[0] : scValType;
82
+ return nativeToScVal(finalVal, { type: typeHint });
83
+ }
84
+
35
85
  // Check if this is an enum object (has 'tag' or 'enum' property)
36
- if (isEnumValue(value) || (typeof value === 'object' && value !== null && 'enum' in value)) {
37
- return convertEnumToScVal(value as SorobanEnumValue);
86
+ if (
87
+ isEnumValue(possibleEnumValue) ||
88
+ (typeof possibleEnumValue === 'object' &&
89
+ possibleEnumValue !== null &&
90
+ 'enum' in possibleEnumValue)
91
+ ) {
92
+ const enumValue = possibleEnumValue as { tag: string; values?: unknown[]; enum?: number };
93
+
94
+ // Handle integer enums
95
+ if ('enum' in enumValue && typeof enumValue.enum === 'number') {
96
+ return nativeToScVal(enumValue.enum, { type: 'u32' });
97
+ }
98
+
99
+ // Handle tagged enums with metadata for proper type conversion
100
+ const tagSymbol = nativeToScVal(enumValue.tag, { type: 'symbol' });
101
+
102
+ if (!enumValue.values || enumValue.values.length === 0) {
103
+ // Unit variant - ScVec containing single ScSymbol
104
+ return xdr.ScVal.scvVec([tagSymbol]);
105
+ }
106
+
107
+ const payloadValues = enumValue.values as unknown[];
108
+
109
+ // Tuple variant - convert each payload value with proper types
110
+ let payloadScVals: xdr.ScVal[];
111
+ // Use the variant type from EnumAwareFunctionParameter's enumMetadata
112
+ type EnumVariant = NonNullable<
113
+ EnumAwareFunctionParameter['enumMetadata']
114
+ >['variants'][number];
115
+ let variant: EnumVariant | undefined;
116
+
117
+ if (enumMetadata) {
118
+ variant = enumMetadata.variants.find((variantEntry) => variantEntry.name === enumValue.tag);
119
+ if (!variant || !variant.payloadTypes) {
120
+ // No variant metadata or payloadTypes - use convertEnumToScVal fallback
121
+ return convertEnumToScVal(enumValue as SorobanEnumValue);
122
+ }
123
+ // Convert each payload value with its corresponding type
124
+ // variant is guaranteed to be defined here due to the check above
125
+ const payloadTypes = variant.payloadTypes;
126
+ const payloadComponents = variant.payloadComponents;
127
+ payloadScVals = payloadTypes.map((payloadType, index) => {
128
+ const payloadSchema = payloadComponents?.[index]
129
+ ? {
130
+ name: `payload_${index}`,
131
+ type: payloadType,
132
+ components: payloadComponents[index],
133
+ }
134
+ : { name: `payload_${index}`, type: payloadType };
135
+
136
+ const val = payloadValues[index];
137
+ return valueToScVal(val, payloadType, payloadSchema, parseValue);
138
+ });
139
+ } else {
140
+ // No enum metadata - use convertEnumToScVal fallback
141
+ return convertEnumToScVal(enumValue as SorobanEnumValue);
142
+ }
143
+
144
+ // For single Tuple payload, wrap all payload ScVals in another ScVec
145
+ // Example: Some((Address, i128)) → ScVec([Symbol("Some"), ScVec([Address, I128])])
146
+ if (variant?.isSingleTuplePayload) {
147
+ const tuplePayloadVec = xdr.ScVal.scvVec(payloadScVals);
148
+ return xdr.ScVal.scvVec([tagSymbol, tuplePayloadVec]);
149
+ }
150
+
151
+ // Return ScVec with tag symbol followed by payload values
152
+ return xdr.ScVal.scvVec([tagSymbol, ...payloadScVals]);
38
153
  }
39
154
 
40
- // Check if this is a struct type
41
- if (isStructType(value, parameterType)) {
155
+ // Check if this is a struct or tuple type
156
+ // Accept array-shaped values for tuple-structs when schema components are provided
157
+ if (
158
+ Array.isArray(possibleEnumValue) &&
159
+ paramSchema?.components &&
160
+ paramSchema.components.length
161
+ ) {
162
+ // Runtime validation: ensure array length matches schema components
163
+ if (possibleEnumValue.length !== paramSchema.components.length) {
164
+ throw new Error(
165
+ `Tuple-struct value length (${possibleEnumValue.length}) does not match schema components (${paramSchema.components.length}) for type ${parameterType}`
166
+ );
167
+ }
168
+ return convertStructToScVal(
169
+ possibleEnumValue as unknown as Record<string, unknown>,
170
+ parameterType,
171
+ paramSchema,
172
+ parseValue,
173
+ (innerValue, innerType, innerSchema) =>
174
+ valueToScVal(innerValue, innerType, innerSchema, parseInnerValue)
175
+ );
176
+ }
177
+
178
+ if (!isPrimitiveParamType(parameterType) && isStructType(value, parameterType)) {
42
179
  return convertStructToScVal(
43
180
  value as Record<string, unknown>,
44
181
  parameterType,
45
182
  paramSchema,
46
- parseValue
183
+ parseValue,
184
+ (innerValue, innerType, innerSchema) =>
185
+ valueToScVal(innerValue, innerType, innerSchema, parseInnerValue)
47
186
  );
48
187
  }
49
188
 
@@ -84,7 +223,28 @@ export function valueToScVal(
84
223
  // Handle Vec<T> types
85
224
  const innerType = parameters[0];
86
225
  if (Array.isArray(value)) {
87
- const convertedElements = value.map((element) => valueToScVal(element, innerType));
226
+ // For enum element types, we need to pass enum metadata
227
+ // Check if paramSchema has enumMetadata or components that should be used for elements
228
+ let elementSchema: FunctionParameter | undefined;
229
+ if (enumMetadata) {
230
+ // This Vec is of enum type - pass the enum metadata to each element
231
+ elementSchema = {
232
+ name: 'element',
233
+ type: innerType,
234
+ enumMetadata,
235
+ } as EnumAwareFunctionParameter;
236
+ } else if (paramSchema?.components) {
237
+ // This Vec is of struct type - pass the components to each element
238
+ elementSchema = {
239
+ name: 'element',
240
+ type: innerType,
241
+ components: paramSchema.components,
242
+ };
243
+ }
244
+
245
+ const convertedElements = value.map((element) =>
246
+ valueToScVal(element, innerType, elementSchema, parseValue)
247
+ );
88
248
  return nativeToScVal(convertedElements);
89
249
  }
90
250
  return nativeToScVal(value);
@@ -94,8 +254,7 @@ export function valueToScVal(
94
254
  // Handle Map<K,V> types in Stellar SDK format
95
255
  if (Array.isArray(value)) {
96
256
  // Expect Stellar SDK format: [{ 0: {value, type}, 1: {value, type} }, ...]
97
- const convertedValue: Record<string, unknown> = {};
98
- const typeHints: Record<string, string[]> = {};
257
+ const mapEntries: xdr.ScMapEntry[] = [];
99
258
 
100
259
  value.forEach(
101
260
  (entry: { 0: { value: string; type: string }; 1: { value: string; type: string } }) => {
@@ -111,7 +270,6 @@ export function valueToScVal(
111
270
  }
112
271
 
113
272
  // Process key and value through parsePrimitive for bytes conversion
114
- // This ensures bytes strings are converted to Uint8Array before nativeToScVal
115
273
  let processedKey: unknown = entry[0].value;
116
274
  let processedValue: unknown = entry[1].value;
117
275
 
@@ -127,26 +285,71 @@ export function valueToScVal(
127
285
  processedValue = valuePrimitive;
128
286
  }
129
287
 
130
- // Use the processed key as the object key (convert to string if needed)
131
- const keyString =
132
- typeof processedKey === 'string' ? processedKey : String(processedKey);
133
- convertedValue[keyString] = processedValue;
134
-
288
+ // Create ScVals for key and value
135
289
  const keyScValType = convertStellarTypeToScValType(entry[0].type);
290
+ const keyTypeHint = Array.isArray(keyScValType) ? keyScValType[0] : keyScValType;
291
+ const keyScVal = nativeToScVal(processedKey, { type: keyTypeHint });
292
+
136
293
  const valueScValType = convertStellarTypeToScValType(entry[1].type);
137
- typeHints[keyString] = [
138
- Array.isArray(keyScValType) ? keyScValType[0] : keyScValType,
139
- Array.isArray(valueScValType) ? valueScValType[0] : valueScValType,
140
- ];
294
+ const valueTypeHint = Array.isArray(valueScValType)
295
+ ? valueScValType[0]
296
+ : valueScValType;
297
+ const valueScVal = nativeToScVal(processedValue, { type: valueTypeHint });
298
+
299
+ mapEntries.push(
300
+ new xdr.ScMapEntry({
301
+ key: keyScVal,
302
+ val: valueScVal,
303
+ })
304
+ );
141
305
  }
142
306
  );
143
307
 
144
- return nativeToScVal(convertedValue, { type: typeHints });
308
+ // Sort map entries by XDR-encoded keys (required by Soroban)
309
+ const sortedMapEntries = mapEntries.sort((a, b) => compareScValsByXdr(a.key(), b.key()));
310
+ return xdr.ScVal.scvMap(sortedMapEntries);
145
311
  }
146
312
 
147
313
  return nativeToScVal(value);
148
314
  }
149
315
 
316
+ case 'Tuple': {
317
+ if (!paramSchema?.components || paramSchema.components.length === 0) {
318
+ throw new Error(
319
+ `Tuple parameter "${paramSchema?.name ?? 'unknown'}" is missing component metadata`
320
+ );
321
+ }
322
+
323
+ const tupleComponents = paramSchema.components;
324
+ const tupleValues: xdr.ScVal[] = [];
325
+
326
+ tupleComponents.forEach((component, index) => {
327
+ const key = component.name ?? `item_${index}`;
328
+ let elementValue: unknown;
329
+
330
+ if (Array.isArray(value)) {
331
+ elementValue = value[index];
332
+ } else if (value && typeof value === 'object') {
333
+ elementValue = (value as Record<string, unknown>)[key];
334
+ }
335
+
336
+ if (typeof elementValue === 'undefined') {
337
+ const expectedTypes = tupleComponents.map((c) => c.type).join(', ');
338
+ throw new Error(
339
+ `Missing tuple value for "${key}" in parameter "${paramSchema.name ?? 'unknown'}". Expected ${tupleComponents.length} values of types [${expectedTypes}] but received: ${JSON.stringify(value)}`
340
+ );
341
+ }
342
+
343
+ if (typeof elementValue === 'string' && isLikelyEnumType(component.type)) {
344
+ elementValue = { tag: elementValue };
345
+ }
346
+
347
+ tupleValues.push(valueToScVal(elementValue, component.type, component, parseInnerValue));
348
+ });
349
+
350
+ return xdr.ScVal.scvVec(tupleValues);
351
+ }
352
+
150
353
  case 'Option': {
151
354
  // Handle Option<T> types
152
355
  const innerType = parameters[0];
@@ -155,7 +358,21 @@ export function valueToScVal(
155
358
  return nativeToScVal(null); // None variant
156
359
  } else {
157
360
  // Some variant - convert the inner value
158
- return valueToScVal(value, innerType);
361
+ let innerSchema: FunctionParameter | undefined;
362
+ if (enumMetadata) {
363
+ innerSchema = {
364
+ name: 'inner',
365
+ type: innerType,
366
+ ...({ enumMetadata } as unknown as Record<string, unknown>),
367
+ } as unknown as FunctionParameter;
368
+ } else if (paramSchema?.components) {
369
+ innerSchema = {
370
+ name: 'inner',
371
+ type: innerType,
372
+ components: paramSchema.components,
373
+ };
374
+ }
375
+ return valueToScVal(value, innerType, innerSchema, parseInnerValue);
159
376
  }
160
377
  }
161
378
 
@@ -1,13 +1,23 @@
1
1
  import { nativeToScVal, xdr } from '@stellar/stellar-sdk';
2
2
 
3
- import type { FunctionParameter } from '@openzeppelin/ui-builder-types';
3
+ import { isEnumValue, type FunctionParameter } from '@openzeppelin/ui-builder-types';
4
4
  import { isPlainObject, logger } from '@openzeppelin/ui-builder-utils';
5
5
 
6
6
  import { convertStellarTypeToScValType } from '../../utils/formatting';
7
+ import { isLikelyEnumType } from '../../utils/type-detection';
8
+ import { compareScValsByXdr } from '../../utils/xdr-ordering';
7
9
  import { parseGenericType } from './generic-parser';
8
10
 
9
11
  const SYSTEM_LOG_TAG = 'StructParser';
10
12
 
13
+ function isTupleStructSchema(schema: FunctionParameter | undefined): boolean {
14
+ if (!schema?.components || schema.components.length === 0) {
15
+ return false;
16
+ }
17
+
18
+ return schema.components.every((component, index) => component.name === index.toString());
19
+ }
20
+
11
21
  /**
12
22
  * Determines if a field value needs parsing through parseStellarInput.
13
23
  * Returns false for already-processed values (like Uint8Array for bytes).
@@ -59,6 +69,7 @@ function needsParsing(value: unknown, fieldType: string): boolean {
59
69
  * @param parameterType - The Stellar parameter type (for error messages)
60
70
  * @param paramSchema - Parameter schema with struct field definitions (required)
61
71
  * @param parseInnerValue - Function to recursively parse inner values
72
+ * @param convertToScVal - Optional function to recursively convert values to ScVal (for tuple structs)
62
73
  * @returns ScVal ready for contract calls
63
74
  * @throws Error if schema information is missing for any field
64
75
  */
@@ -66,10 +77,40 @@ export function convertStructToScVal(
66
77
  structObj: Record<string, unknown>,
67
78
  parameterType: string,
68
79
  paramSchema: FunctionParameter | undefined,
69
- parseInnerValue: (val: unknown, type: string) => unknown
80
+ parseInnerValue: (val: unknown, type: string) => unknown,
81
+ convertToScVal?: (
82
+ value: unknown,
83
+ type: string,
84
+ schema?: FunctionParameter,
85
+ parseValue?: (val: unknown, type: string) => unknown
86
+ ) => xdr.ScVal
70
87
  ): xdr.ScVal {
71
88
  // Laboratory-style struct conversion with proper type hint handling
72
89
 
90
+ // Check if this is a tuple struct (numeric field names like "0", "1", "2")
91
+ // Tuple structs in Soroban need to be serialized as vectors, not maps
92
+ if (isTupleStructSchema(paramSchema) && paramSchema && convertToScVal) {
93
+ const tupleValues = paramSchema.components!.map((component, index) => {
94
+ const key = component.name ?? index.toString();
95
+ let elementValue = structObj[key];
96
+
97
+ if (typeof elementValue === 'undefined') {
98
+ throw new Error(
99
+ `Missing tuple value for "${key}" in struct type "${parameterType}". Received: ${JSON.stringify(structObj)}`
100
+ );
101
+ }
102
+
103
+ // If the element is a string and its type is likely an enum, wrap it as { tag: value }
104
+ if (typeof elementValue === 'string' && isLikelyEnumType(component.type)) {
105
+ elementValue = { tag: elementValue };
106
+ }
107
+
108
+ return convertToScVal(elementValue, component.type, component, parseInnerValue);
109
+ });
110
+
111
+ return xdr.ScVal.scvVec(tupleValues);
112
+ }
113
+
73
114
  // Struct conversion using Laboratory pattern with schema-based type resolution
74
115
  // See: laboratory/src/helpers/sorobanUtils.ts convertObjectToScVal
75
116
  const convertedValue: Record<string, unknown> = {};
@@ -101,6 +142,10 @@ export function convertStructToScVal(
101
142
  parsedValue = fieldValue;
102
143
  }
103
144
 
145
+ if (typeof parsedValue === 'string' && isLikelyEnumType(fieldType)) {
146
+ parsedValue = { tag: parsedValue };
147
+ }
148
+
104
149
  // Handle Map fields specially - convert from SDK format to plain object with type hints
105
150
  if (fieldType.startsWith('Map<') && Array.isArray(parsedValue)) {
106
151
  // Extract key and value types
@@ -149,12 +194,25 @@ export function convertStructToScVal(
149
194
  // Provide nested type hints for Map field (Laboratory pattern)
150
195
  typeHints[fieldName] = ['symbol', mapTypeHints];
151
196
  } else {
152
- convertedValue[fieldName] = parsedValue;
197
+ // Check if this is a Vec or other generic type that needs special handling
198
+ if (convertToScVal && (fieldType.startsWith('Vec<') || isEnumValue(parsedValue))) {
199
+ const fieldSchema = paramSchema?.components?.find((c) => c.name === fieldName);
200
+ // Use valueToScVal for Vec and enum fields to ensure proper conversion
201
+ convertedValue[fieldName] = convertToScVal(
202
+ parsedValue,
203
+ fieldType,
204
+ fieldSchema,
205
+ parseInnerValue
206
+ );
207
+ typeHints[fieldName] = ['symbol', 'scval'];
208
+ } else {
209
+ convertedValue[fieldName] = parsedValue;
153
210
 
154
- // Use exact type from schema for non-Map fields
155
- const scValType = convertStellarTypeToScValType(fieldType);
156
- if (scValType !== 'map-special') {
157
- typeHints[fieldName] = ['symbol', Array.isArray(scValType) ? scValType[0] : scValType];
211
+ // Use exact type from schema for non-Map fields
212
+ const scValType = convertStellarTypeToScValType(fieldType);
213
+ if (scValType !== 'map-special') {
214
+ typeHints[fieldName] = ['symbol', Array.isArray(scValType) ? scValType[0] : scValType];
215
+ }
158
216
  }
159
217
  }
160
218
  } else {
@@ -172,7 +230,55 @@ export function convertStructToScVal(
172
230
  typeHints,
173
231
  });
174
232
 
175
- const scVal = nativeToScVal(convertedValue, { type: typeHints });
233
+ // Check if any fields are enums that need special conversion
234
+ const hasEnumFields = paramSchema?.components?.some((comp) => {
235
+ const fieldValue = convertedValue[comp.name];
236
+ return isEnumValue(fieldValue);
237
+ });
238
+
239
+ let scVal: xdr.ScVal;
240
+
241
+ if (hasEnumFields && convertToScVal && paramSchema?.components) {
242
+ // Manually build the ScMap with proper enum conversion
243
+ const mapEntries: xdr.ScMapEntry[] = [];
244
+
245
+ for (const fieldSchema of paramSchema.components) {
246
+ const fieldName = fieldSchema.name;
247
+ const fieldValue = convertedValue[fieldName];
248
+
249
+ // Create key
250
+ const keyScVal = nativeToScVal(fieldName, { type: 'symbol' });
251
+
252
+ // Create value - check if it's an enum
253
+ let valueScVal: xdr.ScVal;
254
+ if (isEnumValue(fieldValue)) {
255
+ valueScVal = convertToScVal(fieldValue, fieldSchema.type, fieldSchema, parseInnerValue);
256
+ } else {
257
+ // Use nativeToScVal for non-enum fields
258
+ const fieldTypeHint = typeHints[fieldName];
259
+ if (fieldTypeHint && Array.isArray(fieldTypeHint) && fieldTypeHint.length > 1) {
260
+ valueScVal = nativeToScVal(fieldValue, { type: fieldTypeHint[1] });
261
+ } else {
262
+ valueScVal = nativeToScVal(fieldValue);
263
+ }
264
+ }
265
+
266
+ mapEntries.push(
267
+ new xdr.ScMapEntry({
268
+ key: keyScVal,
269
+ val: valueScVal,
270
+ })
271
+ );
272
+ }
273
+
274
+ // Sort map entries by XDR-encoded keys (required by Soroban)
275
+ const sortedMapEntries = mapEntries.sort((a, b) => compareScValsByXdr(a.key(), b.key()));
276
+
277
+ scVal = xdr.ScVal.scvMap(sortedMapEntries);
278
+ } else {
279
+ scVal = nativeToScVal(convertedValue, { type: typeHints });
280
+ }
281
+
176
282
  logger.debug(SYSTEM_LOG_TAG, 'convertStructToScVal generated ScVal:', {
177
283
  parameterType,
178
284
  scValType: scVal.switch().name,
@@ -10,6 +10,7 @@ import type {
10
10
  SorobanMapEntry,
11
11
  } from '../transform/input-parser';
12
12
  import { convertStellarTypeToScValType } from './formatting';
13
+ import { compareScValsByXdr } from './xdr-ordering';
13
14
 
14
15
  const SYSTEM_LOG_TAG = 'StellarInputParsingUtils';
15
16
 
@@ -260,8 +261,14 @@ export function convertObjectToMap(mapArray: SorobanMapEntry[]): {
260
261
  mapType: Record<string, string | string[]>;
261
262
  } {
262
263
  try {
263
- const mapVal = mapArray.reduce((acc: Record<string, unknown>, pair) => {
264
- const key = pair['0'].value as string;
264
+ const sortedEntries = [...mapArray].sort((a, b) => {
265
+ const aKey = getScValFromPrimitive(a['0'] as SorobanArgumentValue);
266
+ const bKey = getScValFromPrimitive(b['0'] as SorobanArgumentValue);
267
+ return compareScValsByXdr(aKey, bKey);
268
+ });
269
+
270
+ const mapVal = sortedEntries.reduce((acc: Record<string, unknown>, pair) => {
271
+ const key = String(pair['0'].value);
265
272
 
266
273
  if (Array.isArray(pair['1'])) {
267
274
  // Handle nested array values
@@ -270,13 +277,23 @@ export function convertObjectToMap(mapArray: SorobanMapEntry[]): {
270
277
  } else {
271
278
  // Handle primitive values
272
279
  const value = pair['1'].value;
273
- acc[key] = pair['1'].type === 'bool' ? value === 'true' : value;
280
+ if (pair['1'].type === 'bool') {
281
+ if (typeof value === 'boolean') {
282
+ acc[key] = value;
283
+ } else if (typeof value === 'string') {
284
+ acc[key] = value === 'true';
285
+ } else {
286
+ acc[key] = Boolean(value);
287
+ }
288
+ } else {
289
+ acc[key] = value;
290
+ }
274
291
  }
275
292
  return acc;
276
293
  }, {});
277
294
 
278
- const mapType = mapArray.reduce((acc: Record<string, string[]>, pair) => {
279
- const key = pair['0'].value as string;
295
+ const mapType = sortedEntries.reduce((acc: Record<string, string[]>, pair) => {
296
+ const key = String(pair['0'].value);
280
297
  const keyTypeHint = convertStellarTypeToScValType(pair['0'].type);
281
298
  const valueTypeHint = convertStellarTypeToScValType(pair['1'].type);
282
299
  acc[key] = [
@@ -107,6 +107,30 @@ export function extractOptionElementType(parameterType: string): ExtractionResul
107
107
  return extractGenericInnerType(parameterType, 'Option');
108
108
  }
109
109
 
110
+ /**
111
+ * Safely extracts the element types from a Stellar Tuple type.
112
+ *
113
+ * @param parameterType - The parameter type (e.g., 'Tuple<U32, Bool>')
114
+ * @returns Array of element types or null if not a Tuple type
115
+ */
116
+ export function extractTupleTypes(parameterType: string): ExtractionResult<string[]> {
117
+ if (!isValidTypeString(parameterType) || !parameterType.startsWith('Tuple<')) {
118
+ return null;
119
+ }
120
+
121
+ const innerContent = extractGenericInnerType(parameterType, 'Tuple');
122
+ if (!innerContent) {
123
+ return null;
124
+ }
125
+
126
+ const parts = splitTopLevelTypes(innerContent);
127
+ if (parts.length === 0) {
128
+ return null;
129
+ }
130
+
131
+ return parts;
132
+ }
133
+
110
134
  /**
111
135
  * Generic function to extract inner content from generic types.
112
136
  *
@@ -246,3 +270,34 @@ function hasInvalidCharacters(str: string): boolean {
246
270
  // Note: Parentheses are NOT allowed in Stellar type strings
247
271
  return !/^[A-Za-z0-9<>,\s_]+$/.test(str);
248
272
  }
273
+
274
+ /**
275
+ * Splits a comma-separated list of types while respecting nested generics.
276
+ */
277
+ function splitTopLevelTypes(content: string): string[] {
278
+ const types: string[] = [];
279
+ let start = 0;
280
+ let level = 0;
281
+
282
+ for (let i = 0; i < content.length; i += 1) {
283
+ const char = content[i];
284
+ if (char === '<') {
285
+ level += 1;
286
+ } else if (char === '>') {
287
+ level -= 1;
288
+ } else if (char === ',' && level === 0) {
289
+ const segment = content.slice(start, i).trim();
290
+ if (segment) {
291
+ types.push(segment);
292
+ }
293
+ start = i + 1;
294
+ }
295
+ }
296
+
297
+ const lastSegment = content.slice(start).trim();
298
+ if (lastSegment) {
299
+ types.push(lastSegment);
300
+ }
301
+
302
+ return types;
303
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Set of primitive Stellar/Soroban parameter types.
3
+ * These types do not have nested components and can be directly serialized.
4
+ *
5
+ * Used to distinguish primitive types from complex types (structs, enums, tuples, maps, vecs)
6
+ * when processing parameters for serialization and validation.
7
+ */
8
+ export const PRIMITIVE_STELLAR_TYPES = new Set([
9
+ 'Bool',
10
+ 'ScString',
11
+ 'ScSymbol',
12
+ 'Address',
13
+ 'Bytes',
14
+ 'U8',
15
+ 'U16',
16
+ 'U32',
17
+ 'U64',
18
+ 'U128',
19
+ 'U256',
20
+ 'I8',
21
+ 'I16',
22
+ 'I32',
23
+ 'I64',
24
+ 'I128',
25
+ 'I256',
26
+ ]);
27
+
28
+ /**
29
+ * Check if a Stellar type is a primitive type.
30
+ * @param type - The Stellar type to check
31
+ * @returns True if the type is primitive, false otherwise
32
+ */
33
+ export function isPrimitiveParamType(type: string): boolean {
34
+ return PRIMITIVE_STELLAR_TYPES.has(type);
35
+ }