Skip to content

Commit 0007d7a

Browse files
committed
KEEP 1153 Read contract
1 parent 2d02cd7 commit 0007d7a

File tree

6 files changed

+653
-7
lines changed

6 files changed

+653
-7
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
8282
- **Resend**: Send Email
8383
- **Slack**: Send Slack Message
8484
- **v0**: Create Chat, Send Message
85-
- **Web3**: Transfer Funds
85+
- **Web3**: Transfer Funds, Read Contract
8686
<!-- PLUGINS:END -->
8787

8888
## Code Generation

components/workflow/config/action-config-renderer.tsx

Lines changed: 233 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { ChevronDown } from "lucide-react";
4-
import { useState } from "react";
4+
import React, { useState } from "react";
55
import { Input } from "@/components/ui/input";
66
import { Label } from "@/components/ui/label";
77
import {
@@ -114,9 +114,187 @@ function SchemaBuilderField(props: FieldProps) {
114114
);
115115
}
116116

117-
const FIELD_RENDERERS: Record<
118-
ActionConfigFieldBase["type"],
119-
React.ComponentType<FieldProps>
117+
type AbiFunctionSelectProps = FieldProps & {
118+
abiValue: string;
119+
};
120+
121+
function AbiFunctionSelectField({
122+
field,
123+
value,
124+
onChange,
125+
disabled,
126+
abiValue,
127+
}: AbiFunctionSelectProps) {
128+
// Parse ABI and extract functions
129+
const functions = React.useMemo(() => {
130+
if (!abiValue || abiValue.trim() === "") {
131+
return [];
132+
}
133+
134+
try {
135+
const abi = JSON.parse(abiValue);
136+
if (!Array.isArray(abi)) {
137+
return [];
138+
}
139+
140+
// Extract all functions from the ABI
141+
return abi
142+
.filter((item) => item.type === "function")
143+
.map((func) => {
144+
const inputs = func.inputs || [];
145+
const params = inputs
146+
.map(
147+
(input: { name: string; type: string }) =>
148+
`${input.type} ${input.name}`
149+
)
150+
.join(", ");
151+
return {
152+
name: func.name,
153+
label: `${func.name}(${params})`,
154+
stateMutability: func.stateMutability || "nonpayable",
155+
};
156+
});
157+
} catch {
158+
return [];
159+
}
160+
}, [abiValue]);
161+
162+
if (functions.length === 0) {
163+
return (
164+
<div className="rounded-md border border-dashed p-3 text-center text-muted-foreground text-sm">
165+
{abiValue
166+
? "No functions found in ABI"
167+
: "Enter ABI above to see available functions"}
168+
</div>
169+
);
170+
}
171+
172+
return (
173+
<Select disabled={disabled} onValueChange={onChange} value={value}>
174+
<SelectTrigger className="w-full" id={field.key}>
175+
<SelectValue placeholder={field.placeholder || "Select a function"} />
176+
</SelectTrigger>
177+
<SelectContent>
178+
{functions.map((func) => (
179+
<SelectItem key={func.name} value={func.name}>
180+
<div className="flex flex-col">
181+
<span>{func.label}</span>
182+
<span className="text-muted-foreground text-xs">
183+
{func.stateMutability}
184+
</span>
185+
</div>
186+
</SelectItem>
187+
))}
188+
</SelectContent>
189+
</Select>
190+
);
191+
}
192+
193+
type AbiFunctionArgsProps = FieldProps & {
194+
abiValue: string;
195+
functionValue: string;
196+
};
197+
198+
function AbiFunctionArgsField({
199+
field,
200+
value,
201+
onChange,
202+
disabled,
203+
abiValue,
204+
functionValue,
205+
}: AbiFunctionArgsProps) {
206+
// Parse the function inputs from the ABI
207+
const functionInputs = React.useMemo(() => {
208+
if (
209+
!(abiValue && functionValue) ||
210+
abiValue.trim() === "" ||
211+
functionValue.trim() === ""
212+
) {
213+
return [];
214+
}
215+
216+
try {
217+
const abi = JSON.parse(abiValue);
218+
if (!Array.isArray(abi)) {
219+
return [];
220+
}
221+
222+
const func = abi.find(
223+
(item) => item.type === "function" && item.name === functionValue
224+
);
225+
226+
if (!func?.inputs) {
227+
return [];
228+
}
229+
230+
return func.inputs.map((input: { name: string; type: string }) => ({
231+
name: input.name || "unnamed",
232+
type: input.type,
233+
}));
234+
} catch {
235+
return [];
236+
}
237+
}, [abiValue, functionValue]);
238+
239+
// Parse current value (JSON array) into individual arg values
240+
const argValues = React.useMemo(() => {
241+
if (!value || value.trim() === "") {
242+
return [];
243+
}
244+
try {
245+
const parsed = JSON.parse(value);
246+
return Array.isArray(parsed) ? parsed : [];
247+
} catch {
248+
return [];
249+
}
250+
}, [value]);
251+
252+
// Handle individual arg change
253+
const handleArgChange = (index: number, newValue: string) => {
254+
const newArgs = [...argValues];
255+
// Ensure array is long enough
256+
while (newArgs.length <= index) {
257+
newArgs.push("");
258+
}
259+
newArgs[index] = newValue;
260+
onChange(JSON.stringify(newArgs));
261+
};
262+
263+
if (functionInputs.length === 0) {
264+
return (
265+
<div className="rounded-md border border-dashed p-3 text-center text-muted-foreground text-sm">
266+
{functionValue
267+
? "This function has no parameters"
268+
: "Select a function above to see parameters"}
269+
</div>
270+
);
271+
}
272+
273+
return (
274+
<div className="space-y-3">
275+
{functionInputs.map(
276+
(input: { name: string; type: string }, index: number) => (
277+
<div className="space-y-1.5" key={`${field.key}-arg-${index}`}>
278+
<Label className="ml-1 text-xs" htmlFor={`${field.key}-${index}`}>
279+
{input.name}{" "}
280+
<span className="text-muted-foreground">({input.type})</span>
281+
</Label>
282+
<TemplateBadgeInput
283+
disabled={disabled}
284+
id={`${field.key}-${index}`}
285+
onChange={(val) => handleArgChange(index, val as string)}
286+
placeholder={`Enter ${input.type} value or {{NodeName.value}}`}
287+
value={(argValues[index] as string) || ""}
288+
/>
289+
</div>
290+
)
291+
)}
292+
</div>
293+
);
294+
}
295+
296+
const FIELD_RENDERERS: Partial<
297+
Record<ActionConfigFieldBase["type"], React.ComponentType<FieldProps>>
120298
> = {
121299
"template-input": TemplateInputField,
122300
"template-textarea": TemplateTextareaField,
@@ -145,8 +323,58 @@ function renderField(
145323

146324
const value =
147325
(config[field.key] as string | undefined) || field.defaultValue || "";
326+
327+
// Special handling for abi-function-select
328+
if (field.type === "abi-function-select") {
329+
const abiField = field.abiField || "abi";
330+
const abiValue = (config[abiField] as string | undefined) || "";
331+
332+
return (
333+
<div className="space-y-2" key={field.key}>
334+
<Label className="ml-1" htmlFor={field.key}>
335+
{field.label}
336+
</Label>
337+
<AbiFunctionSelectField
338+
abiValue={abiValue}
339+
disabled={disabled}
340+
field={field}
341+
onChange={(val) => onUpdateConfig(field.key, val)}
342+
value={value}
343+
/>
344+
</div>
345+
);
346+
}
347+
348+
// Special handling for abi-function-args
349+
if (field.type === "abi-function-args") {
350+
const abiField = field.abiField || "abi";
351+
const functionField = field.abiFunctionField || "abiFunction";
352+
const abiValue = (config[abiField] as string | undefined) || "";
353+
const functionValue = (config[functionField] as string | undefined) || "";
354+
355+
return (
356+
<div className="space-y-2" key={field.key}>
357+
<Label className="ml-1" htmlFor={field.key}>
358+
{field.label}
359+
</Label>
360+
<AbiFunctionArgsField
361+
abiValue={abiValue}
362+
disabled={disabled}
363+
field={field}
364+
functionValue={functionValue}
365+
onChange={(val) => onUpdateConfig(field.key, val)}
366+
value={value}
367+
/>
368+
</div>
369+
);
370+
}
371+
148372
const FieldRenderer = FIELD_RENDERERS[field.type];
149373

374+
if (!FieldRenderer) {
375+
return null;
376+
}
377+
150378
return (
151379
<div className="space-y-2" key={field.key}>
152380
<Label className="ml-1" htmlFor={field.key}>
@@ -155,7 +383,7 @@ function renderField(
155383
<FieldRenderer
156384
disabled={disabled}
157385
field={field}
158-
onChange={(val) => onUpdateConfig(field.key, val)}
386+
onChange={(val: unknown) => onUpdateConfig(field.key, val)}
159387
value={value}
160388
/>
161389
</div>

0 commit comments

Comments
 (0)