@openzeppelin/adapter-stellar 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.
Files changed (165) hide show
  1. package/README.md +272 -0
  2. package/dist/config.cjs +21 -0
  3. package/dist/config.cjs.map +1 -0
  4. package/dist/config.d.cts +8 -0
  5. package/dist/config.d.cts.map +1 -0
  6. package/dist/config.d.mts +8 -0
  7. package/dist/config.d.mts.map +1 -0
  8. package/dist/config.mjs +20 -0
  9. package/dist/config.mjs.map +1 -0
  10. package/dist/index.cjs +7564 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.d.cts +261 -0
  13. package/dist/index.d.cts.map +1 -0
  14. package/dist/index.d.mts +263 -0
  15. package/dist/index.d.mts.map +1 -0
  16. package/dist/index.mjs +7529 -0
  17. package/dist/index.mjs.map +1 -0
  18. package/dist/metadata.cjs +22 -0
  19. package/dist/metadata.cjs.map +1 -0
  20. package/dist/metadata.d.cts +7 -0
  21. package/dist/metadata.d.cts.map +1 -0
  22. package/dist/metadata.d.mts +7 -0
  23. package/dist/metadata.d.mts.map +1 -0
  24. package/dist/metadata.mjs +21 -0
  25. package/dist/metadata.mjs.map +1 -0
  26. package/dist/networks-BrV516-R.d.cts +15 -0
  27. package/dist/networks-BrV516-R.d.cts.map +1 -0
  28. package/dist/networks-C0MmhJcu.d.mts +15 -0
  29. package/dist/networks-C0MmhJcu.d.mts.map +1 -0
  30. package/dist/networks-DgUFSTiC.cjs +76 -0
  31. package/dist/networks-DgUFSTiC.cjs.map +1 -0
  32. package/dist/networks-QbEPbaGT.mjs +46 -0
  33. package/dist/networks-QbEPbaGT.mjs.map +1 -0
  34. package/dist/networks.cjs +8 -0
  35. package/dist/networks.d.cts +2 -0
  36. package/dist/networks.d.mts +2 -0
  37. package/dist/networks.mjs +3 -0
  38. package/dist/vite-config.cjs +43 -0
  39. package/dist/vite-config.cjs.map +1 -0
  40. package/dist/vite-config.d.cts +35 -0
  41. package/dist/vite-config.d.cts.map +1 -0
  42. package/dist/vite-config.d.mts +35 -0
  43. package/dist/vite-config.d.mts.map +1 -0
  44. package/dist/vite-config.mjs +42 -0
  45. package/dist/vite-config.mjs.map +1 -0
  46. package/package.json +114 -0
  47. package/src/__tests__/getDefaultServiceConfig.test.ts +105 -0
  48. package/src/access-control/actions.ts +214 -0
  49. package/src/access-control/feature-detection.ts +238 -0
  50. package/src/access-control/index.ts +54 -0
  51. package/src/access-control/indexer-client.ts +1474 -0
  52. package/src/access-control/onchain-reader.ts +446 -0
  53. package/src/access-control/service.ts +1431 -0
  54. package/src/access-control/validation.ts +256 -0
  55. package/src/adapter.ts +659 -0
  56. package/src/config.ts +43 -0
  57. package/src/configuration/__tests__/explorer.test.ts +80 -0
  58. package/src/configuration/__tests__/rpc.test.ts +355 -0
  59. package/src/configuration/execution.ts +83 -0
  60. package/src/configuration/explorer.ts +105 -0
  61. package/src/configuration/index.ts +5 -0
  62. package/src/configuration/network-services.ts +210 -0
  63. package/src/configuration/rpc.ts +270 -0
  64. package/src/configuration.ts +2 -0
  65. package/src/contract/__tests__/complete-type-coverage.test.ts +78 -0
  66. package/src/contract/index.ts +3 -0
  67. package/src/contract/loader.ts +498 -0
  68. package/src/contract/transformer.ts +1 -0
  69. package/src/contract/type.ts +65 -0
  70. package/src/index.ts +23 -0
  71. package/src/mapping/constants.ts +89 -0
  72. package/src/mapping/enum-metadata.ts +237 -0
  73. package/src/mapping/field-generator.ts +296 -0
  74. package/src/mapping/index.ts +5 -0
  75. package/src/mapping/struct-fields.ts +106 -0
  76. package/src/mapping/tuple-components.ts +43 -0
  77. package/src/mapping/type-coverage-validator.ts +151 -0
  78. package/src/mapping/type-mapper.ts +203 -0
  79. package/src/metadata.ts +16 -0
  80. package/src/networks/README.md +84 -0
  81. package/src/networks/index.ts +19 -0
  82. package/src/networks/mainnet.ts +20 -0
  83. package/src/networks/testnet.ts +20 -0
  84. package/src/networks.ts +2 -0
  85. package/src/query/handler.ts +411 -0
  86. package/src/query/index.ts +4 -0
  87. package/src/query/view-checker.ts +32 -0
  88. package/src/sac/spec-cache.ts +68 -0
  89. package/src/sac/spec-source.ts +35 -0
  90. package/src/sac/xdr.ts +101 -0
  91. package/src/transaction/components/AdvancedInfo.tsx +34 -0
  92. package/src/transaction/components/FeeConfiguration.tsx +41 -0
  93. package/src/transaction/components/StellarRelayerOptions.tsx +60 -0
  94. package/src/transaction/components/TransactionTiming.tsx +77 -0
  95. package/src/transaction/components/index.ts +5 -0
  96. package/src/transaction/components/useStellarRelayerOptions.ts +114 -0
  97. package/src/transaction/eoa.ts +229 -0
  98. package/src/transaction/execution-strategy.ts +33 -0
  99. package/src/transaction/formatter.ts +296 -0
  100. package/src/transaction/index.ts +4 -0
  101. package/src/transaction/relayer.ts +575 -0
  102. package/src/transaction/sender.ts +156 -0
  103. package/src/transform/index.ts +4 -0
  104. package/src/transform/input-parser.ts +9 -0
  105. package/src/transform/output-formatter.ts +133 -0
  106. package/src/transform/parsers/complex-parser.ts +157 -0
  107. package/src/transform/parsers/generic-parser.ts +171 -0
  108. package/src/transform/parsers/index.ts +86 -0
  109. package/src/transform/parsers/primitive-parser.ts +123 -0
  110. package/src/transform/parsers/scval-converter.ts +405 -0
  111. package/src/transform/parsers/struct-parser.ts +324 -0
  112. package/src/transform/parsers/types.ts +35 -0
  113. package/src/types/__tests__/artifacts.test.ts +89 -0
  114. package/src/types/artifacts.ts +19 -0
  115. package/src/utils/__tests__/artifacts.test.ts +77 -0
  116. package/src/utils/artifacts.ts +30 -0
  117. package/src/utils/formatting.ts +122 -0
  118. package/src/utils/index.ts +6 -0
  119. package/src/utils/input-parsing.ts +336 -0
  120. package/src/utils/safe-type-parser.ts +303 -0
  121. package/src/utils/stellar-types.ts +35 -0
  122. package/src/utils/type-detection.ts +163 -0
  123. package/src/utils/xdr-ordering.ts +36 -0
  124. package/src/validation/__tests__/address.test.ts +267 -0
  125. package/src/validation/address.ts +136 -0
  126. package/src/validation/eoa.ts +33 -0
  127. package/src/validation/index.ts +3 -0
  128. package/src/validation/relayer.ts +13 -0
  129. package/src/vite-config.ts +67 -0
  130. package/src/wallet/README.md +93 -0
  131. package/src/wallet/__tests__/connection.test.ts +72 -0
  132. package/src/wallet/components/StellarWalletUiRoot.tsx +161 -0
  133. package/src/wallet/components/account/AccountDisplay.tsx +50 -0
  134. package/src/wallet/components/connect/ConnectButton.tsx +100 -0
  135. package/src/wallet/components/connect/ConnectorDialog.tsx +125 -0
  136. package/src/wallet/components/index.ts +3 -0
  137. package/src/wallet/connection.ts +151 -0
  138. package/src/wallet/context/StellarWalletContext.ts +32 -0
  139. package/src/wallet/context/index.ts +4 -0
  140. package/src/wallet/context/useStellarWalletContext.ts +17 -0
  141. package/src/wallet/hooks/facade-hooks.ts +31 -0
  142. package/src/wallet/hooks/index.ts +7 -0
  143. package/src/wallet/hooks/useStellarAccount.ts +27 -0
  144. package/src/wallet/hooks/useStellarConnect.ts +60 -0
  145. package/src/wallet/hooks/useStellarDisconnect.ts +47 -0
  146. package/src/wallet/hooks/useUiKitConfig.ts +40 -0
  147. package/src/wallet/implementation/wallets-kit-implementation.ts +379 -0
  148. package/src/wallet/index.ts +11 -0
  149. package/src/wallet/services/__tests__/configResolutionService.test.ts +163 -0
  150. package/src/wallet/services/configResolutionService.ts +65 -0
  151. package/src/wallet/stellar-wallets-kit/StellarWalletsKitConnectButton.tsx +82 -0
  152. package/src/wallet/stellar-wallets-kit/__mocks__/@creit.tech/stellar-wallets-kit.ts +48 -0
  153. package/src/wallet/stellar-wallets-kit/__tests__/export-service.test.ts +93 -0
  154. package/src/wallet/stellar-wallets-kit/__tests__/stellarUiKitManager.test.ts +0 -0
  155. package/src/wallet/stellar-wallets-kit/config-generator.ts +75 -0
  156. package/src/wallet/stellar-wallets-kit/export-service.ts +19 -0
  157. package/src/wallet/stellar-wallets-kit/index.ts +3 -0
  158. package/src/wallet/stellar-wallets-kit/stellarUiKitManager.ts +235 -0
  159. package/src/wallet/types.ts +19 -0
  160. package/src/wallet/utils/__tests__/filterWalletComponents.test.ts +150 -0
  161. package/src/wallet/utils/__tests__/uiKitService.test.ts +189 -0
  162. package/src/wallet/utils/filterWalletComponents.ts +89 -0
  163. package/src/wallet/utils/index.ts +3 -0
  164. package/src/wallet/utils/stellarWalletImplementationManager.ts +118 -0
  165. package/src/wallet/utils/uiKitService.ts +74 -0
@@ -0,0 +1,237 @@
1
+ import { xdr } from '@stellar/stellar-sdk';
2
+
3
+ import type { FunctionParameter } from '@openzeppelin/ui-types';
4
+ import { logger } from '@openzeppelin/ui-utils';
5
+
6
+ // Import the type extraction utility from the shared utils module
7
+ import { extractSorobanTypeFromScSpec } from '../utils/type-detection';
8
+ import { extractStructFields, isStructType } from './struct-fields';
9
+ import { buildTupleComponents } from './tuple-components';
10
+
11
+ /**
12
+ * Represents a single enum variant with its type and optional payload information
13
+ */
14
+ export interface EnumVariant {
15
+ /** Name of the variant (e.g., 'One', 'Two', 'Three') */
16
+ name: string;
17
+ /** Type of variant: 'void' for unit variants, 'tuple' for variants with payload, 'integer' for numeric enums */
18
+ type: 'void' | 'tuple' | 'integer';
19
+ /** For tuple variants: array of payload type names (e.g., ['U32', 'ScString']) */
20
+ payloadTypes?: string[];
21
+ /** Optional detailed component metadata for payload types */
22
+ payloadComponents?: (FunctionParameter[] | undefined)[];
23
+ /** For integer variants: the numeric value */
24
+ value?: number;
25
+ /** Flag indicating if this variant has a single Tuple payload that needs wrapping during serialization */
26
+ isSingleTuplePayload?: boolean;
27
+ }
28
+
29
+ /**
30
+ * Metadata about an enum extracted from Stellar contract spec
31
+ */
32
+ export interface EnumMetadata {
33
+ /** Name of the enum type */
34
+ name: string;
35
+ /** Array of variants in the enum */
36
+ variants: EnumVariant[];
37
+ /** True if all variants are unit variants (no payloads), suitable for simple select/radio */
38
+ isUnitOnly: boolean;
39
+ }
40
+
41
+ /**
42
+ * Helper function to flatten a single payload type.
43
+ * Handles tuples, structs, and primitive types differently.
44
+ *
45
+ * @param payloadType - The type to flatten
46
+ * @param entries - Spec entries for struct/enum resolution
47
+ * @param flattenedTypes - Array to accumulate flattened type names
48
+ * @param flattenedComponents - Array to accumulate component metadata
49
+ */
50
+ function flattenPayloadType(
51
+ payloadType: string,
52
+ entries: xdr.ScSpecEntry[],
53
+ flattenedTypes: string[],
54
+ flattenedComponents: (FunctionParameter[] | undefined)[]
55
+ ): void {
56
+ if (payloadType.startsWith('Tuple<')) {
57
+ // Extract tuple components and add them individually for UI rendering
58
+ const tupleComponents = buildTupleComponents(payloadType, entries);
59
+ if (tupleComponents && tupleComponents.length > 0) {
60
+ tupleComponents.forEach((component) => {
61
+ flattenedTypes.push(component.type);
62
+ if (isStructType(entries, component.type)) {
63
+ flattenedComponents.push(extractStructFields(entries, component.type) ?? undefined);
64
+ } else {
65
+ flattenedComponents.push(component.components);
66
+ }
67
+ });
68
+ } else {
69
+ flattenedTypes.push(payloadType);
70
+ flattenedComponents.push(undefined);
71
+ }
72
+ } else if (isStructType(entries, payloadType)) {
73
+ flattenedTypes.push(payloadType);
74
+ flattenedComponents.push(extractStructFields(entries, payloadType) ?? undefined);
75
+ } else {
76
+ flattenedTypes.push(payloadType);
77
+ flattenedComponents.push(undefined);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Extracts enum variant metadata from Stellar contract spec entries
83
+ *
84
+ * @param entries Array of ScSpecEntry from contract spec
85
+ * @param enumName Name of the enum type to extract variants for
86
+ * @returns EnumMetadata if found, null if not found or not an enum
87
+ */
88
+ export function extractEnumVariants(
89
+ entries: xdr.ScSpecEntry[],
90
+ enumName: string
91
+ ): EnumMetadata | null {
92
+ try {
93
+ // Find the entry for the requested enum name
94
+ const entry = entries.find((e) => {
95
+ try {
96
+ return e.value().name().toString() === enumName;
97
+ } catch {
98
+ return false;
99
+ }
100
+ });
101
+
102
+ if (!entry) {
103
+ return null;
104
+ }
105
+
106
+ const entryKind = entry.switch();
107
+
108
+ // Handle UDT Union (tagged union enum like DemoEnum { One, Two(u32), Three(String) })
109
+ if (entryKind.value === xdr.ScSpecEntryKind.scSpecEntryUdtUnionV0().value) {
110
+ const unionUdt = entry.udtUnionV0();
111
+ const cases = unionUdt.cases();
112
+ const variants: EnumVariant[] = [];
113
+ let isUnitOnly = true;
114
+
115
+ for (const caseEntry of cases) {
116
+ const caseKind = caseEntry.switch();
117
+
118
+ if (caseKind.value === xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0().value) {
119
+ // Void case (unit variant)
120
+ const voidCase = caseEntry.voidCase();
121
+ variants.push({
122
+ name: voidCase.name().toString(),
123
+ type: 'void',
124
+ });
125
+ } else if (
126
+ caseKind.value === xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0().value
127
+ ) {
128
+ // Tuple case (variant with payload)
129
+ const tupleCase = caseEntry.tupleCase();
130
+ const rawPayloadTypes = tupleCase
131
+ .type()
132
+ .map((typeDef) => extractSorobanTypeFromScSpec(typeDef));
133
+
134
+ // Track if we have a single Tuple payload that needs special handling
135
+ const isSingleTuplePayload =
136
+ rawPayloadTypes.length === 1 && rawPayloadTypes[0].startsWith('Tuple<');
137
+
138
+ // Flatten tuple payloads for UI rendering
139
+ // Example: Some((Address, i128)) → payloadTypes: ['Address', 'I128'] for UI
140
+ // But we keep the original structure info for serialization
141
+ const flattenedPayloadTypes: string[] = [];
142
+ const flattenedPayloadComponents: (FunctionParameter[] | undefined)[] = [];
143
+
144
+ for (const payloadType of rawPayloadTypes) {
145
+ flattenPayloadType(
146
+ payloadType,
147
+ entries,
148
+ flattenedPayloadTypes,
149
+ flattenedPayloadComponents
150
+ );
151
+ }
152
+
153
+ variants.push({
154
+ name: tupleCase.name().toString(),
155
+ type: 'tuple',
156
+ payloadTypes: flattenedPayloadTypes,
157
+ ...(flattenedPayloadComponents.some(
158
+ (components) => components && components.length > 0
159
+ ) && {
160
+ payloadComponents: flattenedPayloadComponents,
161
+ }),
162
+ // Store metadata about whether this needs tuple wrapping during serialization
163
+ ...(isSingleTuplePayload && { isSingleTuplePayload: true }),
164
+ });
165
+ isUnitOnly = false;
166
+ }
167
+ }
168
+
169
+ return {
170
+ name: enumName,
171
+ variants,
172
+ isUnitOnly,
173
+ };
174
+ }
175
+
176
+ // Handle UDT Enum (integer enum like Priority { Low = 0, Medium = 1, High = 2 })
177
+ if (entryKind.value === xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0().value) {
178
+ const enumUdt = entry.udtEnumV0();
179
+ const cases = enumUdt.cases();
180
+ const variants: EnumVariant[] = [];
181
+
182
+ for (const caseEntry of cases) {
183
+ variants.push({
184
+ name: caseEntry.name().toString(),
185
+ type: 'integer',
186
+ value: caseEntry.value(),
187
+ });
188
+ }
189
+
190
+ return {
191
+ name: enumName,
192
+ variants,
193
+ isUnitOnly: true, // Integer enums are considered unit-only for UI purposes
194
+ };
195
+ }
196
+
197
+ return null;
198
+ } catch (error) {
199
+ logger.error('extractEnumVariants', `Failed to extract enum variants for ${enumName}:`, error);
200
+ return null;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Checks if a parameter type name refers to a user-defined enum/union type
206
+ * by looking for it in the contract spec entries
207
+ *
208
+ * @param entries Array of ScSpecEntry from contract spec
209
+ * @param typeName Name of the type to check
210
+ * @returns true if the type is a UDT enum or union
211
+ */
212
+ export function isEnumType(entries: xdr.ScSpecEntry[], typeName: string): boolean {
213
+ try {
214
+ const entry = entries.find((e) => {
215
+ try {
216
+ const entryName = e.value().name().toString();
217
+ return entryName === typeName;
218
+ } catch {
219
+ return false;
220
+ }
221
+ });
222
+
223
+ if (!entry) {
224
+ return false;
225
+ }
226
+
227
+ const entryKind = entry.switch();
228
+ const isEnum =
229
+ entryKind.value === xdr.ScSpecEntryKind.scSpecEntryUdtUnionV0().value ||
230
+ entryKind.value === xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0().value;
231
+
232
+ return isEnum;
233
+ } catch (error) {
234
+ logger.error('isEnumType', `Failed to check if ${typeName} is enum:`, error);
235
+ return false;
236
+ }
237
+ }
@@ -0,0 +1,296 @@
1
+ import { xdr } from '@stellar/stellar-sdk';
2
+ import { startCase } from 'lodash';
3
+
4
+ import type {
5
+ ContractSchema,
6
+ FieldType,
7
+ FieldValidation,
8
+ FieldValue,
9
+ FormFieldType,
10
+ FunctionParameter,
11
+ } from '@openzeppelin/ui-types';
12
+ import {
13
+ enhanceNumericValidation,
14
+ getDefaultValueForType,
15
+ logger,
16
+ type NumericBoundsMap,
17
+ } from '@openzeppelin/ui-utils';
18
+
19
+ import { extractMapTypes, extractVecElementType } from '../utils/safe-type-parser';
20
+ import { isBytesNType, isLikelyEnumType } from '../utils/type-detection';
21
+ import { extractEnumVariants, isEnumType, type EnumMetadata } from './enum-metadata';
22
+ import { extractStructFields, isStructType } from './struct-fields';
23
+ import { buildTupleComponents } from './tuple-components';
24
+ import { mapStellarParameterTypeToFieldType } from './type-mapper';
25
+
26
+ /**
27
+ * Get default validation rules for a parameter type.
28
+ * Only includes serializable validation rules - no custom functions.
29
+ */
30
+ function getDefaultValidationForType(): FieldValidation {
31
+ return { required: true };
32
+ }
33
+
34
+ /**
35
+ * Stellar/Soroban numeric type bounds.
36
+ * Maps Stellar type names to their min/max value constraints.
37
+ *
38
+ * Note: U64, U128, U256, I64, I128, and I256 are not included here because they exceed
39
+ * JavaScript's Number.MAX_SAFE_INTEGER (2^53 - 1). These types are mapped to 'bigint' field type
40
+ * and handle validation through BigIntField component's internal validation mechanism.
41
+ */
42
+ const STELLAR_NUMERIC_BOUNDS: NumericBoundsMap = {
43
+ U8: { min: 0, max: 255 },
44
+ U16: { min: 0, max: 65_535 },
45
+ U32: { min: 0, max: 4_294_967_295 },
46
+ I8: { min: -128, max: 127 },
47
+ I16: { min: -32_768, max: 32_767 },
48
+ I32: { min: -2_147_483_648, max: 2_147_483_647 },
49
+ };
50
+
51
+ /**
52
+ * Generate default form configuration for a Stellar function parameter. Supports:
53
+ * - WASM artifacts: parameters arrive with baked-in enum metadata and struct components.
54
+ * - SAC runtime specs: metadata is fetched on demand, so we reconstruct the same shape from
55
+ * `metadata.specEntries` to keep the UI identical across contract types.
56
+ */
57
+ export function generateStellarDefaultField<T extends FieldType = FieldType>(
58
+ parameter: FunctionParameter,
59
+ contractSchema?: ContractSchema
60
+ ): FormFieldType<T> {
61
+ // Extract spec entries from contract schema metadata if available
62
+ const specEntries = contractSchema?.metadata?.specEntries as xdr.ScSpecEntry[] | undefined;
63
+ const fieldType = mapStellarParameterTypeToFieldType(parameter.type) as T;
64
+
65
+ // Debug logging for unmapped types
66
+ if (parameter.type === 'unknown') {
67
+ logger.warn(
68
+ 'adapter-stellar',
69
+ `[generateStellarDefaultField] Parameter "${parameter.name}" has type "unknown"`
70
+ );
71
+ }
72
+
73
+ let enumMetadata: EnumMetadata | null = null;
74
+ // WASM artifacts still include `components` on struct inputs because we bake them during
75
+ // the build step. SAC contracts fetch their spec at runtime, so struct parameters show up
76
+ // without that data. Cache whatever the SAC spec gives us so the final field mirrors the
77
+ // WASM experience.
78
+ let structComponents: FunctionParameter[] | null = null;
79
+ let finalFieldType = fieldType;
80
+ let options: { label: string; value: string }[] | undefined;
81
+
82
+ // Check if this parameter is an enum type (by spec or heuristic) and extract metadata
83
+ const enumDetectedBySpec = !!(specEntries && isEnumType(specEntries, parameter.type));
84
+ const enumDetectedHeuristic = isLikelyEnumType(parameter.type);
85
+ if (enumDetectedBySpec || enumDetectedHeuristic) {
86
+ if (enumDetectedBySpec) {
87
+ // We have spec entries, extract full metadata
88
+ enumMetadata = extractEnumVariants(specEntries!, parameter.type);
89
+ if (enumMetadata) {
90
+ if (enumMetadata.isUnitOnly) {
91
+ // Unit-only enums can use select/radio with options
92
+ finalFieldType = 'select' as T;
93
+ options = enumMetadata.variants.map((variant) => ({
94
+ label: variant.name,
95
+ value: variant.type === 'integer' ? variant.value!.toString() : variant.name,
96
+ }));
97
+ } else {
98
+ // Tagged enums with payloads use the composite enum field
99
+ finalFieldType = 'enum' as T;
100
+ }
101
+ }
102
+ } else {
103
+ // No spec entries available, but type looks like enum - use enum field as fallback
104
+ finalFieldType = 'enum' as T;
105
+ // Create minimal enum metadata for fallback
106
+ enumMetadata = {
107
+ name: parameter.type,
108
+ variants: [], // Empty variants will trigger fallback UI
109
+ isUnitOnly: false,
110
+ };
111
+ }
112
+ }
113
+
114
+ // Same story here: WASM schemas already embed struct components, but SAC schemas only
115
+ // expose them inside the downloaded spec. Pull them out up front so the UI sees the
116
+ // same shape regardless of how the metadata was sourced.
117
+ if (specEntries && isStructType(specEntries, parameter.type)) {
118
+ const structFields = extractStructFields(specEntries, parameter.type);
119
+ if (structFields && structFields.length > 0) {
120
+ structComponents = structFields;
121
+ }
122
+ }
123
+
124
+ if (!structComponents) {
125
+ const tupleComponents = buildTupleComponents(parameter.type, specEntries);
126
+ if (tupleComponents && tupleComponents.length > 0) {
127
+ structComponents = tupleComponents;
128
+ }
129
+ }
130
+
131
+ const baseField: FormFieldType<T> = {
132
+ id: `field-${Math.random().toString(36).substring(2, 9)}`,
133
+ name: parameter.name || parameter.type, // Use type if name missing
134
+ label: startCase(parameter.displayName || parameter.name || parameter.type),
135
+ type: finalFieldType,
136
+ placeholder: enumMetadata
137
+ ? `Select ${parameter.displayName || parameter.name || parameter.type}`
138
+ : `Enter ${parameter.displayName || parameter.name || parameter.type}`,
139
+ helperText: parameter.description || '',
140
+ defaultValue: getDefaultValueForType(finalFieldType) as FieldValue<T>,
141
+ validation: getDefaultValidationForType(),
142
+ width: 'full',
143
+ options,
144
+ };
145
+
146
+ baseField.validation = enhanceNumericValidation(
147
+ baseField.validation,
148
+ parameter.type,
149
+ STELLAR_NUMERIC_BOUNDS
150
+ );
151
+
152
+ // Propagate max byte length for BytesN types so the UI can enforce it
153
+ if (isBytesNType(parameter.type)) {
154
+ const sizeMatch = parameter.type.match(/^BytesN<(\d+)>$/);
155
+ const maxBytes = sizeMatch ? Number.parseInt(sizeMatch[1], 10) : undefined;
156
+
157
+ if (!Number.isNaN(maxBytes) && Number.isFinite(maxBytes)) {
158
+ baseField.metadata = {
159
+ ...(baseField.metadata ?? {}),
160
+ maxBytes,
161
+ };
162
+ }
163
+ }
164
+
165
+ // For array types (including arrays of complex types), provide element type information
166
+ if (fieldType === 'array' || fieldType === 'array-object') {
167
+ const elementType = extractVecElementType(parameter.type);
168
+ if (elementType) {
169
+ const elementFieldType = mapStellarParameterTypeToFieldType(elementType);
170
+
171
+ // Check if the element type is an enum or struct and needs additional metadata
172
+ let elementEnumMetadata: EnumMetadata | undefined;
173
+ let elementComponents: FunctionParameter[] | undefined;
174
+ let finalElementFieldType = elementFieldType;
175
+
176
+ // Extract enum metadata for array elements
177
+ if (isLikelyEnumType(elementType)) {
178
+ if (specEntries && isEnumType(specEntries, elementType)) {
179
+ elementEnumMetadata = extractEnumVariants(specEntries, elementType) ?? undefined;
180
+ if (elementEnumMetadata) {
181
+ // Override field type based on enum characteristics
182
+ finalElementFieldType = elementEnumMetadata.isUnitOnly ? 'select' : 'enum';
183
+ }
184
+ }
185
+ }
186
+
187
+ // Extract struct components for array elements
188
+ if (specEntries && isStructType(specEntries, elementType)) {
189
+ const structFields = extractStructFields(specEntries, elementType);
190
+ if (structFields && structFields.length > 0) {
191
+ elementComponents = structFields;
192
+ finalElementFieldType = 'object';
193
+ }
194
+ }
195
+
196
+ // For array-object, if we have components from the parameter, use them
197
+ if (fieldType === 'array-object' && parameter.components) {
198
+ elementComponents = parameter.components;
199
+ finalElementFieldType = 'object';
200
+ }
201
+
202
+ // Build element validation with bounds
203
+ let elementValidation: FieldValidation = { required: true };
204
+ elementValidation = enhanceNumericValidation(
205
+ elementValidation,
206
+ elementType,
207
+ STELLAR_NUMERIC_BOUNDS
208
+ );
209
+
210
+ // Add array-specific properties with full element metadata
211
+ const arrayField = {
212
+ ...baseField,
213
+ type: 'array' as FieldType, // Always use 'array' as the field type
214
+ elementType: finalElementFieldType,
215
+ elementFieldConfig: {
216
+ type: finalElementFieldType,
217
+ validation: elementValidation,
218
+ placeholder: `Enter ${elementType}`,
219
+ originalParameterType: elementType,
220
+ ...(elementEnumMetadata && { enumMetadata: elementEnumMetadata }),
221
+ ...(elementComponents && { components: elementComponents }),
222
+ },
223
+ };
224
+ return arrayField as unknown as FormFieldType<T>;
225
+ }
226
+ }
227
+
228
+ // For map types, provide key and value type information
229
+ if (fieldType === ('map' as FieldType)) {
230
+ const mapTypes = extractMapTypes(parameter.type);
231
+ if (mapTypes) {
232
+ const keyFieldType = mapStellarParameterTypeToFieldType(mapTypes.keyType);
233
+ const valueFieldType = mapStellarParameterTypeToFieldType(mapTypes.valueType);
234
+
235
+ // Add map-specific properties
236
+ const mapField = {
237
+ ...baseField,
238
+ mapMetadata: {
239
+ keyType: keyFieldType,
240
+ valueType: valueFieldType,
241
+ keyFieldConfig: {
242
+ type: keyFieldType,
243
+ validation: enhanceNumericValidation(
244
+ { required: true },
245
+ mapTypes.keyType,
246
+ STELLAR_NUMERIC_BOUNDS
247
+ ),
248
+ placeholder: `Enter ${mapTypes.keyType}`,
249
+ originalParameterType: mapTypes.keyType,
250
+ },
251
+ valueFieldConfig: {
252
+ type: valueFieldType,
253
+ validation: enhanceNumericValidation(
254
+ { required: true },
255
+ mapTypes.valueType,
256
+ STELLAR_NUMERIC_BOUNDS
257
+ ),
258
+ placeholder: `Enter ${mapTypes.valueType}`,
259
+ originalParameterType: mapTypes.valueType,
260
+ },
261
+ },
262
+ validation: {
263
+ ...getDefaultValidationForType(),
264
+ min: 0,
265
+ // No max limit - users can add as many map entries as needed
266
+ },
267
+ };
268
+ return mapField;
269
+ }
270
+ }
271
+
272
+ // Preserve components for object and array-object types
273
+ if (fieldType === 'object' || fieldType === 'array-object') {
274
+ const componentsToUse = parameter.components || structComponents;
275
+ if (componentsToUse) {
276
+ // Prefer the baked-in WASM components, otherwise fall back to the SAC-derived
277
+ // components we cached above.
278
+ const result = {
279
+ ...baseField,
280
+ components: componentsToUse,
281
+ };
282
+ return result;
283
+ }
284
+ }
285
+
286
+ // Add enum metadata if available
287
+ if (enumMetadata) {
288
+ const result = {
289
+ ...baseField,
290
+ enumMetadata,
291
+ };
292
+ return result;
293
+ }
294
+
295
+ return baseField;
296
+ }
@@ -0,0 +1,5 @@
1
+ // Barrel file
2
+
3
+ export * from './constants';
4
+ export * from './type-mapper';
5
+ export * from './field-generator';
@@ -0,0 +1,106 @@
1
+ import { xdr } from '@stellar/stellar-sdk';
2
+
3
+ import type { FunctionParameter } from '@openzeppelin/ui-types';
4
+ import { logger } from '@openzeppelin/ui-utils';
5
+
6
+ // Import the type extraction utility from the shared utils module
7
+ import { extractSorobanTypeFromScSpec } from '../utils/type-detection';
8
+
9
+ /**
10
+ * Extracts struct field definitions from Stellar contract spec entries
11
+ *
12
+ * @param entries Array of ScSpecEntry from contract spec
13
+ * @param structName Name of the struct type to extract fields for
14
+ * @returns Array of FunctionParameter representing struct fields, or null if not found
15
+ */
16
+ export function extractStructFields(
17
+ entries: xdr.ScSpecEntry[],
18
+ structName: string
19
+ ): FunctionParameter[] | null {
20
+ try {
21
+ // Find the entry for the requested struct name
22
+ const entry = entries.find((e) => {
23
+ try {
24
+ return e.value().name().toString() === structName;
25
+ } catch {
26
+ return false;
27
+ }
28
+ });
29
+
30
+ if (!entry) {
31
+ return null;
32
+ }
33
+
34
+ const entryKind = entry.switch();
35
+
36
+ // Handle UDT Struct (like DemoStruct { id: u32, flag: bool, info: Symbol })
37
+ if (entryKind.value === xdr.ScSpecEntryKind.scSpecEntryUdtStructV0().value) {
38
+ const structUdt = entry.udtStructV0();
39
+ const fields = structUdt.fields();
40
+ const structFields: FunctionParameter[] = [];
41
+
42
+ for (const field of fields) {
43
+ const fieldName = field.name().toString();
44
+ const fieldType = extractSorobanTypeFromScSpec(field.type());
45
+
46
+ const fieldParam: FunctionParameter = {
47
+ name: fieldName,
48
+ type: fieldType,
49
+ };
50
+
51
+ // Recursively extract nested struct components
52
+ if (isStructType(entries, fieldType)) {
53
+ const nestedFields = extractStructFields(entries, fieldType);
54
+ if (nestedFields && nestedFields.length > 0) {
55
+ fieldParam.components = nestedFields;
56
+ }
57
+ }
58
+
59
+ structFields.push(fieldParam);
60
+ }
61
+
62
+ return structFields;
63
+ }
64
+
65
+ return null;
66
+ } catch (error) {
67
+ logger.error(
68
+ 'extractStructFields',
69
+ `Failed to extract struct fields for ${structName}:`,
70
+ error
71
+ );
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Determines if a given type name is a struct type in the contract spec
78
+ *
79
+ * @param entries Array of ScSpecEntry from contract spec
80
+ * @param typeName Name of the type to check
81
+ * @returns true if the type is a struct, false otherwise
82
+ */
83
+ export function isStructType(entries: xdr.ScSpecEntry[], typeName: string): boolean {
84
+ try {
85
+ const entry = entries.find((e) => {
86
+ try {
87
+ const entryName = e.value().name().toString();
88
+ return entryName === typeName;
89
+ } catch {
90
+ return false;
91
+ }
92
+ });
93
+
94
+ if (!entry) {
95
+ return false;
96
+ }
97
+
98
+ const entryKind = entry.switch();
99
+ const isStruct = entryKind.value === xdr.ScSpecEntryKind.scSpecEntryUdtStructV0().value;
100
+
101
+ return isStruct;
102
+ } catch (error) {
103
+ logger.error('isStructType', `Failed to check if ${typeName} is struct:`, error);
104
+ return false;
105
+ }
106
+ }
@@ -0,0 +1,43 @@
1
+ import { xdr } from '@stellar/stellar-sdk';
2
+
3
+ import type { FunctionParameter } from '@openzeppelin/ui-types';
4
+
5
+ import { extractTupleTypes } from '../utils/safe-type-parser';
6
+ import { extractStructFields, isStructType } from './struct-fields';
7
+
8
+ /**
9
+ * Builds synthetic FunctionParameter definitions for tuple types so they can be rendered
10
+ * using the existing object field UI.
11
+ */
12
+ export function buildTupleComponents(
13
+ parameterType: string,
14
+ specEntries?: xdr.ScSpecEntry[]
15
+ ): FunctionParameter[] | null {
16
+ const tupleElements = extractTupleTypes(parameterType);
17
+ if (!tupleElements || tupleElements.length === 0) {
18
+ return null;
19
+ }
20
+
21
+ return tupleElements.map((elementType, index) => {
22
+ let nestedComponents: FunctionParameter[] | undefined;
23
+
24
+ if (specEntries && isStructType(specEntries, elementType)) {
25
+ const structFields = extractStructFields(specEntries, elementType);
26
+ if (structFields && structFields.length > 0) {
27
+ nestedComponents = structFields;
28
+ }
29
+ } else if (elementType.startsWith('Tuple<')) {
30
+ const tupleStruct = buildTupleComponents(elementType, specEntries);
31
+ if (tupleStruct && tupleStruct.length > 0) {
32
+ nestedComponents = tupleStruct;
33
+ }
34
+ }
35
+
36
+ return {
37
+ name: `item_${index}`,
38
+ type: elementType,
39
+ displayName: `Value ${index + 1} (${elementType})`,
40
+ ...(nestedComponents && { components: nestedComponents }),
41
+ };
42
+ });
43
+ }