Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/five-trees-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"create-eth": patch
---

- Feat: Better complex struct inputs (#702)
- improve debug struct UI (#726)
- use next-themes to handle theme and update usehooks-ts (#707)
- up rainbowkit version to 1.3.5 (#719)
- Use arbitrumSepolia instead of Goerli (#716)
- Add Optimism Sepolia config (#711)
- Update screenshot example on Readme (#705)
- add use client to inputs barrel file (#699)
- add baseSepolia in hardhat.config (#696)
- removed "use client" from EtherInput, IntergerInput and AddessInput (#688)
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
- 🔥 **Burner Wallet & Local Faucet**: Quickly test your application with a burner wallet and local faucet.
- 🔐 **Integration with Wallet Providers**: Connect to different wallet providers and interact with the Ethereum network.

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

## Requirements

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function DebugContracts() {
const [selectedContract, setSelectedContract] = useLocalStorage<ContractName>(
selectedContractStorageKey,
contractNames[0],
{ initializeWithValue: false },
);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"use client";

import { Dispatch, SetStateAction } from "react";
import { Tuple } from "./Tuple";
import { TupleArray } from "./TupleArray";
import { AbiParameter } from "abitype";
import {
AddressInput,
Expand All @@ -10,6 +12,7 @@ import {
IntegerInput,
IntegerVariant,
} from "~~/components/scaffold-eth";
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";

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

if (paramType.type === "address") {
return <AddressInput {...inputProps} />;
} else if (paramType.type === "bytes32") {
return <Bytes32Input {...inputProps} />;
} else if (paramType.type === "bytes") {
return <BytesInput {...inputProps} />;
} else if (paramType.type === "string") {
return <InputBase {...inputProps} />;
} else if (paramType.type.includes("int") && !paramType.type.includes("[")) {
return <IntegerInput {...inputProps} variant={paramType.type as IntegerVariant} />;
}
const renderInput = () => {
switch (paramType.type) {
case "address":
return <AddressInput {...inputProps} />;
case "bytes32":
return <Bytes32Input {...inputProps} />;
case "bytes":
return <BytesInput {...inputProps} />;
case "string":
return <InputBase {...inputProps} />;
case "tuple":
return (
<Tuple
setParentForm={setForm}
parentForm={form}
abiTupleParameter={paramType as AbiParameterTuple}
parentStateObjectKey={stateObjectKey}
/>
);
default:
// Handling 'int' types and 'tuple[]' types
if (paramType.type.includes("int") && !paramType.type.includes("[")) {
return <IntegerInput {...inputProps} variant={paramType.type as IntegerVariant} />;
} else if (paramType.type.startsWith("tuple[")) {
return (
<TupleArray
setParentForm={setForm}
parentForm={form}
abiTupleParameter={paramType as AbiParameterTuple}
parentStateObjectKey={stateObjectKey}
/>
);
} else {
return <InputBase {...inputProps} />;
}
}
};

return <InputBase {...inputProps} />;
return (
<div className="flex flex-col gap-1.5 w-full">
<div className="flex items-center ml-2">
{paramType.name && <span className="text-xs font-medium mr-2 leading-none">{paramType.name}</span>}
<span className="block text-xs font-extralight leading-none">{paramType.type}</span>
</div>
{renderInput()}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getFunctionInputKey,
getInitialFormState,
getParsedContractFunctionArgs,
transformAbiFunction,
} from "~~/app/debug/_components/contract";
import { getParsedError, notification } from "~~/utils/scaffold-eth";

Expand Down Expand Up @@ -42,7 +43,8 @@ export const ReadOnlyFunctionForm = ({
},
});

const inputElements = abiFunction.inputs.map((input, inputIndex) => {
const transformedFunction = transformAbiFunction(abiFunction);
const inputElements = transformedFunction.inputs.map((input, inputIndex) => {
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
return (
<ContractInput
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { ContractInput } from "./ContractInput";
import { getFunctionInputKey, getInitalTupleFormState } from "./utilsContract";
import { replacer } from "~~/utils/scaffold-eth/common";
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";

type TupleProps = {
abiTupleParameter: AbiParameterTuple;
setParentForm: Dispatch<SetStateAction<Record<string, any>>>;
parentStateObjectKey: string;
parentForm: Record<string, any> | undefined;
};

export const Tuple = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleProps) => {
const [form, setForm] = useState<Record<string, any>>(() => getInitalTupleFormState(abiTupleParameter));

useEffect(() => {
const values = Object.values(form);
const argsStruct: Record<string, any> = {};
abiTupleParameter.components.forEach((component, componentIndex) => {
argsStruct[component.name || `input_${componentIndex}_`] = values[componentIndex];
});

setParentForm(parentForm => ({ ...parentForm, [parentStateObjectKey]: JSON.stringify(argsStruct, replacer) }));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(form, replacer)]);

return (
<div>
<div className="collapse collapse-arrow bg-base-200 pl-4 py-1.5 border-2 border-secondary">
<input type="checkbox" className="min-h-fit peer" />
<div className="collapse-title p-0 min-h-fit peer-checked:mb-2 text-primary-content/50">
<p className="m-0 p-0 text-[1rem]">{abiTupleParameter.internalType}</p>
</div>
<div className="ml-3 flex-col space-y-4 border-secondary/80 border-l-2 pl-4 collapse-content">
{abiTupleParameter?.components?.map((param, index) => {
const key = getFunctionInputKey(abiTupleParameter.name || "tuple", param, index);
return <ContractInput setForm={setForm} form={form} key={key} stateObjectKey={key} paramType={param} />;
})}
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { ContractInput } from "./ContractInput";
import { getFunctionInputKey, getInitalTupleArrayFormState } from "./utilsContract";
import { replacer } from "~~/utils/scaffold-eth/common";
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";

type TupleArrayProps = {
abiTupleParameter: AbiParameterTuple & { isVirtual?: true };
setParentForm: Dispatch<SetStateAction<Record<string, any>>>;
parentStateObjectKey: string;
parentForm: Record<string, any> | undefined;
};

export const TupleArray = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleArrayProps) => {
const [form, setForm] = useState<Record<string, any>>(() => getInitalTupleArrayFormState(abiTupleParameter));
const [additionalInputs, setAdditionalInputs] = useState<Array<typeof abiTupleParameter.components>>([
abiTupleParameter.components,
]);

const depth = (abiTupleParameter.type.match(/\[\]/g) || []).length;

useEffect(() => {
// Extract and group fields based on index prefix
const groupedFields = Object.keys(form).reduce((acc, key) => {
const [indexPrefix, ...restArray] = key.split("_");
const componentName = restArray.join("_");
if (!acc[indexPrefix]) {
acc[indexPrefix] = {};
}
acc[indexPrefix][componentName] = form[key];
return acc;
}, {} as Record<string, Record<string, any>>);

let argsArray: Array<Record<string, any>> = [];

Object.keys(groupedFields).forEach(key => {
const currentKeyValues = Object.values(groupedFields[key]);

const argsStruct: Record<string, any> = {};
abiTupleParameter.components.forEach((component, componentIndex) => {
argsStruct[component.name || `input_${componentIndex}_`] = currentKeyValues[componentIndex];
});

argsArray.push(argsStruct);
});

if (depth > 1) {
argsArray = argsArray.map(args => {
return args[abiTupleParameter.components[0].name || "tuple"];
});
}

setParentForm(parentForm => {
return { ...parentForm, [parentStateObjectKey]: JSON.stringify(argsArray, replacer) };
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(form, replacer)]);

const addInput = () => {
setAdditionalInputs(previousValue => {
const newAdditionalInputs = [...previousValue, abiTupleParameter.components];

// Add the new inputs to the form
setForm(form => {
const newForm = { ...form };
abiTupleParameter.components.forEach((component, componentIndex) => {
const key = getFunctionInputKey(
`${newAdditionalInputs.length - 1}_${abiTupleParameter.name || "tuple"}`,
component,
componentIndex,
);
newForm[key] = "";
});
return newForm;
});

return newAdditionalInputs;
});
};

const removeInput = () => {
// Remove the last inputs from the form
setForm(form => {
const newForm = { ...form };
abiTupleParameter.components.forEach((component, componentIndex) => {
const key = getFunctionInputKey(
`${additionalInputs.length - 1}_${abiTupleParameter.name || "tuple"}`,
component,
componentIndex,
);
delete newForm[key];
});
return newForm;
});
setAdditionalInputs(inputs => inputs.slice(0, -1));
};

return (
<div>
<div className="collapse collapse-arrow bg-base-200 pl-4 py-1.5 border-2 border-secondary">
<input type="checkbox" className="min-h-fit peer" />
<div className="collapse-title p-0 min-h-fit peer-checked:mb-1 text-primary-content/50">
<p className="m-0 text-[1rem]">{abiTupleParameter.internalType}</p>
</div>
<div className="ml-3 flex-col space-y-2 border-secondary/70 border-l-2 pl-4 collapse-content">
{additionalInputs.map((additionalInput, additionalIndex) => (
<div key={additionalIndex} className="space-y-1">
<span className="badge bg-base-300 badge-sm">
{depth > 1 ? `${additionalIndex}` : `tuple[${additionalIndex}]`}
</span>
<div className="space-y-4">
{additionalInput.map((param, index) => {
const key = getFunctionInputKey(
`${additionalIndex}_${abiTupleParameter.name || "tuple"}`,
param,
index,
);
return (
<ContractInput setForm={setForm} form={form} key={key} stateObjectKey={key} paramType={param} />
);
})}
</div>
</div>
))}
<div className="flex space-x-2">
<button className="btn btn-sm btn-secondary" onClick={addInput}>
+
</button>
{additionalInputs.length > 0 && (
<button className="btn btn-sm btn-secondary" onClick={removeInput}>
-
</button>
)}
</div>
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getFunctionInputKey,
getInitialFormState,
getParsedContractFunctionArgs,
transformAbiFunction,
} from "~~/app/debug/_components/contract";
import { IntegerInput } from "~~/components/scaffold-eth";
import { useTransactor } from "~~/hooks/scaffold-eth";
Expand Down Expand Up @@ -72,7 +73,8 @@ export const WriteOnlyFunctionForm = ({
}, [txResult]);

// TODO use `useMemo` to optimize also update in ReadOnlyFunctionForm
const inputs = abiFunction.inputs.map((input, inputIndex) => {
const transformedFunction = transformAbiFunction(abiFunction);
const inputs = transformedFunction.inputs.map((input, inputIndex) => {
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
return (
<ContractInput
Expand All @@ -98,14 +100,20 @@ export const WriteOnlyFunctionForm = ({
</p>
{inputs}
{abiFunction.stateMutability === "payable" ? (
<IntegerInput
value={txValue}
onChange={updatedTxValue => {
setDisplayedTxResult(undefined);
setTxValue(updatedTxValue);
}}
placeholder="value (wei)"
/>
<div className="flex flex-col gap-1.5 w-full">
<div className="flex items-center ml-2">
<span className="text-xs font-medium mr-2 leading-none">payable value</span>
<span className="block text-xs font-extralight leading-none">wei</span>
</div>
<IntegerInput
value={txValue}
onChange={updatedTxValue => {
setDisplayedTxResult(undefined);
setTxValue(updatedTxValue);
}}
placeholder="value (wei)"
/>
</div>
) : null}
<div className="flex justify-between gap-2">
{!zeroInputs && (
Expand Down
Loading