@openzeppelin/ui-renderer 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/LICENSE +661 -0
- package/README.md +224 -0
- package/dist/index-CrGita0t.d.ts +344 -0
- package/dist/index-CrGita0t.d.ts.map +1 -0
- package/dist/index-DMBaQjZf.d.cts +344 -0
- package/dist/index-DMBaQjZf.d.cts.map +1 -0
- package/dist/index.cjs +2101 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +344 -0
- package/dist/index.d.ts +344 -0
- package/dist/index.js +2052 -0
- package/dist/index.js.map +1 -0
- package/package.json +94 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2052 @@
|
|
|
1
|
+
import { AlertCircle, AlertTriangle, CheckCircle, CheckCircle2, ExternalLink, FileText, Info, Key, Loader2, Minimize2, Network, RefreshCw, Settings, Shield, User, Users, XCircle } from "lucide-react";
|
|
2
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { FormProvider, useForm, useWatch } from "react-hook-form";
|
|
4
|
+
import { appConfigService, cn, logger, rateLimitedBatch, sanitizeHtml, userNetworkServiceConfigService, userRpcConfigService } from "@openzeppelin/ui-utils";
|
|
5
|
+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, AddressDisplay, AddressField, Alert, AlertDescription, AlertTitle, AmountField, ArrayField, ArrayObjectField, BigIntField, BooleanField, Button, BytesField, Card, CardContent, CardHeader, CardTitle, CodeEditorField, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, EmptyState, EnumField, FileUploadField, LoadingButton, MapField, NetworkStatusBadge, NumberField, ObjectField, PasswordField, RadioField, RelayerDetailsCard, SelectField, SelectGroupedField, Tabs, TabsContent, TabsList, TabsTrigger, TextAreaField, TextField, UrlField, ViewContractStateButton, useNetworkErrors } from "@openzeppelin/ui-components";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { WalletConnectionUI, useWalletState } from "@openzeppelin/ui-react";
|
|
8
|
+
|
|
9
|
+
//#region src/components/ExecutionConfigDisplay/components/EoaConfigDetails.tsx
|
|
10
|
+
const EoaConfigDetails = ({ config }) => /* @__PURE__ */ jsxs("div", {
|
|
11
|
+
className: "space-y-4",
|
|
12
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
13
|
+
className: "flex items-start space-x-3 p-3 bg-slate-50 rounded-md",
|
|
14
|
+
children: [/* @__PURE__ */ jsx(User, { className: "size-5 text-primary mt-0.5 flex-shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h4", {
|
|
15
|
+
className: "text-sm font-medium mb-1",
|
|
16
|
+
children: "Externally Owned Account (EOA)"
|
|
17
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
18
|
+
className: "text-sm text-muted-foreground",
|
|
19
|
+
children: "Transaction will be executed directly from the connected wallet."
|
|
20
|
+
})] })]
|
|
21
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
22
|
+
className: "flex items-start space-x-3 p-3 bg-slate-50 rounded-md",
|
|
23
|
+
children: [/* @__PURE__ */ jsx(Key, { className: "size-5 text-primary mt-0.5 flex-shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [
|
|
24
|
+
/* @__PURE__ */ jsx("h4", {
|
|
25
|
+
className: "text-sm font-medium mb-1",
|
|
26
|
+
children: "Execution Restrictions"
|
|
27
|
+
}),
|
|
28
|
+
/* @__PURE__ */ jsx("p", {
|
|
29
|
+
className: "text-sm text-muted-foreground",
|
|
30
|
+
children: config.allowAny ? "Any connected wallet can try to execute this transaction." : config.specificAddress ? "Only this address can try to execute this transaction:" : "No specific address restrictions defined."
|
|
31
|
+
}),
|
|
32
|
+
config.specificAddress && !config.allowAny && /* @__PURE__ */ jsx(AddressDisplay, {
|
|
33
|
+
className: "mt-2",
|
|
34
|
+
address: config.specificAddress
|
|
35
|
+
})
|
|
36
|
+
] })]
|
|
37
|
+
})]
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/components/ExecutionConfigDisplay/components/ExecutionMethodTrigger.tsx
|
|
42
|
+
const ExecutionMethodTrigger = ({ executionConfig, isValid, error, className }) => {
|
|
43
|
+
const getMethodIcon = (method) => {
|
|
44
|
+
const iconColorClass = !isValid ? "text-red-500" : "text-primary";
|
|
45
|
+
switch (method) {
|
|
46
|
+
case "eoa": return /* @__PURE__ */ jsx(User, { className: `size-3.5 ${iconColorClass}` });
|
|
47
|
+
case "relayer": return /* @__PURE__ */ jsx(Shield, { className: `size-3.5 ${iconColorClass}` });
|
|
48
|
+
case "multisig": return /* @__PURE__ */ jsx(Users, { className: `size-3.5 ${iconColorClass}` });
|
|
49
|
+
default: return /* @__PURE__ */ jsx(Key, { className: `size-3.5 ${!isValid ? "text-red-500" : "text-muted"}` });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return /* @__PURE__ */ jsxs(DialogTrigger, {
|
|
53
|
+
className: cn("inline-flex items-center gap-2 px-3 py-2 text-xs rounded-md border group", "transition-all duration-200 hover:bg-accent hover:text-accent-foreground", !isValid ? "border-red-300 bg-red-50 text-red-800 hover:bg-red-50/80" : "border-slate-200 bg-white text-slate-700", className),
|
|
54
|
+
style: !isValid ? { animation: "subtle-pulse-scale 2s cubic-bezier(0.4, 0, 0.6, 1) infinite" } : void 0,
|
|
55
|
+
children: [
|
|
56
|
+
/* @__PURE__ */ jsxs("div", {
|
|
57
|
+
className: "flex items-center gap-1.5",
|
|
58
|
+
children: [
|
|
59
|
+
getMethodIcon(executionConfig.method),
|
|
60
|
+
/* @__PURE__ */ jsx("span", {
|
|
61
|
+
className: "font-semibold",
|
|
62
|
+
children: "Execution:"
|
|
63
|
+
}),
|
|
64
|
+
/* @__PURE__ */ jsx("span", {
|
|
65
|
+
className: "uppercase",
|
|
66
|
+
children: executionConfig.method
|
|
67
|
+
})
|
|
68
|
+
]
|
|
69
|
+
}),
|
|
70
|
+
/* @__PURE__ */ jsx("div", {
|
|
71
|
+
className: "flex items-center ml-2",
|
|
72
|
+
children: !isValid ? /* @__PURE__ */ jsx(AlertCircle, { className: "size-3.5 text-red-500" }) : /* @__PURE__ */ jsx(Info, { className: "size-3.5 text-muted-foreground transition-colors group-hover:text-foreground" })
|
|
73
|
+
}),
|
|
74
|
+
!isValid && error && /* @__PURE__ */ jsx("div", {
|
|
75
|
+
className: "sr-only group-hover:not-sr-only group-hover:absolute group-hover:top-full group-hover:left-0 group-hover:mt-1 group-hover:px-2 group-hover:py-1 group-hover:bg-red-900 group-hover:text-white group-hover:text-xs group-hover:rounded group-hover:shadow-lg group-hover:z-50 group-hover:max-w-xs",
|
|
76
|
+
children: error
|
|
77
|
+
}),
|
|
78
|
+
/* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: `
|
|
79
|
+
@keyframes subtle-pulse-scale {
|
|
80
|
+
0%, 100% {
|
|
81
|
+
opacity: 0.65;
|
|
82
|
+
transform: scale(1);
|
|
83
|
+
}
|
|
84
|
+
50% {
|
|
85
|
+
opacity: 1;
|
|
86
|
+
transform: scale(1.02);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
` } })
|
|
90
|
+
]
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/components/ExecutionConfigDisplay/components/ExecutionConfigCard.tsx
|
|
96
|
+
const ExecutionConfigCard = ({ config }) => {
|
|
97
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
98
|
+
className: "flex items-start space-x-3 p-3 bg-slate-50 rounded-md",
|
|
99
|
+
children: [/* @__PURE__ */ jsx(Settings, { className: "size-5 text-primary mt-0.5 flex-shrink-0" }), /* @__PURE__ */ jsxs("div", {
|
|
100
|
+
className: "flex-1",
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ jsx("h4", {
|
|
103
|
+
className: "text-sm font-medium mb-1",
|
|
104
|
+
children: "Execution Configuration"
|
|
105
|
+
}),
|
|
106
|
+
/* @__PURE__ */ jsx("p", {
|
|
107
|
+
className: "text-sm text-muted-foreground mb-2",
|
|
108
|
+
children: "Configuration parameters that will be used for transaction execution."
|
|
109
|
+
}),
|
|
110
|
+
/* @__PURE__ */ jsxs("div", {
|
|
111
|
+
className: "space-y-2",
|
|
112
|
+
children: [config.transactionOptions && Object.keys(config.transactionOptions).length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
|
|
113
|
+
className: "text-xs text-muted-foreground font-medium",
|
|
114
|
+
children: "Transaction Options:"
|
|
115
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
116
|
+
className: "mt-1 bg-white border rounded p-2",
|
|
117
|
+
children: Object.entries(config.transactionOptions).map(([key, value]) => /* @__PURE__ */ jsxs("div", {
|
|
118
|
+
className: "flex items-center justify-between py-1 border-b last:border-b-0",
|
|
119
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
120
|
+
className: "text-xs font-mono text-slate-600",
|
|
121
|
+
children: [key, ":"]
|
|
122
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
123
|
+
className: "text-xs font-mono bg-slate-100 px-2 py-0.5 rounded",
|
|
124
|
+
children: typeof value === "object" && value !== null ? JSON.stringify(value) : String(value)
|
|
125
|
+
})]
|
|
126
|
+
}, key))
|
|
127
|
+
})] }), (!config.transactionOptions || Object.keys(config.transactionOptions).length === 0) && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
|
|
128
|
+
className: "text-xs text-muted-foreground font-medium",
|
|
129
|
+
children: "Transaction Options:"
|
|
130
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
131
|
+
className: "text-xs text-muted-foreground ml-2",
|
|
132
|
+
children: "None configured"
|
|
133
|
+
})] })]
|
|
134
|
+
})
|
|
135
|
+
]
|
|
136
|
+
})]
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/components/ExecutionConfigDisplay/components/RelayerConfigDetails.tsx
|
|
142
|
+
const RelayerConfigDetails = ({ config, enhancedDetails, loading = false }) => {
|
|
143
|
+
const { relayer } = config;
|
|
144
|
+
const { activeAdapter } = useWalletState();
|
|
145
|
+
const labels = activeAdapter?.getUiLabels?.();
|
|
146
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
147
|
+
className: "space-y-4",
|
|
148
|
+
children: [
|
|
149
|
+
/* @__PURE__ */ jsxs("div", {
|
|
150
|
+
className: "flex items-start space-x-3 p-3 bg-slate-50 rounded-md",
|
|
151
|
+
children: [/* @__PURE__ */ jsx(Shield, { className: "size-5 text-primary mt-0.5 flex-shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h4", {
|
|
152
|
+
className: "text-sm font-medium mb-1",
|
|
153
|
+
children: "OpenZeppelin Relayer"
|
|
154
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
155
|
+
className: "text-sm text-muted-foreground",
|
|
156
|
+
children: "Transaction will be sent via the selected OpenZeppelin Relayer."
|
|
157
|
+
})] })]
|
|
158
|
+
}),
|
|
159
|
+
/* @__PURE__ */ jsx(RelayerDetailsCard, {
|
|
160
|
+
details: relayer,
|
|
161
|
+
enhancedDetails,
|
|
162
|
+
loading,
|
|
163
|
+
labels
|
|
164
|
+
}),
|
|
165
|
+
/* @__PURE__ */ jsxs("div", {
|
|
166
|
+
className: "flex items-start space-x-3 p-3 bg-slate-50 rounded-md",
|
|
167
|
+
children: [/* @__PURE__ */ jsx(Network, { className: "size-5 text-primary mt-0.5 flex-shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h4", {
|
|
168
|
+
className: "text-sm font-medium mb-1",
|
|
169
|
+
children: "Service Endpoint"
|
|
170
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
171
|
+
className: "text-xs text-muted-foreground font-mono break-all",
|
|
172
|
+
children: config.serviceUrl
|
|
173
|
+
})] })]
|
|
174
|
+
}),
|
|
175
|
+
/* @__PURE__ */ jsx(ExecutionConfigCard, { config })
|
|
176
|
+
]
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/components/ExecutionConfigDisplay/hooks/useExecutionValidation.ts
|
|
182
|
+
const useExecutionValidation = ({ executionConfig, adapter, runtimeApiKey }) => {
|
|
183
|
+
const [validationResult, setValidationResult] = useState({ isValid: true });
|
|
184
|
+
const validateConfig = useCallback(async () => {
|
|
185
|
+
if (!adapter) {
|
|
186
|
+
setValidationResult({
|
|
187
|
+
isValid: false,
|
|
188
|
+
error: "No adapter available for validation"
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const result = await adapter.validateExecutionConfig(executionConfig);
|
|
194
|
+
if (result === true) {
|
|
195
|
+
let runtimeError;
|
|
196
|
+
if (executionConfig.method === "relayer" && (!runtimeApiKey || runtimeApiKey.trim() === "")) runtimeError = "Relayer API key is required for transaction execution";
|
|
197
|
+
setValidationResult({
|
|
198
|
+
isValid: !runtimeError,
|
|
199
|
+
error: runtimeError
|
|
200
|
+
});
|
|
201
|
+
} else setValidationResult({
|
|
202
|
+
isValid: false,
|
|
203
|
+
error: result
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
setValidationResult({
|
|
207
|
+
isValid: false,
|
|
208
|
+
error: error instanceof Error ? error.message : "Validation failed"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}, [
|
|
212
|
+
executionConfig,
|
|
213
|
+
adapter,
|
|
214
|
+
runtimeApiKey
|
|
215
|
+
]);
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
validateConfig();
|
|
218
|
+
}, [validateConfig]);
|
|
219
|
+
return validationResult;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/components/ExecutionConfigDisplay/ExecutionConfigDisplay.tsx
|
|
224
|
+
const ExecutionConfigDisplay = ({ executionConfig, adapter, error, onRuntimeApiKeyChange }) => {
|
|
225
|
+
const { control, watch } = useForm({ defaultValues: { runtimeApiKey: "" } });
|
|
226
|
+
const runtimeApiKey = watch("runtimeApiKey");
|
|
227
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
228
|
+
const [enhancedRelayerDetails, setEnhancedRelayerDetails] = useState(null);
|
|
229
|
+
const [relayerDetailsLoading, setRelayerDetailsLoading] = useState(false);
|
|
230
|
+
const { isValid, error: validationError } = useExecutionValidation({
|
|
231
|
+
executionConfig,
|
|
232
|
+
adapter,
|
|
233
|
+
runtimeApiKey
|
|
234
|
+
});
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
onRuntimeApiKeyChange?.(runtimeApiKey);
|
|
237
|
+
}, [runtimeApiKey, onRuntimeApiKeyChange]);
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (isOpen && executionConfig.method === "relayer" && runtimeApiKey && adapter?.getRelayer) {
|
|
240
|
+
const relayerConfig = executionConfig;
|
|
241
|
+
setRelayerDetailsLoading(true);
|
|
242
|
+
adapter.getRelayer(relayerConfig.serviceUrl, runtimeApiKey, relayerConfig.relayer.relayerId).then((details) => {
|
|
243
|
+
setEnhancedRelayerDetails(details);
|
|
244
|
+
}).catch((err) => {
|
|
245
|
+
logger.error("ExecutionConfigDisplay", "Failed to fetch enhanced relayer details:", err);
|
|
246
|
+
setEnhancedRelayerDetails(null);
|
|
247
|
+
}).finally(() => {
|
|
248
|
+
setRelayerDetailsLoading(false);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}, [
|
|
252
|
+
isOpen,
|
|
253
|
+
executionConfig,
|
|
254
|
+
runtimeApiKey,
|
|
255
|
+
adapter
|
|
256
|
+
]);
|
|
257
|
+
const getExecutionContent = () => {
|
|
258
|
+
switch (executionConfig.method) {
|
|
259
|
+
case "eoa": return /* @__PURE__ */ jsx(EoaConfigDetails, { config: executionConfig });
|
|
260
|
+
case "relayer": return /* @__PURE__ */ jsx(RelayerConfigDetails, {
|
|
261
|
+
config: executionConfig,
|
|
262
|
+
enhancedDetails: enhancedRelayerDetails,
|
|
263
|
+
loading: relayerDetailsLoading
|
|
264
|
+
});
|
|
265
|
+
default: return /* @__PURE__ */ jsx(EmptyState, {
|
|
266
|
+
icon: /* @__PURE__ */ jsx(AlertTriangle, { className: "h-6 w-6 text-muted-foreground" }),
|
|
267
|
+
title: "Unknown Execution Method",
|
|
268
|
+
description: "The selected execution method is not recognized. Please check your configuration or contact support.",
|
|
269
|
+
size: "small"
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const displayError = validationError || error || void 0;
|
|
274
|
+
return /* @__PURE__ */ jsxs(Dialog, {
|
|
275
|
+
open: isOpen,
|
|
276
|
+
onOpenChange: setIsOpen,
|
|
277
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
278
|
+
className: "flex justify-end w-full",
|
|
279
|
+
children: /* @__PURE__ */ jsx(ExecutionMethodTrigger, {
|
|
280
|
+
executionConfig,
|
|
281
|
+
isValid,
|
|
282
|
+
error: displayError
|
|
283
|
+
})
|
|
284
|
+
}), /* @__PURE__ */ jsxs(DialogContent, {
|
|
285
|
+
className: "sm:max-w-[550px] max-h-[90vh] flex flex-col",
|
|
286
|
+
children: [/* @__PURE__ */ jsxs(DialogHeader, {
|
|
287
|
+
className: "border-b pb-4 shrink-0",
|
|
288
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
289
|
+
className: "flex items-start justify-between",
|
|
290
|
+
children: /* @__PURE__ */ jsxs(DialogTitle, {
|
|
291
|
+
className: "flex items-center gap-2 text-xl",
|
|
292
|
+
children: ["Execution Method", displayError && /* @__PURE__ */ jsx(AlertCircle, { className: "h-5 w-5 text-red-500" })]
|
|
293
|
+
})
|
|
294
|
+
}), /* @__PURE__ */ jsx(DialogDescription, {
|
|
295
|
+
className: "mt-2",
|
|
296
|
+
children: "This outlines how the transaction will be signed and submitted to the blockchain."
|
|
297
|
+
})]
|
|
298
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
299
|
+
className: "overflow-y-auto grow",
|
|
300
|
+
children: [
|
|
301
|
+
/* @__PURE__ */ jsx("div", {
|
|
302
|
+
className: "p-6",
|
|
303
|
+
children: getExecutionContent()
|
|
304
|
+
}),
|
|
305
|
+
executionConfig.method === "relayer" && /* @__PURE__ */ jsx("div", {
|
|
306
|
+
className: "px-6 pb-4",
|
|
307
|
+
children: /* @__PURE__ */ jsx(PasswordField, {
|
|
308
|
+
id: "runtime-api-key",
|
|
309
|
+
label: "Relayer API Key",
|
|
310
|
+
name: "runtimeApiKey",
|
|
311
|
+
control,
|
|
312
|
+
placeholder: "Enter your API key",
|
|
313
|
+
validation: { required: true },
|
|
314
|
+
helperText: "This key is required to send the transaction and is not stored."
|
|
315
|
+
})
|
|
316
|
+
}),
|
|
317
|
+
displayError && /* @__PURE__ */ jsx("div", {
|
|
318
|
+
className: "px-6 pb-4",
|
|
319
|
+
children: /* @__PURE__ */ jsx(Alert, {
|
|
320
|
+
variant: "destructive",
|
|
321
|
+
className: "p-3 border border-red-300",
|
|
322
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
323
|
+
className: "flex items-start",
|
|
324
|
+
children: [/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 shrink-0" }), /* @__PURE__ */ jsx(AlertDescription, {
|
|
325
|
+
className: "pl-2 text-sm",
|
|
326
|
+
children: displayError
|
|
327
|
+
})]
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
]
|
|
332
|
+
})]
|
|
333
|
+
})]
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/components/transaction/TransactionExecuteButton.tsx
|
|
339
|
+
/**
|
|
340
|
+
* TransactionExecuteButton Component
|
|
341
|
+
*
|
|
342
|
+
* Displays a button for executing a transaction, which is disabled if the wallet is not connected,
|
|
343
|
+
* the form is invalid, or a transaction is currently being submitted.
|
|
344
|
+
*
|
|
345
|
+
* @param props The component props
|
|
346
|
+
* @returns A React component
|
|
347
|
+
*/
|
|
348
|
+
function TransactionExecuteButton({ isWalletConnected, isSubmitting, isFormValid, variant = "default", functionDetails, canExecuteLocally = false }) {
|
|
349
|
+
const canExecute = canExecuteLocally || functionDetails?.stateMutability === "pure";
|
|
350
|
+
return /* @__PURE__ */ jsx(LoadingButton, {
|
|
351
|
+
type: "submit",
|
|
352
|
+
disabled: !isWalletConnected && !canExecute || !isFormValid,
|
|
353
|
+
loading: isSubmitting,
|
|
354
|
+
variant,
|
|
355
|
+
size: "lg",
|
|
356
|
+
className: "w-full md:w-auto",
|
|
357
|
+
children: canExecute ? isSubmitting ? "Executing..." : "Execute Locally" : isSubmitting ? "Executing..." : "Execute Transaction"
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/utils/formUtils.ts
|
|
363
|
+
/**
|
|
364
|
+
* Validate a field value against validation rules
|
|
365
|
+
*/
|
|
366
|
+
function validateField(value, validation) {
|
|
367
|
+
if (validation?.required && (value === "" || value === null || value === void 0)) return "This field is required";
|
|
368
|
+
if (validation?.min !== void 0 && typeof value === "number" && value < validation.min) return `Value must be at least ${validation.min}`;
|
|
369
|
+
if (validation?.max !== void 0 && typeof value === "number" && value > validation.max) return `Value must be at most ${validation.max}`;
|
|
370
|
+
if (validation?.pattern !== void 0 && typeof value === "string" && new RegExp(validation.pattern).test(value) === false) return "Value does not match the required pattern";
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Creates a transform for address fields
|
|
375
|
+
*
|
|
376
|
+
* @param adapter The blockchain adapter to use for validation
|
|
377
|
+
* @returns Transform functions for address fields
|
|
378
|
+
*/
|
|
379
|
+
function createAddressTransform(adapter) {
|
|
380
|
+
return {
|
|
381
|
+
input: (value) => {
|
|
382
|
+
if (value === null || value === void 0) return "";
|
|
383
|
+
return String(value);
|
|
384
|
+
},
|
|
385
|
+
output: (value) => {
|
|
386
|
+
const address = String(value || "");
|
|
387
|
+
if (adapter.isValidAddress?.(address)) return address;
|
|
388
|
+
return "";
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Creates a transform for number fields
|
|
394
|
+
*
|
|
395
|
+
* @returns Transform functions for number fields
|
|
396
|
+
*/
|
|
397
|
+
function createNumberTransform() {
|
|
398
|
+
return {
|
|
399
|
+
input: (value) => {
|
|
400
|
+
if (value === void 0 || value === null) return "";
|
|
401
|
+
return String(value);
|
|
402
|
+
},
|
|
403
|
+
output: (value) => {
|
|
404
|
+
const num = Number(value);
|
|
405
|
+
return isNaN(num) ? 0 : num;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Creates a transform for bigint fields (large integers beyond JS Number precision)
|
|
411
|
+
*
|
|
412
|
+
* @returns Transform functions for bigint fields
|
|
413
|
+
*/
|
|
414
|
+
function createBigIntTransform() {
|
|
415
|
+
return {
|
|
416
|
+
input: (value) => {
|
|
417
|
+
if (value === void 0 || value === null) return "";
|
|
418
|
+
return String(value);
|
|
419
|
+
},
|
|
420
|
+
output: (value) => {
|
|
421
|
+
const str = String(value || "");
|
|
422
|
+
if (str && !/^-?\d+$/.test(str)) return "";
|
|
423
|
+
return str;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Creates a transform for boolean fields
|
|
429
|
+
*
|
|
430
|
+
* @returns Transform functions for boolean fields
|
|
431
|
+
*/
|
|
432
|
+
function createBooleanTransform() {
|
|
433
|
+
return {
|
|
434
|
+
input: (value) => {
|
|
435
|
+
if (value === void 0 || value === null) return false;
|
|
436
|
+
if (typeof value === "string") return value.toLowerCase() === "true" || value === "1";
|
|
437
|
+
return Boolean(value);
|
|
438
|
+
},
|
|
439
|
+
output: (value) => {
|
|
440
|
+
if (typeof value === "string") return value.toLowerCase() === "true" || value === "1";
|
|
441
|
+
return Boolean(value);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Creates a transform for complex type fields (arrays, objects, etc.)
|
|
447
|
+
*
|
|
448
|
+
* @returns Transform functions for complex fields
|
|
449
|
+
*/
|
|
450
|
+
function createComplexTypeTransform() {
|
|
451
|
+
return {
|
|
452
|
+
input: (value) => {
|
|
453
|
+
if (value === void 0 || value === null) return "";
|
|
454
|
+
if (typeof value === "string") return value;
|
|
455
|
+
try {
|
|
456
|
+
return JSON.stringify(value, null, 2);
|
|
457
|
+
} catch {
|
|
458
|
+
return "";
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
output: (value) => {
|
|
462
|
+
if (typeof value !== "string" || !value) return null;
|
|
463
|
+
try {
|
|
464
|
+
return JSON.parse(value);
|
|
465
|
+
} catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Creates a transform for text fields
|
|
473
|
+
*
|
|
474
|
+
* @returns Transform functions for text-based fields
|
|
475
|
+
*/
|
|
476
|
+
function createTextTransform() {
|
|
477
|
+
return {
|
|
478
|
+
input: (value) => {
|
|
479
|
+
if (value === null || value === void 0) return "";
|
|
480
|
+
return String(value);
|
|
481
|
+
},
|
|
482
|
+
output: (value) => {
|
|
483
|
+
return String(value || "");
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Creates a transform function based on field type
|
|
489
|
+
*
|
|
490
|
+
* @param fieldType The type of field to create transforms for
|
|
491
|
+
* @param adapter Optional adapter for address validation
|
|
492
|
+
* @returns Transform functions for the field type
|
|
493
|
+
*/
|
|
494
|
+
function createTransformForFieldType(fieldType, adapter) {
|
|
495
|
+
switch (fieldType) {
|
|
496
|
+
case "blockchain-address":
|
|
497
|
+
if (!adapter) throw new Error(`createTransformForFieldType: Adapter is required for 'blockchain-address' field type but was not provided.`);
|
|
498
|
+
return createAddressTransform(adapter);
|
|
499
|
+
case "number":
|
|
500
|
+
case "amount": return createNumberTransform();
|
|
501
|
+
case "bigint": return createBigIntTransform();
|
|
502
|
+
case "checkbox": return createBooleanTransform();
|
|
503
|
+
case "text":
|
|
504
|
+
case "email":
|
|
505
|
+
case "password":
|
|
506
|
+
case "textarea": return createTextTransform();
|
|
507
|
+
case "array": return createArrayTransform();
|
|
508
|
+
case "object": return createObjectTransform();
|
|
509
|
+
case "array-object": return createArrayObjectTransform();
|
|
510
|
+
case "enum": return createComplexTypeTransform();
|
|
511
|
+
default:
|
|
512
|
+
logger.warn("formUtils", `createTransformForFieldType: No specific transform for fieldType "${fieldType}". Falling back to createComplexTypeTransform. Ensure adapter maps all expected ABI types to specific FieldTypes.`);
|
|
513
|
+
return createComplexTypeTransform();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Generate a default value for a given field type based on parameter constraints
|
|
518
|
+
*/
|
|
519
|
+
function generateDefaultValue(parameterType, constraints = {}) {
|
|
520
|
+
const type = parameterType.toLowerCase();
|
|
521
|
+
if (type.includes("bool")) return false;
|
|
522
|
+
else if (type.includes("int") || type.includes("number")) return constraints.min !== void 0 ? constraints.min : 0;
|
|
523
|
+
else if (type.includes("string") || type.includes("address")) return "";
|
|
524
|
+
else if (type.includes("array") || type.includes("[]")) return [];
|
|
525
|
+
else return null;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Returns the appropriate default value based on field type
|
|
529
|
+
*/
|
|
530
|
+
function getDefaultValueByFieldType(fieldType) {
|
|
531
|
+
switch (fieldType) {
|
|
532
|
+
case "checkbox": return false;
|
|
533
|
+
case "number": return "";
|
|
534
|
+
default: return "";
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Creates a complete default values object for form initialization
|
|
539
|
+
* Ensures all fields have appropriate default values to avoid React controlled/uncontrolled input warnings
|
|
540
|
+
*
|
|
541
|
+
* @param fields The form field definitions
|
|
542
|
+
* @param existingDefaults Any existing default values to preserve
|
|
543
|
+
* @returns A complete form values object with no undefined values
|
|
544
|
+
*/
|
|
545
|
+
function createDefaultFormValues(fields, existingDefaults = {}) {
|
|
546
|
+
const defaults = { ...existingDefaults };
|
|
547
|
+
if (!fields) return defaults;
|
|
548
|
+
fields.forEach((field) => {
|
|
549
|
+
if (defaults[field.name] === void 0) defaults[field.name] = getDefaultValueByFieldType(field.type);
|
|
550
|
+
});
|
|
551
|
+
return defaults;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Creates a transform for array fields
|
|
555
|
+
*
|
|
556
|
+
* @returns Transform functions for array fields
|
|
557
|
+
*/
|
|
558
|
+
function createArrayTransform() {
|
|
559
|
+
return {
|
|
560
|
+
input: (value) => {
|
|
561
|
+
if (!Array.isArray(value)) {
|
|
562
|
+
logger.warn("formUtils", "createArrayTransform input received non-array value:", value);
|
|
563
|
+
return "[]";
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
return JSON.stringify(value, null, 2);
|
|
567
|
+
} catch {
|
|
568
|
+
return "[]";
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
output: (value) => {
|
|
572
|
+
if (typeof value === "string") try {
|
|
573
|
+
const parsed = JSON.parse(value);
|
|
574
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
575
|
+
} catch {
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
return Array.isArray(value) ? value : [];
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Creates a transform for object fields
|
|
584
|
+
*
|
|
585
|
+
* @returns Transform functions for object fields
|
|
586
|
+
*/
|
|
587
|
+
function createObjectTransform() {
|
|
588
|
+
return {
|
|
589
|
+
input: (value) => {
|
|
590
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
591
|
+
logger.warn("formUtils", "createObjectTransform input received non-object or null value:", value);
|
|
592
|
+
return "{}";
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
return JSON.stringify(value, null, 2);
|
|
596
|
+
} catch {
|
|
597
|
+
return "{}";
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
output: (value) => {
|
|
601
|
+
if (typeof value === "string") {
|
|
602
|
+
if (!value.trim()) return {};
|
|
603
|
+
try {
|
|
604
|
+
const parsed = JSON.parse(value);
|
|
605
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
606
|
+
return {};
|
|
607
|
+
} catch {
|
|
608
|
+
return {};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
612
|
+
return {};
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Creates a transform for array-object fields
|
|
618
|
+
*
|
|
619
|
+
* @returns Transform functions for array-object fields
|
|
620
|
+
*/
|
|
621
|
+
function createArrayObjectTransform() {
|
|
622
|
+
return {
|
|
623
|
+
input: (value) => {
|
|
624
|
+
if (!Array.isArray(value)) {
|
|
625
|
+
logger.warn("formUtils", "createArrayObjectTransform input received non-array value:", value);
|
|
626
|
+
return "[]";
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const validItems = value.filter((item) => item && typeof item === "object" && !Array.isArray(item));
|
|
630
|
+
return JSON.stringify(validItems, null, 2);
|
|
631
|
+
} catch {
|
|
632
|
+
return "[]";
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
output: (value) => {
|
|
636
|
+
if (typeof value === "string") {
|
|
637
|
+
if (!value.trim()) return [];
|
|
638
|
+
try {
|
|
639
|
+
const parsed = JSON.parse(value);
|
|
640
|
+
if (Array.isArray(parsed)) return parsed.filter((item) => item && typeof item === "object" && !Array.isArray(item));
|
|
641
|
+
return [];
|
|
642
|
+
} catch {
|
|
643
|
+
return [];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (Array.isArray(value)) return value.filter((item) => item && typeof item === "object" && !Array.isArray(item));
|
|
647
|
+
return [];
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
//#endregion
|
|
653
|
+
//#region src/utils/runtimeSecretExtractor.ts
|
|
654
|
+
/**
|
|
655
|
+
* Extracts runtime secrets from form submission data and field configuration.
|
|
656
|
+
*
|
|
657
|
+
* Handles two cases:
|
|
658
|
+
* 1. User-provided runtime secret (field value in form data)
|
|
659
|
+
* 2. Hardcoded readonly runtime secret (from field configuration)
|
|
660
|
+
*
|
|
661
|
+
* Returns both the extracted secrets and the cleaned contract arguments
|
|
662
|
+
* (with runtimeSecret fields removed).
|
|
663
|
+
*
|
|
664
|
+
* @param data - Form submission data from React Hook Form
|
|
665
|
+
* @param fields - Field configuration array from the schema
|
|
666
|
+
* @returns Object containing extracted secrets and cleaned contract arguments
|
|
667
|
+
*/
|
|
668
|
+
function extractRuntimeSecrets(data, fields) {
|
|
669
|
+
const runtimeSecrets = {};
|
|
670
|
+
const contractArgs = { ...data };
|
|
671
|
+
logger.debug("TransactionForm", "All form data values:", data);
|
|
672
|
+
logger.debug("TransactionForm", "Schema fields:", fields.map((f) => ({
|
|
673
|
+
name: f.name,
|
|
674
|
+
type: f.type,
|
|
675
|
+
readOnly: f.readOnly
|
|
676
|
+
})));
|
|
677
|
+
fields.forEach((field) => {
|
|
678
|
+
if (field.type === "runtimeSecret" && field.adapterBinding?.key) {
|
|
679
|
+
let secretValue = data[field.name];
|
|
680
|
+
if (field.readOnly && !secretValue && "hardcodedValue" in field) {
|
|
681
|
+
secretValue = field.hardcodedValue;
|
|
682
|
+
logger.debug("TransactionForm", `Using hardcoded value for readonly runtimeSecret field: ${field.name}`, { secretValue });
|
|
683
|
+
}
|
|
684
|
+
logger.debug("TransactionForm", `Processing runtimeSecret field: ${field.name}`, {
|
|
685
|
+
secretValue,
|
|
686
|
+
readOnly: field.readOnly
|
|
687
|
+
});
|
|
688
|
+
if (secretValue) runtimeSecrets[field.adapterBinding.key] = secretValue;
|
|
689
|
+
delete contractArgs[field.name];
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
logger.debug("TransactionForm", "Extracted runtime secrets:", Object.keys(runtimeSecrets), runtimeSecrets);
|
|
693
|
+
return {
|
|
694
|
+
runtimeSecrets,
|
|
695
|
+
contractArgs
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
//#endregion
|
|
700
|
+
//#region src/components/fieldRegistry.ts
|
|
701
|
+
/**
|
|
702
|
+
* Registry of field components mapped to their respective types.
|
|
703
|
+
* All field components in this registry are designed specifically for React Hook Form integration
|
|
704
|
+
* and are meant to be used within the DynamicFormField system, not as standalone components.
|
|
705
|
+
*/
|
|
706
|
+
const fieldComponents = {
|
|
707
|
+
text: TextField,
|
|
708
|
+
number: NumberField,
|
|
709
|
+
bigint: BigIntField,
|
|
710
|
+
"blockchain-address": AddressField,
|
|
711
|
+
checkbox: BooleanField,
|
|
712
|
+
radio: RadioField,
|
|
713
|
+
select: SelectField,
|
|
714
|
+
"select-grouped": SelectGroupedField,
|
|
715
|
+
textarea: TextAreaField,
|
|
716
|
+
bytes: BytesField,
|
|
717
|
+
"code-editor": CodeEditorField,
|
|
718
|
+
date: () => React.createElement("div", null, "Date field not implemented yet"),
|
|
719
|
+
email: () => React.createElement("div", null, "Email field not implemented yet"),
|
|
720
|
+
password: PasswordField,
|
|
721
|
+
amount: AmountField,
|
|
722
|
+
array: ArrayField,
|
|
723
|
+
object: ObjectField,
|
|
724
|
+
"array-object": ArrayObjectField,
|
|
725
|
+
map: MapField,
|
|
726
|
+
url: UrlField,
|
|
727
|
+
enum: EnumField,
|
|
728
|
+
hidden: () => null,
|
|
729
|
+
"file-upload": FileUploadField,
|
|
730
|
+
runtimeSecret: PasswordField
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
//#endregion
|
|
734
|
+
//#region src/components/DynamicFormField.tsx
|
|
735
|
+
/**
|
|
736
|
+
* Evaluates whether a field should be rendered based on its visibility conditions
|
|
737
|
+
*/
|
|
738
|
+
function useShouldRenderField(field, control) {
|
|
739
|
+
const formValues = useWatch({ control });
|
|
740
|
+
if (field.isHidden) return false;
|
|
741
|
+
if (!field.visibleWhen) return true;
|
|
742
|
+
return (Array.isArray(field.visibleWhen) ? field.visibleWhen : [field.visibleWhen]).every((condition) => {
|
|
743
|
+
const dependentValue = formValues[condition.field];
|
|
744
|
+
switch (condition.operator) {
|
|
745
|
+
case "equals": return dependentValue === condition.value;
|
|
746
|
+
case "notEquals": return dependentValue !== condition.value;
|
|
747
|
+
case "contains": return String(dependentValue).includes(String(condition.value || ""));
|
|
748
|
+
case "greaterThan": return Number(dependentValue) > Number(condition.value || 0);
|
|
749
|
+
case "lessThan": return Number(dependentValue) < Number(condition.value || 0);
|
|
750
|
+
case "matches":
|
|
751
|
+
if (typeof condition.value === "string") return new RegExp(condition.value).test(String(dependentValue || ""));
|
|
752
|
+
return false;
|
|
753
|
+
default: return true;
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Dynamic Form Field Component
|
|
759
|
+
*
|
|
760
|
+
* Renders the appropriate field component based on the field type defined in the form schema.
|
|
761
|
+
* This component is part of the app rendering system architecture where:
|
|
762
|
+
* 1. Form schemas are generated from contract functions using adapters
|
|
763
|
+
* 2. The schemas are rendered using the TransactionForm component
|
|
764
|
+
* 3. TransactionForm uses DynamicFormField to render appropriate field components based on the schema
|
|
765
|
+
*
|
|
766
|
+
* The field components (TextField, NumberField, AddressField, etc.) are specifically designed
|
|
767
|
+
* for React Hook Form integration and should not be used as standalone components.
|
|
768
|
+
*
|
|
769
|
+
* @returns The rendered form field component or null if the field should not be visible
|
|
770
|
+
*/
|
|
771
|
+
function DynamicFormField({ field, control, adapter, contractSchema }) {
|
|
772
|
+
const renderPayloadField = useCallback((payloadField, payloadIndex) => {
|
|
773
|
+
let enhancedPayloadField;
|
|
774
|
+
if (payloadField.originalParameterType) {
|
|
775
|
+
const generatedField = adapter.generateDefaultField({
|
|
776
|
+
name: payloadField.name || `payload_${payloadIndex}`,
|
|
777
|
+
type: payloadField.originalParameterType
|
|
778
|
+
}, contractSchema);
|
|
779
|
+
enhancedPayloadField = {
|
|
780
|
+
...generatedField,
|
|
781
|
+
...payloadField,
|
|
782
|
+
type: generatedField.type,
|
|
783
|
+
label: payloadField.label ?? generatedField.label,
|
|
784
|
+
placeholder: payloadField.placeholder ?? generatedField.placeholder,
|
|
785
|
+
helperText: payloadField.helperText ?? generatedField.helperText
|
|
786
|
+
};
|
|
787
|
+
} else enhancedPayloadField = {
|
|
788
|
+
...payloadField,
|
|
789
|
+
type: payloadField.type ?? "text"
|
|
790
|
+
};
|
|
791
|
+
return /* @__PURE__ */ jsx(DynamicFormField, {
|
|
792
|
+
field: enhancedPayloadField,
|
|
793
|
+
control,
|
|
794
|
+
adapter,
|
|
795
|
+
contractSchema
|
|
796
|
+
}, `${field.id}-payload-${payloadIndex}`);
|
|
797
|
+
}, [
|
|
798
|
+
field.id,
|
|
799
|
+
control,
|
|
800
|
+
adapter,
|
|
801
|
+
contractSchema
|
|
802
|
+
]);
|
|
803
|
+
const renderKeyField = useCallback((keyField, entryIndex) => {
|
|
804
|
+
const mappedKeyType = keyField.originalParameterType ? adapter.mapParameterTypeToFieldType(keyField.originalParameterType) : keyField.type;
|
|
805
|
+
return /* @__PURE__ */ jsx(DynamicFormField, {
|
|
806
|
+
field: {
|
|
807
|
+
...keyField,
|
|
808
|
+
type: mappedKeyType,
|
|
809
|
+
readOnly: keyField.readOnly ?? field.readOnly
|
|
810
|
+
},
|
|
811
|
+
control,
|
|
812
|
+
adapter,
|
|
813
|
+
contractSchema
|
|
814
|
+
}, `${field.id}-key-${entryIndex}`);
|
|
815
|
+
}, [
|
|
816
|
+
field.id,
|
|
817
|
+
field.readOnly,
|
|
818
|
+
control,
|
|
819
|
+
adapter,
|
|
820
|
+
contractSchema
|
|
821
|
+
]);
|
|
822
|
+
const renderValueField = useCallback((valueField, entryIndex) => {
|
|
823
|
+
const mappedValueType = valueField.originalParameterType ? adapter.mapParameterTypeToFieldType(valueField.originalParameterType) : valueField.type;
|
|
824
|
+
return /* @__PURE__ */ jsx(DynamicFormField, {
|
|
825
|
+
field: {
|
|
826
|
+
...valueField,
|
|
827
|
+
type: mappedValueType,
|
|
828
|
+
readOnly: valueField.readOnly ?? field.readOnly
|
|
829
|
+
},
|
|
830
|
+
control,
|
|
831
|
+
adapter,
|
|
832
|
+
contractSchema
|
|
833
|
+
}, `${field.id}-value-${entryIndex}`);
|
|
834
|
+
}, [
|
|
835
|
+
field.id,
|
|
836
|
+
field.readOnly,
|
|
837
|
+
control,
|
|
838
|
+
adapter,
|
|
839
|
+
contractSchema
|
|
840
|
+
]);
|
|
841
|
+
if (!useShouldRenderField(field, control)) return null;
|
|
842
|
+
const FieldComponent = fieldComponents[field.type];
|
|
843
|
+
if (!FieldComponent) {
|
|
844
|
+
logger.warn("DynamicFormField", `No component registered for field type: ${field.type}`);
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
const enhancedProps = {
|
|
848
|
+
...getFieldSpecificProps(field, {
|
|
849
|
+
renderPayloadField,
|
|
850
|
+
renderKeyField,
|
|
851
|
+
renderValueField
|
|
852
|
+
}, contractSchema),
|
|
853
|
+
...field.type === "array" && { renderElement: (elementField, index) => /* @__PURE__ */ jsx(DynamicFormField, {
|
|
854
|
+
field: {
|
|
855
|
+
...elementField,
|
|
856
|
+
readOnly: elementField.readOnly ?? field.readOnly
|
|
857
|
+
},
|
|
858
|
+
control,
|
|
859
|
+
adapter,
|
|
860
|
+
contractSchema
|
|
861
|
+
}, `${field.id}-element-${index}`) },
|
|
862
|
+
...field.type === "object" && { renderProperty: (propertyField, propertyName) => /* @__PURE__ */ jsx(DynamicFormField, {
|
|
863
|
+
field: {
|
|
864
|
+
...propertyField,
|
|
865
|
+
readOnly: propertyField.readOnly ?? field.readOnly
|
|
866
|
+
},
|
|
867
|
+
control,
|
|
868
|
+
adapter,
|
|
869
|
+
contractSchema
|
|
870
|
+
}, `${field.id}-property-${propertyName}`) },
|
|
871
|
+
...field.type === "array-object" && { renderProperty: (propertyField, itemIndex, propertyName) => /* @__PURE__ */ jsx(DynamicFormField, {
|
|
872
|
+
field: {
|
|
873
|
+
...propertyField,
|
|
874
|
+
readOnly: propertyField.readOnly ?? field.readOnly
|
|
875
|
+
},
|
|
876
|
+
control,
|
|
877
|
+
adapter,
|
|
878
|
+
contractSchema
|
|
879
|
+
}, `${field.id}-item-${itemIndex}-property-${propertyName}`) }
|
|
880
|
+
};
|
|
881
|
+
return /* @__PURE__ */ jsx(FieldComponent, {
|
|
882
|
+
id: field.id,
|
|
883
|
+
label: field.label,
|
|
884
|
+
placeholder: field.placeholder,
|
|
885
|
+
helperText: field.helperText,
|
|
886
|
+
width: field.width,
|
|
887
|
+
validation: field.validation,
|
|
888
|
+
control,
|
|
889
|
+
name: field.name,
|
|
890
|
+
adapter,
|
|
891
|
+
readOnly: field.readOnly,
|
|
892
|
+
contractSchema,
|
|
893
|
+
...enhancedProps
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Extract field-specific props based on field type
|
|
898
|
+
*/
|
|
899
|
+
function getFieldSpecificProps(field, renderFunctions, contractSchema) {
|
|
900
|
+
switch (field.type) {
|
|
901
|
+
case "number": return {
|
|
902
|
+
min: field.validation?.min,
|
|
903
|
+
max: field.validation?.max,
|
|
904
|
+
step: field.options?.find((opt) => opt.label === "step")?.value
|
|
905
|
+
};
|
|
906
|
+
case "array": return {
|
|
907
|
+
elementType: field.elementType || "text",
|
|
908
|
+
minItems: field.validation?.min,
|
|
909
|
+
maxItems: field.validation?.max,
|
|
910
|
+
elementFieldConfig: field.elementFieldConfig
|
|
911
|
+
};
|
|
912
|
+
case "object": return {
|
|
913
|
+
components: field.components || [],
|
|
914
|
+
showCard: true,
|
|
915
|
+
contractSchema
|
|
916
|
+
};
|
|
917
|
+
case "array-object": return {
|
|
918
|
+
components: field.components || [],
|
|
919
|
+
minItems: field.validation?.min,
|
|
920
|
+
maxItems: field.validation?.max,
|
|
921
|
+
collapsible: true,
|
|
922
|
+
defaultCollapsed: false
|
|
923
|
+
};
|
|
924
|
+
case "blockchain-address": return {};
|
|
925
|
+
case "checkbox": return {};
|
|
926
|
+
case "code-editor": return {
|
|
927
|
+
language: field.codeEditorProps?.language || "json",
|
|
928
|
+
theme: field.codeEditorProps?.theme || "light",
|
|
929
|
+
height: field.codeEditorProps?.height || "200px",
|
|
930
|
+
maxHeight: field.codeEditorProps?.maxHeight || "400px",
|
|
931
|
+
performanceThreshold: field.codeEditorProps?.performanceThreshold || 5e3
|
|
932
|
+
};
|
|
933
|
+
case "enum": return {
|
|
934
|
+
enumMetadata: field.enumMetadata,
|
|
935
|
+
renderPayloadField: renderFunctions.renderPayloadField
|
|
936
|
+
};
|
|
937
|
+
case "select": return {
|
|
938
|
+
options: field.options || [],
|
|
939
|
+
defaultValue: field.defaultValue || void 0
|
|
940
|
+
};
|
|
941
|
+
case "radio": return { options: field.options || [] };
|
|
942
|
+
case "map": return {
|
|
943
|
+
mapMetadata: field.mapMetadata,
|
|
944
|
+
minItems: field.validation?.min,
|
|
945
|
+
renderKeyField: renderFunctions.renderKeyField,
|
|
946
|
+
renderValueField: renderFunctions.renderValueField
|
|
947
|
+
};
|
|
948
|
+
default: return {};
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
//#endregion
|
|
953
|
+
//#region src/components/transaction/TransactionHashDisplay.tsx
|
|
954
|
+
/**
|
|
955
|
+
* Renders a transaction hash with proper formatting and optional explorer link.
|
|
956
|
+
* This component ensures transaction hashes are displayed in a consistent way
|
|
957
|
+
* with proper word-breaking for long strings.
|
|
958
|
+
*/
|
|
959
|
+
function TransactionHashDisplay({ txHash, explorerUrl }) {
|
|
960
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
961
|
+
className: "text-sm mt-2 relative",
|
|
962
|
+
children: [
|
|
963
|
+
/* @__PURE__ */ jsx("div", {
|
|
964
|
+
className: "text-muted-foreground text-xs mb-1",
|
|
965
|
+
children: "Transaction:"
|
|
966
|
+
}),
|
|
967
|
+
/* @__PURE__ */ jsx("div", {
|
|
968
|
+
className: "mb-2",
|
|
969
|
+
children: /* @__PURE__ */ jsx("code", {
|
|
970
|
+
className: "font-mono text-xs bg-gray-100 px-2 py-1.5 rounded break-all inline-block",
|
|
971
|
+
children: txHash
|
|
972
|
+
})
|
|
973
|
+
}),
|
|
974
|
+
explorerUrl && /* @__PURE__ */ jsx("div", {
|
|
975
|
+
className: "relative z-10 pointer-events-auto",
|
|
976
|
+
children: /* @__PURE__ */ jsxs("a", {
|
|
977
|
+
href: explorerUrl,
|
|
978
|
+
target: "_blank",
|
|
979
|
+
rel: "noopener noreferrer",
|
|
980
|
+
className: "inline-flex items-center text-xs text-primary hover:text-primary/80 hover:underline py-1 px-2 cursor-pointer",
|
|
981
|
+
onClick: () => {
|
|
982
|
+
logger.info("TransactionHashDisplay", "Explorer link clicked", explorerUrl);
|
|
983
|
+
},
|
|
984
|
+
children: [/* @__PURE__ */ jsx(ExternalLink, {
|
|
985
|
+
size: 12,
|
|
986
|
+
className: "mr-1 flex-shrink-0"
|
|
987
|
+
}), /* @__PURE__ */ jsx("span", { children: "View on explorer" })]
|
|
988
|
+
})
|
|
989
|
+
})
|
|
990
|
+
]
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
//#endregion
|
|
995
|
+
//#region src/components/transaction/TransactionStatusDisplay.tsx
|
|
996
|
+
/**
|
|
997
|
+
* Helper function to format error messages that contain transaction hashes
|
|
998
|
+
* This adds proper word breaking for transaction hashes while maintaining readability
|
|
999
|
+
*/
|
|
1000
|
+
function formatErrorWithHash(errorMsg) {
|
|
1001
|
+
if (!errorMsg) return "An unknown error occurred.";
|
|
1002
|
+
const hashRegex = /(0x[a-fA-F0-9]{40,})/g;
|
|
1003
|
+
if (!hashRegex.test(errorMsg)) return /* @__PURE__ */ jsx("span", {
|
|
1004
|
+
className: "break-word",
|
|
1005
|
+
children: errorMsg
|
|
1006
|
+
});
|
|
1007
|
+
return /* @__PURE__ */ jsx("span", {
|
|
1008
|
+
className: "break-word",
|
|
1009
|
+
children: errorMsg.split(hashRegex).map((part, i) => {
|
|
1010
|
+
if (part.match(/^0x[a-fA-F0-9]{40,}$/)) return /* @__PURE__ */ jsx("code", {
|
|
1011
|
+
className: "font-mono px-1 py-0.5 bg-gray-100 rounded text-xs break-all",
|
|
1012
|
+
children: part
|
|
1013
|
+
}, i);
|
|
1014
|
+
return part;
|
|
1015
|
+
})
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
/** Displays the current status of a transaction with appropriate icons and messages. */
|
|
1019
|
+
function TransactionStatusDisplay({ status, txHash, error, explorerUrl, className, customTitle, customMessage, result, functionDetails, adapter }) {
|
|
1020
|
+
if (status === "idle") return null;
|
|
1021
|
+
let variant = "default";
|
|
1022
|
+
let defaultTitle = "";
|
|
1023
|
+
let defaultMessage = null;
|
|
1024
|
+
let icon = null;
|
|
1025
|
+
if (status === "pendingSignature") {
|
|
1026
|
+
defaultTitle = "Pending Signature";
|
|
1027
|
+
icon = /* @__PURE__ */ jsx(Loader2, { className: "size-5 animate-spin text-primary" });
|
|
1028
|
+
defaultMessage = "Please check your wallet to sign. After signing, your transaction will be submitted and confirmed automatically.";
|
|
1029
|
+
variant = "default";
|
|
1030
|
+
} else if (status === "pendingConfirmation") {
|
|
1031
|
+
defaultTitle = "Processing Transaction";
|
|
1032
|
+
icon = /* @__PURE__ */ jsx(Loader2, { className: "size-5 animate-spin text-primary" });
|
|
1033
|
+
defaultMessage = "Waiting for the transaction to be confirmed on the blockchain...";
|
|
1034
|
+
variant = "default";
|
|
1035
|
+
} else if (status === "pendingRelayer") {
|
|
1036
|
+
defaultTitle = "Waiting for Relayer";
|
|
1037
|
+
icon = /* @__PURE__ */ jsx(Loader2, { className: "size-5 animate-spin text-primary" });
|
|
1038
|
+
defaultMessage = "The transaction is pending with the relayer and will be submitted shortly.";
|
|
1039
|
+
variant = "default";
|
|
1040
|
+
} else if (status === "success") {
|
|
1041
|
+
defaultTitle = "Transaction Successful";
|
|
1042
|
+
icon = /* @__PURE__ */ jsx(CheckCircle, { className: "size-5 text-green-600" });
|
|
1043
|
+
defaultMessage = "Your transaction has been confirmed.";
|
|
1044
|
+
variant = "success";
|
|
1045
|
+
} else if (status === "error") {
|
|
1046
|
+
defaultTitle = "Transaction Failed";
|
|
1047
|
+
icon = /* @__PURE__ */ jsx(AlertCircle, { className: "size-5 text-destructive" });
|
|
1048
|
+
variant = "destructive";
|
|
1049
|
+
}
|
|
1050
|
+
const title = customTitle || defaultTitle;
|
|
1051
|
+
let formattedResult = null;
|
|
1052
|
+
if (result !== void 0 && result !== null && adapter && functionDetails) try {
|
|
1053
|
+
formattedResult = adapter.formatFunctionResult(result, functionDetails);
|
|
1054
|
+
} catch {
|
|
1055
|
+
formattedResult = JSON.stringify(result, null, 2);
|
|
1056
|
+
}
|
|
1057
|
+
else if (result !== void 0 && result !== null) formattedResult = JSON.stringify(result, null, 2);
|
|
1058
|
+
let content = null;
|
|
1059
|
+
if (status === "error") content = /* @__PURE__ */ jsxs("div", { children: [error ? formatErrorWithHash(error) : customMessage ? /* @__PURE__ */ jsx("span", {
|
|
1060
|
+
className: "break-word",
|
|
1061
|
+
children: customMessage
|
|
1062
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
1063
|
+
className: "break-word",
|
|
1064
|
+
children: "An unknown error occurred."
|
|
1065
|
+
}), txHash && /* @__PURE__ */ jsx(TransactionHashDisplay, {
|
|
1066
|
+
txHash,
|
|
1067
|
+
explorerUrl: explorerUrl || null
|
|
1068
|
+
})] });
|
|
1069
|
+
else {
|
|
1070
|
+
const messageText = customMessage || defaultMessage || "";
|
|
1071
|
+
content = /* @__PURE__ */ jsxs("div", {
|
|
1072
|
+
className: "space-y-3",
|
|
1073
|
+
children: [
|
|
1074
|
+
messageText && /* @__PURE__ */ jsx("p", { children: messageText }),
|
|
1075
|
+
txHash && /* @__PURE__ */ jsx(TransactionHashDisplay, {
|
|
1076
|
+
txHash,
|
|
1077
|
+
explorerUrl: explorerUrl || null
|
|
1078
|
+
}),
|
|
1079
|
+
formattedResult && /* @__PURE__ */ jsxs("div", {
|
|
1080
|
+
className: "mt-3 pt-3 border-t border-border",
|
|
1081
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1082
|
+
className: "text-sm font-medium mb-2",
|
|
1083
|
+
children: "Result:"
|
|
1084
|
+
}), /* @__PURE__ */ jsx("pre", {
|
|
1085
|
+
className: "text-xs bg-muted p-3 rounded-md overflow-auto max-h-48",
|
|
1086
|
+
children: formattedResult
|
|
1087
|
+
})]
|
|
1088
|
+
})
|
|
1089
|
+
]
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
return /* @__PURE__ */ jsx(Alert, {
|
|
1093
|
+
variant,
|
|
1094
|
+
className: cn("relative py-4 px-5 overflow-hidden", className),
|
|
1095
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1096
|
+
className: "flex items-start",
|
|
1097
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1098
|
+
className: "shrink-0 mr-3 mt-0.5",
|
|
1099
|
+
children: icon
|
|
1100
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1101
|
+
className: "flex-1 min-w-0",
|
|
1102
|
+
children: [/* @__PURE__ */ jsx(AlertTitle, {
|
|
1103
|
+
className: "mb-1 text-base font-medium",
|
|
1104
|
+
children: title
|
|
1105
|
+
}), /* @__PURE__ */ jsx(AlertDescription, {
|
|
1106
|
+
className: "text-sm overflow-hidden",
|
|
1107
|
+
children: content
|
|
1108
|
+
})]
|
|
1109
|
+
})]
|
|
1110
|
+
})
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
//#endregion
|
|
1115
|
+
//#region src/components/TransactionForm.tsx
|
|
1116
|
+
/**
|
|
1117
|
+
* Transaction states that should disable the form (pending operations)
|
|
1118
|
+
*/
|
|
1119
|
+
const PENDING_STATES = [
|
|
1120
|
+
"pendingSignature",
|
|
1121
|
+
"pendingConfirmation",
|
|
1122
|
+
"pendingRelayer"
|
|
1123
|
+
];
|
|
1124
|
+
/**
|
|
1125
|
+
* Transaction Form Component
|
|
1126
|
+
*
|
|
1127
|
+
* This is the main entry point for the app rendering system. It represents the top level of
|
|
1128
|
+
* the app rendering architecture:
|
|
1129
|
+
*
|
|
1130
|
+
* 1. TransactionForm receives a schema and adapter from the transaction builder app
|
|
1131
|
+
* 2. It sets up React Hook Form for state management and validation
|
|
1132
|
+
* 3. It renders fields dynamically using the DynamicFormField component
|
|
1133
|
+
* 4. Provides wallet connection UI (demo implementation)
|
|
1134
|
+
* 5. On submission, it processes data through the adapter before passing to handlers
|
|
1135
|
+
*
|
|
1136
|
+
* Note: The previewMode prop is currently used only for demo purposes and does not affect
|
|
1137
|
+
* the visibility of wallet connection or transaction execution UI. In the future, it will be used
|
|
1138
|
+
* to enable/disable actual blockchain interactions without changing the UI structure.
|
|
1139
|
+
*
|
|
1140
|
+
* @returns The rendered form component
|
|
1141
|
+
*/
|
|
1142
|
+
function TransactionForm({ schema, contractSchema, adapter, isWalletConnected = false, executionConfig }) {
|
|
1143
|
+
const [formError, setFormError] = useState(null);
|
|
1144
|
+
const [executionConfigError, setExecutionConfigError] = useState(null);
|
|
1145
|
+
const [runtimeApiKey, setRuntimeApiKey] = useState("");
|
|
1146
|
+
const [txStatus, setTxStatus] = useState("idle");
|
|
1147
|
+
const [txHash, setTxHash] = useState(null);
|
|
1148
|
+
const [txError, setTxError] = useState(null);
|
|
1149
|
+
const [txStatusDetails, setTxStatusDetails] = useState(null);
|
|
1150
|
+
const [txResult, setTxResult] = useState(null);
|
|
1151
|
+
const networkConfig = adapter.networkConfig;
|
|
1152
|
+
const methods = useForm({
|
|
1153
|
+
mode: schema.validation?.mode || "onChange",
|
|
1154
|
+
defaultValues: createDefaultFormValues(schema.fields, schema.defaultValues)
|
|
1155
|
+
});
|
|
1156
|
+
const { isValid } = methods.formState;
|
|
1157
|
+
const currentFunction = contractSchema?.functions.find((fn) => fn.id === schema.functionId);
|
|
1158
|
+
const canExecuteLocally = currentFunction?.stateMutability === "pure";
|
|
1159
|
+
useEffect(() => {
|
|
1160
|
+
methods.reset(createDefaultFormValues(schema.fields, schema.defaultValues));
|
|
1161
|
+
setTxStatus("idle");
|
|
1162
|
+
setTxHash(null);
|
|
1163
|
+
setTxError(null);
|
|
1164
|
+
setFormError(null);
|
|
1165
|
+
setExecutionConfigError(null);
|
|
1166
|
+
setTxStatusDetails(null);
|
|
1167
|
+
setTxResult(null);
|
|
1168
|
+
}, [schema, methods]);
|
|
1169
|
+
useEffect(() => {
|
|
1170
|
+
const validateExecConfig = async () => {
|
|
1171
|
+
if (executionConfig && adapter && adapter.validateExecutionConfig) try {
|
|
1172
|
+
logger.info("TransactionForm", "Re-validating execution config:", executionConfig);
|
|
1173
|
+
const validationResult = await adapter.validateExecutionConfig(executionConfig);
|
|
1174
|
+
if (typeof validationResult === "string") setExecutionConfigError(`Execution Configuration Error: ${validationResult}`);
|
|
1175
|
+
else setExecutionConfigError(null);
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
logger.error("TransactionForm", "Error reactive exec config validation:", error);
|
|
1178
|
+
setExecutionConfigError(`Exec Config Validation Failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1179
|
+
}
|
|
1180
|
+
else setExecutionConfigError(null);
|
|
1181
|
+
};
|
|
1182
|
+
validateExecConfig();
|
|
1183
|
+
}, [
|
|
1184
|
+
executionConfig,
|
|
1185
|
+
adapter,
|
|
1186
|
+
isWalletConnected
|
|
1187
|
+
]);
|
|
1188
|
+
const executeTransaction = async (data) => {
|
|
1189
|
+
logger.info("TransactionForm", "Internal form submission attempt", data);
|
|
1190
|
+
setTxStatus("idle");
|
|
1191
|
+
setTxHash(null);
|
|
1192
|
+
setTxError(null);
|
|
1193
|
+
setTxStatusDetails(null);
|
|
1194
|
+
setTxResult(null);
|
|
1195
|
+
if (!adapter) {
|
|
1196
|
+
logger.error("TransactionForm", "Adapter not provided.");
|
|
1197
|
+
setTxError("Configuration error: Adapter not available.");
|
|
1198
|
+
setTxStatus("error");
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
if (!canExecuteLocally && !isWalletConnected) {
|
|
1202
|
+
logger.warn("TransactionForm", "Wallet not connected for submission.");
|
|
1203
|
+
setTxError("Please connect your wallet to submit the transaction.");
|
|
1204
|
+
setTxStatus("error");
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
try {
|
|
1208
|
+
const { contractArgs, runtimeSecrets } = extractRuntimeSecrets(data, schema.fields);
|
|
1209
|
+
const formattedData = adapter.formatTransactionData(contractSchema, schema.functionId, contractArgs, schema.fields);
|
|
1210
|
+
logger.info("TransactionForm", "Formatted transaction data:", formattedData);
|
|
1211
|
+
const onStatusChange = (status, details) => {
|
|
1212
|
+
logger.info("TransactionForm", `Status Update: ${status}`, details);
|
|
1213
|
+
setTxStatus(status);
|
|
1214
|
+
setTxStatusDetails(details);
|
|
1215
|
+
if (details.transactionId) setTxHash(details.transactionId);
|
|
1216
|
+
if (details.txHash) setTxHash(details.txHash);
|
|
1217
|
+
};
|
|
1218
|
+
const firstSecretValue = Object.values(runtimeSecrets)[0];
|
|
1219
|
+
const { txHash: finalTxHash, result } = await adapter.signAndBroadcast(formattedData, executionConfig || {
|
|
1220
|
+
method: "eoa",
|
|
1221
|
+
allowAny: true
|
|
1222
|
+
}, onStatusChange, runtimeApiKey, firstSecretValue);
|
|
1223
|
+
logger.info("TransactionForm", `Transaction submitted with final hash: ${finalTxHash}`);
|
|
1224
|
+
setTxHash(finalTxHash);
|
|
1225
|
+
if (result !== void 0) {
|
|
1226
|
+
setTxResult(result);
|
|
1227
|
+
logger.info("TransactionForm", "Execution result received:", result);
|
|
1228
|
+
}
|
|
1229
|
+
if (canExecuteLocally) {
|
|
1230
|
+
setTxStatus("success");
|
|
1231
|
+
setTxError(null);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
if (adapter.waitForTransactionConfirmation) {
|
|
1235
|
+
setTxStatus("pendingConfirmation");
|
|
1236
|
+
logger.info("TransactionForm", `Waiting for confirmation for tx: ${finalTxHash}`);
|
|
1237
|
+
const confirmationResult = await adapter.waitForTransactionConfirmation(finalTxHash);
|
|
1238
|
+
if (confirmationResult.status === "success") {
|
|
1239
|
+
logger.info("TransactionForm", `Transaction confirmed: ${finalTxHash}`, confirmationResult.receipt);
|
|
1240
|
+
setTxStatus("success");
|
|
1241
|
+
setTxError(null);
|
|
1242
|
+
} else {
|
|
1243
|
+
logger.error("TransactionForm", `Transaction failed confirmation: ${finalTxHash}`, confirmationResult.error);
|
|
1244
|
+
setTxError(confirmationResult.error?.message ?? "Transaction failed during confirmation.");
|
|
1245
|
+
setTxStatus("error");
|
|
1246
|
+
}
|
|
1247
|
+
} else {
|
|
1248
|
+
logger.warn("TransactionForm", "Adapter does not support waitForTransactionConfirmation. Marking as success after submission.");
|
|
1249
|
+
setTxStatus("success");
|
|
1250
|
+
setTxError(null);
|
|
1251
|
+
}
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
logger.error("TransactionForm", "Transaction error during submission process:", error);
|
|
1254
|
+
setTxError(error instanceof Error ? error.message : "An unknown error occurred.");
|
|
1255
|
+
setTxStatus("error");
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
const renderFormContent = () => {
|
|
1259
|
+
if (!schema.fields || schema.fields.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
1260
|
+
className: "form-empty-state",
|
|
1261
|
+
children: "No fields defined in schema"
|
|
1262
|
+
});
|
|
1263
|
+
const { errors } = methods.formState;
|
|
1264
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1265
|
+
className: "form-fields-container space-y-4",
|
|
1266
|
+
children: schema.fields.map((field) => /* @__PURE__ */ jsx(DynamicFormField, {
|
|
1267
|
+
field,
|
|
1268
|
+
control: methods.control,
|
|
1269
|
+
error: errors[field.name]?.message,
|
|
1270
|
+
adapter,
|
|
1271
|
+
contractSchema
|
|
1272
|
+
}, field.id))
|
|
1273
|
+
});
|
|
1274
|
+
};
|
|
1275
|
+
const getLayoutClasses = () => {
|
|
1276
|
+
return "grid grid-cols-1 gap-4";
|
|
1277
|
+
};
|
|
1278
|
+
const getButtonVariant = () => {
|
|
1279
|
+
const { submitButton } = schema;
|
|
1280
|
+
if (!submitButton?.variant) return "default";
|
|
1281
|
+
switch (submitButton.variant) {
|
|
1282
|
+
case "primary": return "default";
|
|
1283
|
+
case "secondary": return "secondary";
|
|
1284
|
+
case "outline": return "outline";
|
|
1285
|
+
default: return "default";
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
const getExplorerTxUrl = (hash) => {
|
|
1289
|
+
if (!adapter || !hash || !networkConfig) return null;
|
|
1290
|
+
if (adapter.getExplorerTxUrl) return adapter.getExplorerTxUrl(hash);
|
|
1291
|
+
logger.warn("TransactionForm", "getExplorerTxUrl not implemented by adapter, trying getExplorerUrl as fallback (might expect address).");
|
|
1292
|
+
return adapter.getExplorerUrl ? adapter.getExplorerUrl(hash) : null;
|
|
1293
|
+
};
|
|
1294
|
+
return /* @__PURE__ */ jsxs(FormProvider, {
|
|
1295
|
+
...methods,
|
|
1296
|
+
children: [
|
|
1297
|
+
/* @__PURE__ */ jsx("div", {
|
|
1298
|
+
className: "mb-4 flex items-center justify-between",
|
|
1299
|
+
children: schema.title && /* @__PURE__ */ jsx("h2", {
|
|
1300
|
+
className: "text-xl font-bold",
|
|
1301
|
+
children: schema.title
|
|
1302
|
+
})
|
|
1303
|
+
}),
|
|
1304
|
+
/* @__PURE__ */ jsx("div", {
|
|
1305
|
+
className: "description-container mb-6",
|
|
1306
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1307
|
+
className: "text-muted-foreground rounded-md border border-gray-100 bg-gray-50 p-3 text-sm",
|
|
1308
|
+
children: schema.description || "No description provided."
|
|
1309
|
+
})
|
|
1310
|
+
}),
|
|
1311
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1312
|
+
className: "flex flex-col space-y-4",
|
|
1313
|
+
children: [
|
|
1314
|
+
formError && /* @__PURE__ */ jsx("div", {
|
|
1315
|
+
className: "form-error rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700",
|
|
1316
|
+
children: formError
|
|
1317
|
+
}),
|
|
1318
|
+
txStatus !== "idle" && /* @__PURE__ */ jsx("div", {
|
|
1319
|
+
className: "mb-8 pointer-events-auto",
|
|
1320
|
+
children: /* @__PURE__ */ jsx(TransactionStatusDisplay, {
|
|
1321
|
+
status: txStatus,
|
|
1322
|
+
txHash,
|
|
1323
|
+
error: txError,
|
|
1324
|
+
explorerUrl: txHash ? getExplorerTxUrl(txHash) : null,
|
|
1325
|
+
customTitle: txStatusDetails?.title,
|
|
1326
|
+
customMessage: txStatusDetails?.message,
|
|
1327
|
+
result: txResult,
|
|
1328
|
+
functionDetails: currentFunction,
|
|
1329
|
+
adapter
|
|
1330
|
+
})
|
|
1331
|
+
}),
|
|
1332
|
+
/* @__PURE__ */ jsxs("form", {
|
|
1333
|
+
className: `transaction-form flex flex-col ${getLayoutClasses()} ${PENDING_STATES.includes(txStatus) ? "opacity-70 pointer-events-none" : ""}`,
|
|
1334
|
+
noValidate: true,
|
|
1335
|
+
onSubmit: methods.handleSubmit(executeTransaction),
|
|
1336
|
+
children: [
|
|
1337
|
+
executionConfigError && /* @__PURE__ */ jsxs("div", {
|
|
1338
|
+
className: "form-error rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700",
|
|
1339
|
+
children: [/* @__PURE__ */ jsx(AlertCircle, { className: "mr-2 h-4 w-4" }), executionConfigError]
|
|
1340
|
+
}),
|
|
1341
|
+
/* @__PURE__ */ jsx("div", {
|
|
1342
|
+
className: "mb-6",
|
|
1343
|
+
children: renderFormContent()
|
|
1344
|
+
}),
|
|
1345
|
+
executionConfig && /* @__PURE__ */ jsx("div", {
|
|
1346
|
+
className: "w-full",
|
|
1347
|
+
children: /* @__PURE__ */ jsx(ExecutionConfigDisplay, {
|
|
1348
|
+
executionConfig,
|
|
1349
|
+
adapter,
|
|
1350
|
+
error: executionConfigError,
|
|
1351
|
+
onRuntimeApiKeyChange: setRuntimeApiKey
|
|
1352
|
+
})
|
|
1353
|
+
}),
|
|
1354
|
+
/* @__PURE__ */ jsx("div", {
|
|
1355
|
+
className: "mt-4 border-t border-gray-100 pt-4",
|
|
1356
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1357
|
+
className: "flex justify-end items-center gap-2",
|
|
1358
|
+
children: /* @__PURE__ */ jsx(TransactionExecuteButton, {
|
|
1359
|
+
isWalletConnected,
|
|
1360
|
+
isSubmitting: txStatus === "pendingSignature" || txStatus === "pendingConfirmation",
|
|
1361
|
+
isFormValid: isValid && executionConfigError === null,
|
|
1362
|
+
variant: getButtonVariant(),
|
|
1363
|
+
functionDetails: currentFunction,
|
|
1364
|
+
canExecuteLocally
|
|
1365
|
+
})
|
|
1366
|
+
})
|
|
1367
|
+
})
|
|
1368
|
+
]
|
|
1369
|
+
})
|
|
1370
|
+
]
|
|
1371
|
+
})
|
|
1372
|
+
]
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
//#endregion
|
|
1377
|
+
//#region src/components/ContractStateWidget/components/FunctionResult.tsx
|
|
1378
|
+
/**
|
|
1379
|
+
* Component for displaying formatted function results (strings)
|
|
1380
|
+
*/
|
|
1381
|
+
function FunctionResult({ functionDetails, result, loading }) {
|
|
1382
|
+
const formattedResult = result ?? "";
|
|
1383
|
+
const hasResult = typeof result === "string";
|
|
1384
|
+
const isError = hasResult && (result.startsWith("Error:") || result.startsWith("[Error:"));
|
|
1385
|
+
const headerRef = useRef(null);
|
|
1386
|
+
const [fontSize, setFontSize] = useState("xs");
|
|
1387
|
+
const hasScaledDownRef = useRef(false);
|
|
1388
|
+
const lastContentRef = useRef("");
|
|
1389
|
+
const outputs = useMemo(() => functionDetails.outputs || [], [functionDetails.outputs]);
|
|
1390
|
+
const currentContent = `${functionDetails.name}-${outputs.map((o) => o.type).join(",")}`;
|
|
1391
|
+
useEffect(() => {
|
|
1392
|
+
if (lastContentRef.current !== currentContent) {
|
|
1393
|
+
lastContentRef.current = currentContent;
|
|
1394
|
+
hasScaledDownRef.current = false;
|
|
1395
|
+
setFontSize("xs");
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const checkOverflow = () => {
|
|
1399
|
+
if (!headerRef.current) return;
|
|
1400
|
+
const container = headerRef.current;
|
|
1401
|
+
if (container.scrollWidth > container.clientWidth && fontSize === "xs" && !hasScaledDownRef.current) {
|
|
1402
|
+
hasScaledDownRef.current = true;
|
|
1403
|
+
setFontSize("2xs");
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
const timeoutId = setTimeout(checkOverflow, 10);
|
|
1407
|
+
return () => {
|
|
1408
|
+
clearTimeout(timeoutId);
|
|
1409
|
+
};
|
|
1410
|
+
}, [
|
|
1411
|
+
functionDetails.name,
|
|
1412
|
+
outputs,
|
|
1413
|
+
fontSize,
|
|
1414
|
+
currentContent
|
|
1415
|
+
]);
|
|
1416
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1417
|
+
className: "border rounded-sm p-2",
|
|
1418
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1419
|
+
ref: headerRef,
|
|
1420
|
+
className: cn("font-medium mb-1 flex flex-wrap items-baseline gap-x-2 gap-y-1 leading-tight overflow-hidden", fontSize === "xs" ? "text-xs" : "text-[10px]"),
|
|
1421
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1422
|
+
className: "flex-shrink-0 min-w-0 break-all",
|
|
1423
|
+
children: functionDetails.name
|
|
1424
|
+
}), outputs.length > 0 && /* @__PURE__ */ jsx("span", {
|
|
1425
|
+
className: "text-muted-foreground flex-shrink-0 whitespace-nowrap",
|
|
1426
|
+
children: `→ ${outputs.map((o) => o.type).join(", ")}`
|
|
1427
|
+
})]
|
|
1428
|
+
}), loading ? /* @__PURE__ */ jsx("div", {
|
|
1429
|
+
className: "text-xs text-muted-foreground italic animate-pulse",
|
|
1430
|
+
children: "Loading..."
|
|
1431
|
+
}) : hasResult ? /* @__PURE__ */ jsx("pre", {
|
|
1432
|
+
className: cn("text-xs p-1 max-h-24 bg-muted overflow-auto rounded whitespace-pre-wrap break-all", "animate-fade-in", isError && "text-destructive"),
|
|
1433
|
+
children: formattedResult
|
|
1434
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
1435
|
+
className: "text-xs text-muted-foreground italic",
|
|
1436
|
+
children: "Click Refresh to fetch result"
|
|
1437
|
+
})]
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
//#endregion
|
|
1442
|
+
//#region src/components/ContractStateWidget/components/ViewFunctionsPanel.tsx
|
|
1443
|
+
/**
|
|
1444
|
+
* Panel for displaying and querying simple view functions (functions without parameters)
|
|
1445
|
+
*/
|
|
1446
|
+
function ViewFunctionsPanel({ functions, contractAddress, adapter, contractSchema, className }) {
|
|
1447
|
+
const safeFunctions = adapter.filterAutoQueryableFunctions ? adapter.filterAutoQueryableFunctions(functions) : functions;
|
|
1448
|
+
const [results, setResults] = useState({});
|
|
1449
|
+
const [loadingStates, setLoadingStates] = useState({});
|
|
1450
|
+
const [hasQueried, setHasQueried] = useState(false);
|
|
1451
|
+
const [isQueryInProgress, setIsQueryInProgress] = useState(false);
|
|
1452
|
+
const handleQueryAll = useCallback(async () => {
|
|
1453
|
+
if (safeFunctions.length === 0) return;
|
|
1454
|
+
if (isQueryInProgress) {
|
|
1455
|
+
logger.info("ViewFunctionsPanel", "Query already in progress, skipping...");
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
setIsQueryInProgress(true);
|
|
1459
|
+
const initialLoadingStates = {};
|
|
1460
|
+
safeFunctions.forEach((func) => {
|
|
1461
|
+
initialLoadingStates[func.id] = true;
|
|
1462
|
+
});
|
|
1463
|
+
setLoadingStates(initialLoadingStates);
|
|
1464
|
+
try {
|
|
1465
|
+
await rateLimitedBatch(safeFunctions.map((func) => async () => {
|
|
1466
|
+
try {
|
|
1467
|
+
const result = await adapter.queryViewFunction(contractAddress, func.id, [], contractSchema);
|
|
1468
|
+
let formattedResult;
|
|
1469
|
+
try {
|
|
1470
|
+
formattedResult = adapter.formatFunctionResult(result, func);
|
|
1471
|
+
} catch (formatError) {
|
|
1472
|
+
logger.error("ViewFunctionsPanel", `Error formatting result for ${func.name}:`, formatError);
|
|
1473
|
+
formattedResult = `Error formatting result: ${formatError instanceof Error ? formatError.message : "Unknown error"}`;
|
|
1474
|
+
}
|
|
1475
|
+
setResults((prev) => ({
|
|
1476
|
+
...prev,
|
|
1477
|
+
[func.id]: formattedResult
|
|
1478
|
+
}));
|
|
1479
|
+
setLoadingStates((prev) => ({
|
|
1480
|
+
...prev,
|
|
1481
|
+
[func.id]: false
|
|
1482
|
+
}));
|
|
1483
|
+
return {
|
|
1484
|
+
funcId: func.id,
|
|
1485
|
+
success: true
|
|
1486
|
+
};
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
logger.error("ViewFunctionsPanel", `Error calling ${func.name}:`, err);
|
|
1489
|
+
const errorMessage = `Error: ${err instanceof Error ? err.message : "Unknown error"}`;
|
|
1490
|
+
setResults((prev) => ({
|
|
1491
|
+
...prev,
|
|
1492
|
+
[func.id]: errorMessage
|
|
1493
|
+
}));
|
|
1494
|
+
setLoadingStates((prev) => ({
|
|
1495
|
+
...prev,
|
|
1496
|
+
[func.id]: false
|
|
1497
|
+
}));
|
|
1498
|
+
return {
|
|
1499
|
+
funcId: func.id,
|
|
1500
|
+
success: false
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
}), 1, 200);
|
|
1504
|
+
} catch (err) {
|
|
1505
|
+
logger.error("ViewFunctionsPanel", "Error querying functions:", err);
|
|
1506
|
+
setLoadingStates({});
|
|
1507
|
+
} finally {
|
|
1508
|
+
setHasQueried(true);
|
|
1509
|
+
setIsQueryInProgress(false);
|
|
1510
|
+
}
|
|
1511
|
+
}, [
|
|
1512
|
+
safeFunctions,
|
|
1513
|
+
contractAddress,
|
|
1514
|
+
adapter,
|
|
1515
|
+
contractSchema,
|
|
1516
|
+
isQueryInProgress
|
|
1517
|
+
]);
|
|
1518
|
+
useEffect(() => {
|
|
1519
|
+
let mounted = true;
|
|
1520
|
+
let timeoutId = null;
|
|
1521
|
+
const performInitialQuery = async () => {
|
|
1522
|
+
if (safeFunctions.length > 0 && mounted && !hasQueried && !isQueryInProgress) timeoutId = setTimeout(() => {
|
|
1523
|
+
if (mounted) handleQueryAll();
|
|
1524
|
+
}, 100);
|
|
1525
|
+
};
|
|
1526
|
+
performInitialQuery();
|
|
1527
|
+
return () => {
|
|
1528
|
+
mounted = false;
|
|
1529
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1530
|
+
};
|
|
1531
|
+
}, [
|
|
1532
|
+
safeFunctions.length,
|
|
1533
|
+
hasQueried,
|
|
1534
|
+
isQueryInProgress,
|
|
1535
|
+
handleQueryAll
|
|
1536
|
+
]);
|
|
1537
|
+
useEffect(() => {
|
|
1538
|
+
const networkId = adapter.networkConfig?.id;
|
|
1539
|
+
if (!networkId) return;
|
|
1540
|
+
return userRpcConfigService.subscribe(networkId, (event) => {
|
|
1541
|
+
logger.info("ViewFunctionsPanel", "RPC configuration changed:", event);
|
|
1542
|
+
handleQueryAll();
|
|
1543
|
+
});
|
|
1544
|
+
}, [adapter.networkConfig?.id, handleQueryAll]);
|
|
1545
|
+
if (safeFunctions.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
1546
|
+
className: "text-xs text-muted-foreground",
|
|
1547
|
+
children: "No simple view functions found in this contract."
|
|
1548
|
+
});
|
|
1549
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1550
|
+
className: cn("space-y-4", className),
|
|
1551
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1552
|
+
className: "flex items-center justify-between",
|
|
1553
|
+
children: [/* @__PURE__ */ jsx("h4", {
|
|
1554
|
+
className: "text-xs font-medium",
|
|
1555
|
+
children: "View Functions"
|
|
1556
|
+
}), /* @__PURE__ */ jsxs(Button, {
|
|
1557
|
+
onClick: () => {
|
|
1558
|
+
setHasQueried(false);
|
|
1559
|
+
handleQueryAll();
|
|
1560
|
+
},
|
|
1561
|
+
disabled: isQueryInProgress,
|
|
1562
|
+
size: "sm",
|
|
1563
|
+
variant: "outline",
|
|
1564
|
+
className: "h-6 w-6 p-0 rounded-full",
|
|
1565
|
+
title: "Refresh all view functions",
|
|
1566
|
+
children: [/* @__PURE__ */ jsx(RefreshCw, {
|
|
1567
|
+
size: 14,
|
|
1568
|
+
className: `${isQueryInProgress ? "animate-spin" : ""}`
|
|
1569
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1570
|
+
className: "sr-only",
|
|
1571
|
+
children: isQueryInProgress ? "Querying..." : "Refresh All"
|
|
1572
|
+
})]
|
|
1573
|
+
})]
|
|
1574
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1575
|
+
className: "space-y-2 overflow-y-auto pr-1 flex-grow min-h-0",
|
|
1576
|
+
children: safeFunctions.map((func) => /* @__PURE__ */ jsx(FunctionResult, {
|
|
1577
|
+
functionDetails: func,
|
|
1578
|
+
result: results[func.id],
|
|
1579
|
+
loading: loadingStates[func.id] || false
|
|
1580
|
+
}, func.id))
|
|
1581
|
+
})]
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
//#endregion
|
|
1586
|
+
//#region src/components/ContractStateWidget/ContractStateWidget.tsx
|
|
1587
|
+
/**
|
|
1588
|
+
* ContractStateWidget - Compact widget for displaying contract state
|
|
1589
|
+
* Shows contract state by allowing users to query simple view functions (no parameters)
|
|
1590
|
+
*/
|
|
1591
|
+
function ContractStateWidget({ contractSchema, contractAddress, adapter, isVisible = true, onToggle, className, error }) {
|
|
1592
|
+
const [viewFunctions, setViewFunctions] = useState([]);
|
|
1593
|
+
const [animationState, setAnimationState] = useState(isVisible ? "entered" : "exited");
|
|
1594
|
+
const networkConfig = adapter?.networkConfig;
|
|
1595
|
+
const [lastSchema, setLastSchema] = useState(contractSchema ?? null);
|
|
1596
|
+
useEffect(() => {
|
|
1597
|
+
if (contractSchema) setLastSchema(contractSchema);
|
|
1598
|
+
}, [contractSchema]);
|
|
1599
|
+
const effectiveSchema = useMemo(() => contractSchema ?? lastSchema, [contractSchema, lastSchema]);
|
|
1600
|
+
useEffect(() => {
|
|
1601
|
+
if (!effectiveSchema || !adapter) return;
|
|
1602
|
+
setViewFunctions(effectiveSchema.functions.filter((fn) => adapter.isViewFunction(fn)).filter((fn) => fn.inputs.length === 0));
|
|
1603
|
+
}, [effectiveSchema, adapter]);
|
|
1604
|
+
useEffect(() => {
|
|
1605
|
+
if (isVisible) {
|
|
1606
|
+
setAnimationState("entering");
|
|
1607
|
+
const timer = setTimeout(() => {
|
|
1608
|
+
setAnimationState("entered");
|
|
1609
|
+
}, 300);
|
|
1610
|
+
return () => clearTimeout(timer);
|
|
1611
|
+
} else {
|
|
1612
|
+
setAnimationState("exiting");
|
|
1613
|
+
const timer = setTimeout(() => {
|
|
1614
|
+
setAnimationState("exited");
|
|
1615
|
+
}, 500);
|
|
1616
|
+
return () => clearTimeout(timer);
|
|
1617
|
+
}
|
|
1618
|
+
}, [isVisible]);
|
|
1619
|
+
const handleToggle = () => {
|
|
1620
|
+
if (onToggle) onToggle();
|
|
1621
|
+
};
|
|
1622
|
+
if (!contractAddress || !adapter || !networkConfig) return null;
|
|
1623
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [(animationState === "entering" || animationState === "entered" || animationState === "exiting") && /* @__PURE__ */ jsx("div", {
|
|
1624
|
+
className: cn("fixed inset-0 z-[9998] md:hidden bg-background/60 backdrop-blur-sm transition-opacity duration-500 ease-in-out", animationState === "entering" || animationState === "entered" ? "opacity-100" : "opacity-0"),
|
|
1625
|
+
"aria-hidden": "true",
|
|
1626
|
+
onClick: onToggle ? handleToggle : void 0
|
|
1627
|
+
}), /* @__PURE__ */ jsxs(Card, {
|
|
1628
|
+
className: cn("overflow-hidden p-0 gap-0 flex flex-col transition-transform duration-500 ease-in-out will-change-transform transform-gpu", animationState === "entering" || animationState === "entered" ? "translate-y-0" : "translate-y-[120%]", animationState === "entering" || animationState === "entered" ? "md:scale-100 md:opacity-100" : "md:scale-95 md:opacity-0", "fixed bottom-4 inset-x-4 z-[9999] max-h-[50vh] shadow-xl bg-background backdrop-blur-md rounded-lg border border-border", "md:relative md:inset-auto md:bottom-auto md:shadow-none md:bg-card md:backdrop-blur-none md:z-auto md:mb-2 md:max-h-160", (animationState === "exiting" || animationState === "exited") && "pointer-events-none", animationState === "exited" && "md:hidden", className),
|
|
1629
|
+
"aria-hidden": animationState === "exited",
|
|
1630
|
+
children: [/* @__PURE__ */ jsxs(CardHeader, {
|
|
1631
|
+
className: "pb-2 pt-2 px-3 flex-shrink-0 flex flex-row items-center justify-between",
|
|
1632
|
+
children: [/* @__PURE__ */ jsx(CardTitle, {
|
|
1633
|
+
className: "text-sm font-medium",
|
|
1634
|
+
children: "Contract State"
|
|
1635
|
+
}), onToggle && /* @__PURE__ */ jsxs(Button, {
|
|
1636
|
+
variant: "ghost",
|
|
1637
|
+
size: "sm",
|
|
1638
|
+
className: "h-7 w-7 p-0",
|
|
1639
|
+
onClick: handleToggle,
|
|
1640
|
+
title: "Minimize Contract State",
|
|
1641
|
+
children: [/* @__PURE__ */ jsx(Minimize2, { size: 14 }), /* @__PURE__ */ jsx("span", {
|
|
1642
|
+
className: "sr-only",
|
|
1643
|
+
children: "Minimize Contract State"
|
|
1644
|
+
})]
|
|
1645
|
+
})]
|
|
1646
|
+
}), /* @__PURE__ */ jsx(CardContent, {
|
|
1647
|
+
className: "space-y-3 px-3 py-2 flex-grow overflow-y-auto flex flex-col min-h-0",
|
|
1648
|
+
children: error ? /* @__PURE__ */ jsxs("div", {
|
|
1649
|
+
className: "text-sm text-red-500 bg-red-50 border border-red-200 rounded-md p-3 flex flex-col items-center justify-center h-full",
|
|
1650
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1651
|
+
className: "font-medium text-center",
|
|
1652
|
+
children: "Error loading contract state"
|
|
1653
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1654
|
+
className: "mt-1 text-xs text-center",
|
|
1655
|
+
children: error.message
|
|
1656
|
+
})]
|
|
1657
|
+
}) : !effectiveSchema && !lastSchema || !adapter ? /* @__PURE__ */ jsxs("div", {
|
|
1658
|
+
className: "flex flex-col items-center justify-center h-full space-y-3 py-6",
|
|
1659
|
+
children: [/* @__PURE__ */ jsx(Loader2, { className: "h-8 w-8 text-primary animate-spin opacity-70" }), /* @__PURE__ */ jsxs("div", {
|
|
1660
|
+
className: "text-center space-y-1",
|
|
1661
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1662
|
+
className: "text-sm font-medium text-muted-foreground",
|
|
1663
|
+
children: "Loading contract info..."
|
|
1664
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1665
|
+
className: "text-xs text-muted-foreground",
|
|
1666
|
+
children: "Retrieving contract data and available functions"
|
|
1667
|
+
})]
|
|
1668
|
+
})]
|
|
1669
|
+
}) : viewFunctions.length > 0 && effectiveSchema ? /* @__PURE__ */ jsx(ViewFunctionsPanel, {
|
|
1670
|
+
functions: viewFunctions,
|
|
1671
|
+
contractAddress,
|
|
1672
|
+
adapter,
|
|
1673
|
+
contractSchema: effectiveSchema,
|
|
1674
|
+
className: "flex-grow flex flex-col min-h-0"
|
|
1675
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
1676
|
+
className: "flex flex-col items-center justify-center h-full py-4 text-center",
|
|
1677
|
+
children: [
|
|
1678
|
+
/* @__PURE__ */ jsx("div", {
|
|
1679
|
+
className: "rounded-full bg-muted p-3 mb-3",
|
|
1680
|
+
children: /* @__PURE__ */ jsx(FileText, { className: "h-6 w-6 text-muted-foreground" })
|
|
1681
|
+
}),
|
|
1682
|
+
/* @__PURE__ */ jsx("p", {
|
|
1683
|
+
className: "text-sm font-medium",
|
|
1684
|
+
children: "No simple view functions found"
|
|
1685
|
+
}),
|
|
1686
|
+
/* @__PURE__ */ jsx("p", {
|
|
1687
|
+
className: "text-xs text-muted-foreground mt-1 mb-4 max-w-[220px]",
|
|
1688
|
+
children: "This contract doesn't have any simple view functions that can be queried without parameters"
|
|
1689
|
+
})
|
|
1690
|
+
]
|
|
1691
|
+
})
|
|
1692
|
+
})]
|
|
1693
|
+
})] });
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
//#endregion
|
|
1697
|
+
//#region src/components/ContractActionBar/ContractActionBar.tsx
|
|
1698
|
+
/**
|
|
1699
|
+
* ContractActionBar - A composable action bar for contract forms
|
|
1700
|
+
* Displays network information and contract state toggle button
|
|
1701
|
+
* Can be extended with additional actions via children
|
|
1702
|
+
*/
|
|
1703
|
+
function ContractActionBar({ networkConfig, contractAddress = null, onToggleContractState, isWidgetExpanded = false, children, className = "" }) {
|
|
1704
|
+
if (!networkConfig) return null;
|
|
1705
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1706
|
+
className: `bg-background border-b mb-6 pb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between ${className}`,
|
|
1707
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1708
|
+
className: "flex flex-col gap-2 sm:flex-row sm:items-center",
|
|
1709
|
+
children: [/* @__PURE__ */ jsx(NetworkStatusBadge, { network: networkConfig }), contractAddress && onToggleContractState && !isWidgetExpanded && /* @__PURE__ */ jsx(ViewContractStateButton, {
|
|
1710
|
+
contractAddress,
|
|
1711
|
+
onToggle: onToggleContractState
|
|
1712
|
+
})]
|
|
1713
|
+
}), children && /* @__PURE__ */ jsx("div", {
|
|
1714
|
+
className: "flex gap-2 sm:ml-auto",
|
|
1715
|
+
children
|
|
1716
|
+
})]
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
//#endregion
|
|
1721
|
+
//#region src/components/network/NetworkServiceSettingsPanel.tsx
|
|
1722
|
+
/** Panel for configuring network service settings like RPC endpoints. */
|
|
1723
|
+
function NetworkServiceSettingsPanel({ adapter, networkId, service, onSettingsChanged }) {
|
|
1724
|
+
const [isTesting, setIsTesting] = useState(false);
|
|
1725
|
+
const [result, setResult] = useState(null);
|
|
1726
|
+
const { control, handleSubmit, setValue, getValues, watch, formState: { isDirty } } = useForm({ defaultValues: {} });
|
|
1727
|
+
const fields = useMemo(() => service.fields, [service]);
|
|
1728
|
+
const primaryFields = useMemo(() => fields.filter((f) => !f.metadata?.section), [fields]);
|
|
1729
|
+
const sectionGroups = useMemo(() => {
|
|
1730
|
+
const groups = {};
|
|
1731
|
+
for (const f of fields) {
|
|
1732
|
+
const md = f.metadata || {};
|
|
1733
|
+
const section = typeof md.section === "string" ? md.section : void 0;
|
|
1734
|
+
if (!section) continue;
|
|
1735
|
+
if (!groups[section]) groups[section] = {
|
|
1736
|
+
label: typeof md.sectionLabel === "string" ? md.sectionLabel : void 0,
|
|
1737
|
+
help: typeof md.sectionHelp === "string" ? md.sectionHelp : void 0,
|
|
1738
|
+
fields: []
|
|
1739
|
+
};
|
|
1740
|
+
if (!groups[section].label && typeof md.sectionLabel === "string") groups[section].label = md.sectionLabel;
|
|
1741
|
+
if (!groups[section].help && typeof md.sectionHelp === "string") groups[section].help = md.sectionHelp;
|
|
1742
|
+
groups[section].fields.push(f);
|
|
1743
|
+
}
|
|
1744
|
+
return groups;
|
|
1745
|
+
}, [fields]);
|
|
1746
|
+
useEffect(() => {
|
|
1747
|
+
try {
|
|
1748
|
+
const existing = userNetworkServiceConfigService.get(networkId, service.id);
|
|
1749
|
+
if (existing && typeof existing === "object") for (const f of fields) {
|
|
1750
|
+
const v = existing[f.name];
|
|
1751
|
+
if (v !== void 0) setValue(f.name, v);
|
|
1752
|
+
}
|
|
1753
|
+
for (const f of fields) {
|
|
1754
|
+
const current = getValues()[f.name];
|
|
1755
|
+
if (!(current !== void 0 && current !== null && current !== "") && "defaultValue" in f && f.defaultValue !== void 0) setValue(f.name, f.defaultValue);
|
|
1756
|
+
}
|
|
1757
|
+
} catch (e) {
|
|
1758
|
+
logger.error("NetworkServiceSettingsPanel", "Error loading config", e);
|
|
1759
|
+
}
|
|
1760
|
+
}, [
|
|
1761
|
+
networkId,
|
|
1762
|
+
service.id,
|
|
1763
|
+
fields,
|
|
1764
|
+
setValue,
|
|
1765
|
+
getValues
|
|
1766
|
+
]);
|
|
1767
|
+
const testConnection = useCallback(async () => {
|
|
1768
|
+
if (!adapter.testNetworkServiceConnection) return;
|
|
1769
|
+
setIsTesting(true);
|
|
1770
|
+
setResult(null);
|
|
1771
|
+
try {
|
|
1772
|
+
const data = getValues();
|
|
1773
|
+
const r = await adapter.testNetworkServiceConnection(service.id, data);
|
|
1774
|
+
setResult({
|
|
1775
|
+
success: r.success,
|
|
1776
|
+
message: r.error || (r.success ? "Connection successful" : "Connection failed"),
|
|
1777
|
+
latencyMs: r.latency
|
|
1778
|
+
});
|
|
1779
|
+
} catch (e) {
|
|
1780
|
+
setResult({
|
|
1781
|
+
success: false,
|
|
1782
|
+
message: e instanceof Error ? e.message : "Connection test failed"
|
|
1783
|
+
});
|
|
1784
|
+
} finally {
|
|
1785
|
+
setIsTesting(false);
|
|
1786
|
+
}
|
|
1787
|
+
}, [
|
|
1788
|
+
adapter,
|
|
1789
|
+
service.id,
|
|
1790
|
+
getValues
|
|
1791
|
+
]);
|
|
1792
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
1793
|
+
onSubmit: handleSubmit(useCallback(async (formData) => {
|
|
1794
|
+
const data = formData;
|
|
1795
|
+
if (adapter.validateNetworkServiceConfig) {
|
|
1796
|
+
if (!await adapter.validateNetworkServiceConfig(service.id, data)) {
|
|
1797
|
+
setResult({
|
|
1798
|
+
success: false,
|
|
1799
|
+
message: "Invalid configuration"
|
|
1800
|
+
});
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
if (Object.values(data).some((v) => v !== void 0 && v !== null && v !== "")) userNetworkServiceConfigService.save(networkId, service.id, data);
|
|
1805
|
+
else userNetworkServiceConfigService.clear(networkId, service.id);
|
|
1806
|
+
onSettingsChanged?.();
|
|
1807
|
+
setResult({
|
|
1808
|
+
success: true,
|
|
1809
|
+
message: "Settings saved successfully"
|
|
1810
|
+
});
|
|
1811
|
+
}, [
|
|
1812
|
+
adapter,
|
|
1813
|
+
networkId,
|
|
1814
|
+
service.id,
|
|
1815
|
+
onSettingsChanged
|
|
1816
|
+
])),
|
|
1817
|
+
className: "space-y-6",
|
|
1818
|
+
children: [
|
|
1819
|
+
service.description && /* @__PURE__ */ jsxs(Alert, { children: [
|
|
1820
|
+
/* @__PURE__ */ jsx(Info, { className: "h-4 w-4" }),
|
|
1821
|
+
/* @__PURE__ */ jsx(AlertTitle, {
|
|
1822
|
+
className: "text-sm",
|
|
1823
|
+
children: service.label
|
|
1824
|
+
}),
|
|
1825
|
+
/* @__PURE__ */ jsx(AlertDescription, {
|
|
1826
|
+
className: "space-y-1 text-xs",
|
|
1827
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1828
|
+
className: "mt-2",
|
|
1829
|
+
children: service.description
|
|
1830
|
+
})
|
|
1831
|
+
})
|
|
1832
|
+
] }),
|
|
1833
|
+
fields.filter((f) => f.metadata?.note).length > 0 && /* @__PURE__ */ jsx("div", {
|
|
1834
|
+
className: "space-y-4",
|
|
1835
|
+
children: fields.filter((f) => f.metadata?.note).map((f) => {
|
|
1836
|
+
const note = f.metadata?.note;
|
|
1837
|
+
return /* @__PURE__ */ jsxs(Alert, { children: [
|
|
1838
|
+
/* @__PURE__ */ jsx(note?.variant === "warning" ? AlertTriangle : Info, { className: "h-4 w-4" }),
|
|
1839
|
+
note?.title && /* @__PURE__ */ jsx(AlertTitle, {
|
|
1840
|
+
className: "text-sm",
|
|
1841
|
+
children: note.title
|
|
1842
|
+
}),
|
|
1843
|
+
note?.lines && note.lines.length > 0 && /* @__PURE__ */ jsx(AlertDescription, {
|
|
1844
|
+
className: "space-y-1 text-xs",
|
|
1845
|
+
children: note.lines.map((ln, idx) => note.html ? /* @__PURE__ */ jsx("p", { dangerouslySetInnerHTML: { __html: sanitizeHtml(ln) } }, idx) : /* @__PURE__ */ jsx("p", { children: ln }, idx))
|
|
1846
|
+
})
|
|
1847
|
+
] }, f.id);
|
|
1848
|
+
})
|
|
1849
|
+
}),
|
|
1850
|
+
primaryFields.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
1851
|
+
className: "space-y-4",
|
|
1852
|
+
children: primaryFields.map((field) => /* @__PURE__ */ jsx(DynamicFormField, {
|
|
1853
|
+
field,
|
|
1854
|
+
control,
|
|
1855
|
+
adapter
|
|
1856
|
+
}, field.id))
|
|
1857
|
+
}),
|
|
1858
|
+
Object.keys(sectionGroups).length > 0 && /* @__PURE__ */ jsx(Accordion, {
|
|
1859
|
+
type: "single",
|
|
1860
|
+
collapsible: true,
|
|
1861
|
+
className: "w-full",
|
|
1862
|
+
children: /* @__PURE__ */ jsxs(AccordionItem, {
|
|
1863
|
+
value: "advanced-settings",
|
|
1864
|
+
className: "border rounded-md",
|
|
1865
|
+
children: [/* @__PURE__ */ jsx(AccordionTrigger, {
|
|
1866
|
+
className: "text-sm font-medium px-3 py-2 hover:no-underline",
|
|
1867
|
+
children: "Advanced Settings"
|
|
1868
|
+
}), /* @__PURE__ */ jsx(AccordionContent, {
|
|
1869
|
+
className: "px-3 pb-4",
|
|
1870
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1871
|
+
className: "space-y-6 pt-2",
|
|
1872
|
+
children: Object.entries(sectionGroups).map(([sectionId, group]) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
1873
|
+
className: "mb-3",
|
|
1874
|
+
children: [/* @__PURE__ */ jsx("h4", {
|
|
1875
|
+
className: "text-sm font-medium text-foreground",
|
|
1876
|
+
children: group.label || sectionId.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
|
|
1877
|
+
}), group.help && /* @__PURE__ */ jsx("p", {
|
|
1878
|
+
className: "text-xs text-muted-foreground mt-1",
|
|
1879
|
+
children: group.help
|
|
1880
|
+
})]
|
|
1881
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1882
|
+
className: "space-y-4",
|
|
1883
|
+
children: group.fields.map((field) => {
|
|
1884
|
+
const md = field.metadata || {};
|
|
1885
|
+
const indentUnder = typeof md["nestUnder"] === "string" ? md["nestUnder"] : void 0;
|
|
1886
|
+
const disabledWhen = md["disabledWhen"];
|
|
1887
|
+
let isDisabled = false;
|
|
1888
|
+
if (disabledWhen && typeof disabledWhen.field === "string") {
|
|
1889
|
+
const depVal = watch(disabledWhen.field);
|
|
1890
|
+
if ("equals" in disabledWhen) isDisabled = depVal === disabledWhen.equals;
|
|
1891
|
+
else if ("notEquals" in disabledWhen) isDisabled = depVal !== disabledWhen.notEquals;
|
|
1892
|
+
}
|
|
1893
|
+
const wrappedField = {
|
|
1894
|
+
...field,
|
|
1895
|
+
readOnly: isDisabled
|
|
1896
|
+
};
|
|
1897
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1898
|
+
className: indentUnder ? "ml-6" : "",
|
|
1899
|
+
children: /* @__PURE__ */ jsx(DynamicFormField, {
|
|
1900
|
+
field: wrappedField,
|
|
1901
|
+
control,
|
|
1902
|
+
adapter
|
|
1903
|
+
})
|
|
1904
|
+
}, field.id);
|
|
1905
|
+
})
|
|
1906
|
+
})] }, sectionId))
|
|
1907
|
+
})
|
|
1908
|
+
})]
|
|
1909
|
+
})
|
|
1910
|
+
}),
|
|
1911
|
+
result && /* @__PURE__ */ jsxs("div", {
|
|
1912
|
+
className: `flex items-center gap-2 text-sm ${result.success ? "text-green-600" : "text-red-600"}`,
|
|
1913
|
+
children: [
|
|
1914
|
+
result.success ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(XCircle, { className: "h-4 w-4" }),
|
|
1915
|
+
/* @__PURE__ */ jsx("span", { children: result.message }),
|
|
1916
|
+
result.latencyMs && /* @__PURE__ */ jsxs("span", {
|
|
1917
|
+
className: "text-muted-foreground",
|
|
1918
|
+
children: [
|
|
1919
|
+
"(",
|
|
1920
|
+
result.latencyMs,
|
|
1921
|
+
"ms)"
|
|
1922
|
+
]
|
|
1923
|
+
})
|
|
1924
|
+
]
|
|
1925
|
+
}),
|
|
1926
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1927
|
+
className: "flex justify-end gap-2",
|
|
1928
|
+
children: [
|
|
1929
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1930
|
+
type: "button",
|
|
1931
|
+
variant: "outline",
|
|
1932
|
+
onClick: () => {
|
|
1933
|
+
for (const f of fields) setValue(f.name, void 0);
|
|
1934
|
+
userNetworkServiceConfigService.clear(networkId, service.id);
|
|
1935
|
+
setResult(null);
|
|
1936
|
+
onSettingsChanged?.();
|
|
1937
|
+
},
|
|
1938
|
+
children: "Reset to Default"
|
|
1939
|
+
}),
|
|
1940
|
+
(() => {
|
|
1941
|
+
if (service.supportsConnectionTest === false) return false;
|
|
1942
|
+
const hideTest = fields.some((f) => {
|
|
1943
|
+
return f.metadata?.hideTestConnection === true;
|
|
1944
|
+
});
|
|
1945
|
+
return Boolean(adapter.testNetworkServiceConnection) && !hideTest;
|
|
1946
|
+
})() ? /* @__PURE__ */ jsx(Button, {
|
|
1947
|
+
type: "button",
|
|
1948
|
+
variant: "outline",
|
|
1949
|
+
onClick: testConnection,
|
|
1950
|
+
disabled: isTesting,
|
|
1951
|
+
children: isTesting ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "Testing..."] }) : "Test Connection"
|
|
1952
|
+
}) : null,
|
|
1953
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1954
|
+
type: "submit",
|
|
1955
|
+
disabled: !isDirty,
|
|
1956
|
+
children: "Save Settings"
|
|
1957
|
+
})
|
|
1958
|
+
]
|
|
1959
|
+
})
|
|
1960
|
+
]
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
//#endregion
|
|
1965
|
+
//#region src/components/network/NetworkSettingsDialog.tsx
|
|
1966
|
+
const NetworkSettingsDialog = ({ isOpen, onOpenChange, networkConfig, adapter }) => {
|
|
1967
|
+
const services = adapter?.getNetworkServiceForms?.() ?? [];
|
|
1968
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
1969
|
+
open: isOpen,
|
|
1970
|
+
onOpenChange,
|
|
1971
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
1972
|
+
className: "max-w-2xl sm:max-w-3xl",
|
|
1973
|
+
children: [/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogTitle, { children: "Network Settings" }), /* @__PURE__ */ jsxs(DialogDescription, { children: ["Configure settings for ", networkConfig?.name] })] }), networkConfig && adapter && services.length > 0 ? /* @__PURE__ */ jsxs(Tabs, {
|
|
1974
|
+
defaultValue: services[0]?.id,
|
|
1975
|
+
className: "w-full",
|
|
1976
|
+
children: [/* @__PURE__ */ jsx(TabsList, {
|
|
1977
|
+
className: `grid w-full ${services.length <= 2 ? "grid-cols-2" : services.length === 3 ? "grid-cols-3" : "grid-cols-4"}`,
|
|
1978
|
+
children: services.map((svc) => /* @__PURE__ */ jsx(TabsTrigger, {
|
|
1979
|
+
value: svc.id,
|
|
1980
|
+
children: svc.label
|
|
1981
|
+
}, svc.id))
|
|
1982
|
+
}), services.map((svc) => /* @__PURE__ */ jsx(TabsContent, {
|
|
1983
|
+
value: svc.id,
|
|
1984
|
+
className: "px-2",
|
|
1985
|
+
children: /* @__PURE__ */ jsx(NetworkServiceSettingsPanel, {
|
|
1986
|
+
adapter,
|
|
1987
|
+
networkId: networkConfig.id,
|
|
1988
|
+
service: svc,
|
|
1989
|
+
onSettingsChanged: () => {
|
|
1990
|
+
const cfg = appConfigService.getTypedNestedConfig("walletui", "config");
|
|
1991
|
+
adapter.configureUiKit?.(cfg ?? {
|
|
1992
|
+
kitName: "custom",
|
|
1993
|
+
kitConfig: {}
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
})
|
|
1997
|
+
}, svc.id))]
|
|
1998
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
1999
|
+
className: "flex items-center justify-center py-8",
|
|
2000
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2001
|
+
className: "text-center text-muted-foreground",
|
|
2002
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
2003
|
+
className: "text-sm",
|
|
2004
|
+
children: "No network configuration available"
|
|
2005
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
2006
|
+
className: "text-xs mt-1",
|
|
2007
|
+
children: "This network uses default settings that cannot be customized"
|
|
2008
|
+
})]
|
|
2009
|
+
})
|
|
2010
|
+
})]
|
|
2011
|
+
})
|
|
2012
|
+
});
|
|
2013
|
+
};
|
|
2014
|
+
|
|
2015
|
+
//#endregion
|
|
2016
|
+
//#region src/components/WalletConnectionWithSettings.tsx
|
|
2017
|
+
/**
|
|
2018
|
+
* Enhanced wallet connection header with network settings menu.
|
|
2019
|
+
* Used in exported apps to provide access to RPC and Explorer configuration.
|
|
2020
|
+
*/
|
|
2021
|
+
const WalletConnectionWithSettings = () => {
|
|
2022
|
+
const { isAdapterLoading, activeAdapter, activeNetworkConfig } = useWalletState();
|
|
2023
|
+
const { setOpenNetworkSettingsHandler } = useNetworkErrors();
|
|
2024
|
+
const [showNetworkSettings, setShowNetworkSettings] = useState(false);
|
|
2025
|
+
const openNetworkSettings = useCallback((networkId) => {
|
|
2026
|
+
if (activeNetworkConfig && networkId === activeNetworkConfig.id) setShowNetworkSettings(true);
|
|
2027
|
+
}, [activeNetworkConfig]);
|
|
2028
|
+
useEffect(() => {
|
|
2029
|
+
setOpenNetworkSettingsHandler(openNetworkSettings);
|
|
2030
|
+
}, [openNetworkSettings, setOpenNetworkSettingsHandler]);
|
|
2031
|
+
if (isAdapterLoading) return /* @__PURE__ */ jsx("div", { className: "h-9 w-28 animate-pulse rounded bg-muted" });
|
|
2032
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
2033
|
+
className: "flex items-center gap-2",
|
|
2034
|
+
children: [/* @__PURE__ */ jsx(WalletConnectionUI, {}), activeAdapter && activeNetworkConfig && /* @__PURE__ */ jsx(Button, {
|
|
2035
|
+
variant: "ghost",
|
|
2036
|
+
size: "icon",
|
|
2037
|
+
className: "h-9 w-9",
|
|
2038
|
+
title: "Network Settings",
|
|
2039
|
+
onClick: () => setShowNetworkSettings(true),
|
|
2040
|
+
children: /* @__PURE__ */ jsx(Settings, { className: "h-4 w-4" })
|
|
2041
|
+
})]
|
|
2042
|
+
}), /* @__PURE__ */ jsx(NetworkSettingsDialog, {
|
|
2043
|
+
isOpen: showNetworkSettings,
|
|
2044
|
+
onOpenChange: setShowNetworkSettings,
|
|
2045
|
+
networkConfig: activeNetworkConfig,
|
|
2046
|
+
adapter: activeAdapter
|
|
2047
|
+
})] });
|
|
2048
|
+
};
|
|
2049
|
+
|
|
2050
|
+
//#endregion
|
|
2051
|
+
export { ContractActionBar, ContractStateWidget, DynamicFormField, ExecutionConfigDisplay, NetworkSettingsDialog, TransactionExecuteButton, TransactionForm, WalletConnectionWithSettings, createAddressTransform, createArrayObjectTransform, createArrayTransform, createBigIntTransform, createBooleanTransform, createComplexTypeTransform, createDefaultFormValues, createNumberTransform, createObjectTransform, createTextTransform, createTransformForFieldType, generateDefaultValue, getDefaultValueByFieldType, validateField };
|
|
2052
|
+
//# sourceMappingURL=index.js.map
|