Skip to content

Commit 6a25874

Browse files
Merge pull request #728 from scaffold-eth/cli-backmerge
2 parents 67188c1 + f610b64 commit 6a25874

File tree

28 files changed

+471
-146
lines changed

28 files changed

+471
-146
lines changed

.changeset/five-trees-hunt.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"create-eth": patch
3+
---
4+
5+
- Feat: Better complex struct inputs (#702)
6+
- improve debug struct UI (#726)
7+
- use next-themes to handle theme and update usehooks-ts (#707)
8+
- up rainbowkit version to 1.3.5 (#719)
9+
- Use arbitrumSepolia instead of Goerli (#716)
10+
- Add Optimism Sepolia config (#711)
11+
- Update screenshot example on Readme (#705)
12+
- add use client to inputs barrel file (#699)
13+
- add baseSepolia in hardhat.config (#696)
14+
- removed "use client" from EtherInput, IntergerInput and AddessInput (#688)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
- 🔥 **Burner Wallet & Local Faucet**: Quickly test your application with a burner wallet and local faucet.
1919
- 🔐 **Integration with Wallet Providers**: Connect to different wallet providers and interact with the Ethereum network.
2020

21-
![Debug Contracts tab](https://github.com/scaffold-eth/scaffold-eth-2/assets/55535804/1171422a-0ce4-4203-bcd4-d2d1941d198b)
21+
![Debug Contracts tab](https://github.com/scaffold-eth/scaffold-eth-2/assets/55535804/b237af0c-5027-4849-a5c1-2e31495cccb1)
2222

2323
## Requirements
2424

templates/base/.yarn/patches/usehooks-ts-npm-2.7.2-fceffe0e43.patch

Lines changed: 0 additions & 13 deletions
This file was deleted.

templates/base/packages/nextjs/app/debug/_components/DebugContracts.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export function DebugContracts() {
1515
const [selectedContract, setSelectedContract] = useLocalStorage<ContractName>(
1616
selectedContractStorageKey,
1717
contractNames[0],
18+
{ initializeWithValue: false },
1819
);
1920

2021
useEffect(() => {

templates/base/packages/nextjs/app/debug/_components/contract/ContractInput.tsx

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use client";
22

33
import { Dispatch, SetStateAction } from "react";
4+
import { Tuple } from "./Tuple";
5+
import { TupleArray } from "./TupleArray";
46
import { AbiParameter } from "abitype";
57
import {
68
AddressInput,
@@ -10,6 +12,7 @@ import {
1012
IntegerInput,
1113
IntegerVariant,
1214
} from "~~/components/scaffold-eth";
15+
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
1316

1417
type ContractInputProps = {
1518
setForm: Dispatch<SetStateAction<Record<string, any>>>;
@@ -31,17 +34,51 @@ export const ContractInput = ({ setForm, form, stateObjectKey, paramType }: Cont
3134
},
3235
};
3336

34-
if (paramType.type === "address") {
35-
return <AddressInput {...inputProps} />;
36-
} else if (paramType.type === "bytes32") {
37-
return <Bytes32Input {...inputProps} />;
38-
} else if (paramType.type === "bytes") {
39-
return <BytesInput {...inputProps} />;
40-
} else if (paramType.type === "string") {
41-
return <InputBase {...inputProps} />;
42-
} else if (paramType.type.includes("int") && !paramType.type.includes("[")) {
43-
return <IntegerInput {...inputProps} variant={paramType.type as IntegerVariant} />;
44-
}
37+
const renderInput = () => {
38+
switch (paramType.type) {
39+
case "address":
40+
return <AddressInput {...inputProps} />;
41+
case "bytes32":
42+
return <Bytes32Input {...inputProps} />;
43+
case "bytes":
44+
return <BytesInput {...inputProps} />;
45+
case "string":
46+
return <InputBase {...inputProps} />;
47+
case "tuple":
48+
return (
49+
<Tuple
50+
setParentForm={setForm}
51+
parentForm={form}
52+
abiTupleParameter={paramType as AbiParameterTuple}
53+
parentStateObjectKey={stateObjectKey}
54+
/>
55+
);
56+
default:
57+
// Handling 'int' types and 'tuple[]' types
58+
if (paramType.type.includes("int") && !paramType.type.includes("[")) {
59+
return <IntegerInput {...inputProps} variant={paramType.type as IntegerVariant} />;
60+
} else if (paramType.type.startsWith("tuple[")) {
61+
return (
62+
<TupleArray
63+
setParentForm={setForm}
64+
parentForm={form}
65+
abiTupleParameter={paramType as AbiParameterTuple}
66+
parentStateObjectKey={stateObjectKey}
67+
/>
68+
);
69+
} else {
70+
return <InputBase {...inputProps} />;
71+
}
72+
}
73+
};
4574

46-
return <InputBase {...inputProps} />;
75+
return (
76+
<div className="flex flex-col gap-1.5 w-full">
77+
<div className="flex items-center ml-2">
78+
{paramType.name && <span className="text-xs font-medium mr-2 leading-none">{paramType.name}</span>}
79+
<span className="block text-xs font-extralight leading-none">{paramType.type}</span>
80+
</div>
81+
{renderInput()}
82+
</div>
83+
);
4784
};

templates/base/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getFunctionInputKey,
1212
getInitialFormState,
1313
getParsedContractFunctionArgs,
14+
transformAbiFunction,
1415
} from "~~/app/debug/_components/contract";
1516
import { getParsedError, notification } from "~~/utils/scaffold-eth";
1617

@@ -42,7 +43,8 @@ export const ReadOnlyFunctionForm = ({
4243
},
4344
});
4445

45-
const inputElements = abiFunction.inputs.map((input, inputIndex) => {
46+
const transformedFunction = transformAbiFunction(abiFunction);
47+
const inputElements = transformedFunction.inputs.map((input, inputIndex) => {
4648
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
4749
return (
4850
<ContractInput
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Dispatch, SetStateAction, useEffect, useState } from "react";
2+
import { ContractInput } from "./ContractInput";
3+
import { getFunctionInputKey, getInitalTupleFormState } from "./utilsContract";
4+
import { replacer } from "~~/utils/scaffold-eth/common";
5+
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
6+
7+
type TupleProps = {
8+
abiTupleParameter: AbiParameterTuple;
9+
setParentForm: Dispatch<SetStateAction<Record<string, any>>>;
10+
parentStateObjectKey: string;
11+
parentForm: Record<string, any> | undefined;
12+
};
13+
14+
export const Tuple = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleProps) => {
15+
const [form, setForm] = useState<Record<string, any>>(() => getInitalTupleFormState(abiTupleParameter));
16+
17+
useEffect(() => {
18+
const values = Object.values(form);
19+
const argsStruct: Record<string, any> = {};
20+
abiTupleParameter.components.forEach((component, componentIndex) => {
21+
argsStruct[component.name || `input_${componentIndex}_`] = values[componentIndex];
22+
});
23+
24+
setParentForm(parentForm => ({ ...parentForm, [parentStateObjectKey]: JSON.stringify(argsStruct, replacer) }));
25+
// eslint-disable-next-line react-hooks/exhaustive-deps
26+
}, [JSON.stringify(form, replacer)]);
27+
28+
return (
29+
<div>
30+
<div className="collapse collapse-arrow bg-base-200 pl-4 py-1.5 border-2 border-secondary">
31+
<input type="checkbox" className="min-h-fit peer" />
32+
<div className="collapse-title p-0 min-h-fit peer-checked:mb-2 text-primary-content/50">
33+
<p className="m-0 p-0 text-[1rem]">{abiTupleParameter.internalType}</p>
34+
</div>
35+
<div className="ml-3 flex-col space-y-4 border-secondary/80 border-l-2 pl-4 collapse-content">
36+
{abiTupleParameter?.components?.map((param, index) => {
37+
const key = getFunctionInputKey(abiTupleParameter.name || "tuple", param, index);
38+
return <ContractInput setForm={setForm} form={form} key={key} stateObjectKey={key} paramType={param} />;
39+
})}
40+
</div>
41+
</div>
42+
</div>
43+
);
44+
};
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { Dispatch, SetStateAction, useEffect, useState } from "react";
2+
import { ContractInput } from "./ContractInput";
3+
import { getFunctionInputKey, getInitalTupleArrayFormState } from "./utilsContract";
4+
import { replacer } from "~~/utils/scaffold-eth/common";
5+
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
6+
7+
type TupleArrayProps = {
8+
abiTupleParameter: AbiParameterTuple & { isVirtual?: true };
9+
setParentForm: Dispatch<SetStateAction<Record<string, any>>>;
10+
parentStateObjectKey: string;
11+
parentForm: Record<string, any> | undefined;
12+
};
13+
14+
export const TupleArray = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleArrayProps) => {
15+
const [form, setForm] = useState<Record<string, any>>(() => getInitalTupleArrayFormState(abiTupleParameter));
16+
const [additionalInputs, setAdditionalInputs] = useState<Array<typeof abiTupleParameter.components>>([
17+
abiTupleParameter.components,
18+
]);
19+
20+
const depth = (abiTupleParameter.type.match(/\[\]/g) || []).length;
21+
22+
useEffect(() => {
23+
// Extract and group fields based on index prefix
24+
const groupedFields = Object.keys(form).reduce((acc, key) => {
25+
const [indexPrefix, ...restArray] = key.split("_");
26+
const componentName = restArray.join("_");
27+
if (!acc[indexPrefix]) {
28+
acc[indexPrefix] = {};
29+
}
30+
acc[indexPrefix][componentName] = form[key];
31+
return acc;
32+
}, {} as Record<string, Record<string, any>>);
33+
34+
let argsArray: Array<Record<string, any>> = [];
35+
36+
Object.keys(groupedFields).forEach(key => {
37+
const currentKeyValues = Object.values(groupedFields[key]);
38+
39+
const argsStruct: Record<string, any> = {};
40+
abiTupleParameter.components.forEach((component, componentIndex) => {
41+
argsStruct[component.name || `input_${componentIndex}_`] = currentKeyValues[componentIndex];
42+
});
43+
44+
argsArray.push(argsStruct);
45+
});
46+
47+
if (depth > 1) {
48+
argsArray = argsArray.map(args => {
49+
return args[abiTupleParameter.components[0].name || "tuple"];
50+
});
51+
}
52+
53+
setParentForm(parentForm => {
54+
return { ...parentForm, [parentStateObjectKey]: JSON.stringify(argsArray, replacer) };
55+
});
56+
// eslint-disable-next-line react-hooks/exhaustive-deps
57+
}, [JSON.stringify(form, replacer)]);
58+
59+
const addInput = () => {
60+
setAdditionalInputs(previousValue => {
61+
const newAdditionalInputs = [...previousValue, abiTupleParameter.components];
62+
63+
// Add the new inputs to the form
64+
setForm(form => {
65+
const newForm = { ...form };
66+
abiTupleParameter.components.forEach((component, componentIndex) => {
67+
const key = getFunctionInputKey(
68+
`${newAdditionalInputs.length - 1}_${abiTupleParameter.name || "tuple"}`,
69+
component,
70+
componentIndex,
71+
);
72+
newForm[key] = "";
73+
});
74+
return newForm;
75+
});
76+
77+
return newAdditionalInputs;
78+
});
79+
};
80+
81+
const removeInput = () => {
82+
// Remove the last inputs from the form
83+
setForm(form => {
84+
const newForm = { ...form };
85+
abiTupleParameter.components.forEach((component, componentIndex) => {
86+
const key = getFunctionInputKey(
87+
`${additionalInputs.length - 1}_${abiTupleParameter.name || "tuple"}`,
88+
component,
89+
componentIndex,
90+
);
91+
delete newForm[key];
92+
});
93+
return newForm;
94+
});
95+
setAdditionalInputs(inputs => inputs.slice(0, -1));
96+
};
97+
98+
return (
99+
<div>
100+
<div className="collapse collapse-arrow bg-base-200 pl-4 py-1.5 border-2 border-secondary">
101+
<input type="checkbox" className="min-h-fit peer" />
102+
<div className="collapse-title p-0 min-h-fit peer-checked:mb-1 text-primary-content/50">
103+
<p className="m-0 text-[1rem]">{abiTupleParameter.internalType}</p>
104+
</div>
105+
<div className="ml-3 flex-col space-y-2 border-secondary/70 border-l-2 pl-4 collapse-content">
106+
{additionalInputs.map((additionalInput, additionalIndex) => (
107+
<div key={additionalIndex} className="space-y-1">
108+
<span className="badge bg-base-300 badge-sm">
109+
{depth > 1 ? `${additionalIndex}` : `tuple[${additionalIndex}]`}
110+
</span>
111+
<div className="space-y-4">
112+
{additionalInput.map((param, index) => {
113+
const key = getFunctionInputKey(
114+
`${additionalIndex}_${abiTupleParameter.name || "tuple"}`,
115+
param,
116+
index,
117+
);
118+
return (
119+
<ContractInput setForm={setForm} form={form} key={key} stateObjectKey={key} paramType={param} />
120+
);
121+
})}
122+
</div>
123+
</div>
124+
))}
125+
<div className="flex space-x-2">
126+
<button className="btn btn-sm btn-secondary" onClick={addInput}>
127+
+
128+
</button>
129+
{additionalInputs.length > 0 && (
130+
<button className="btn btn-sm btn-secondary" onClick={removeInput}>
131+
-
132+
</button>
133+
)}
134+
</div>
135+
</div>
136+
</div>
137+
</div>
138+
);
139+
};

templates/base/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getFunctionInputKey,
1212
getInitialFormState,
1313
getParsedContractFunctionArgs,
14+
transformAbiFunction,
1415
} from "~~/app/debug/_components/contract";
1516
import { IntegerInput } from "~~/components/scaffold-eth";
1617
import { useTransactor } from "~~/hooks/scaffold-eth";
@@ -72,7 +73,8 @@ export const WriteOnlyFunctionForm = ({
7273
}, [txResult]);
7374

7475
// TODO use `useMemo` to optimize also update in ReadOnlyFunctionForm
75-
const inputs = abiFunction.inputs.map((input, inputIndex) => {
76+
const transformedFunction = transformAbiFunction(abiFunction);
77+
const inputs = transformedFunction.inputs.map((input, inputIndex) => {
7678
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
7779
return (
7880
<ContractInput
@@ -98,14 +100,20 @@ export const WriteOnlyFunctionForm = ({
98100
</p>
99101
{inputs}
100102
{abiFunction.stateMutability === "payable" ? (
101-
<IntegerInput
102-
value={txValue}
103-
onChange={updatedTxValue => {
104-
setDisplayedTxResult(undefined);
105-
setTxValue(updatedTxValue);
106-
}}
107-
placeholder="value (wei)"
108-
/>
103+
<div className="flex flex-col gap-1.5 w-full">
104+
<div className="flex items-center ml-2">
105+
<span className="text-xs font-medium mr-2 leading-none">payable value</span>
106+
<span className="block text-xs font-extralight leading-none">wei</span>
107+
</div>
108+
<IntegerInput
109+
value={txValue}
110+
onChange={updatedTxValue => {
111+
setDisplayedTxResult(undefined);
112+
setTxValue(updatedTxValue);
113+
}}
114+
placeholder="value (wei)"
115+
/>
116+
</div>
109117
) : null}
110118
<div className="flex justify-between gap-2">
111119
{!zeroInputs && (

0 commit comments

Comments
 (0)