@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.
- package/README.md +87 -0
- package/dist/index.cjs +6148 -3052
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +6005 -2898
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/src/access-control/actions.ts +214 -0
- package/src/access-control/feature-detection.ts +226 -0
- package/src/access-control/index.ts +54 -0
- package/src/access-control/indexer-client.ts +1297 -0
- package/src/access-control/onchain-reader.ts +450 -0
- package/src/access-control/service.ts +1347 -0
- package/src/access-control/validation.ts +256 -0
- package/src/adapter.ts +21 -0
- package/src/configuration/network-services.ts +130 -9
- package/src/mapping/enum-metadata.ts +75 -2
- package/src/mapping/field-generator.ts +103 -14
- package/src/mapping/struct-fields.ts +12 -2
- package/src/mapping/tuple-components.ts +43 -0
- package/src/networks/mainnet.ts +1 -0
- package/src/networks/testnet.ts +1 -0
- package/src/query/handler.ts +6 -4
- package/src/sac/xdr.ts +1 -2
- package/src/transaction/eoa.ts +8 -1
- package/src/transaction/formatter.ts +138 -10
- package/src/transaction/relayer.ts +8 -1
- package/src/transform/parsers/index.ts +7 -0
- package/src/transform/parsers/scval-converter.ts +238 -21
- package/src/transform/parsers/struct-parser.ts +114 -8
- package/src/utils/input-parsing.ts +22 -5
- package/src/utils/safe-type-parser.ts +55 -0
- package/src/utils/stellar-types.ts +35 -0
- package/src/utils/xdr-ordering.ts +36 -0
|
@@ -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 {
|
|
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 (
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
|
264
|
-
const
|
|
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
|
-
|
|
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 =
|
|
279
|
-
const key = pair['0'].value
|
|
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
|
+
}
|