diff --git a/packages/nextjs/app/debug/_components/contract/ContractInput.tsx b/packages/nextjs/app/debug/_components/contract/ContractInput.tsx index e27c56ee4..e5e5f93a3 100644 --- a/packages/nextjs/app/debug/_components/contract/ContractInput.tsx +++ b/packages/nextjs/app/debug/_components/contract/ContractInput.tsx @@ -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, @@ -10,6 +12,7 @@ import { IntegerInput, IntegerVariant, } from "~~/components/scaffold-eth"; +import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract"; type ContractInputProps = { setForm: Dispatch>>; @@ -31,17 +34,51 @@ export const ContractInput = ({ setForm, form, stateObjectKey, paramType }: Cont }, }; - if (paramType.type === "address") { - return ; - } else if (paramType.type === "bytes32") { - return ; - } else if (paramType.type === "bytes") { - return ; - } else if (paramType.type === "string") { - return ; - } else if (paramType.type.includes("int") && !paramType.type.includes("[")) { - return ; - } + const renderInput = () => { + switch (paramType.type) { + case "address": + return ; + case "bytes32": + return ; + case "bytes": + return ; + case "string": + return ; + case "tuple": + return ( + + ); + default: + // Handling 'int' types and 'tuple[]' types + if (paramType.type.includes("int") && !paramType.type.includes("[")) { + return ; + } else if (paramType.type.startsWith("tuple[")) { + return ( + + ); + } else { + return ; + } + } + }; - return ; + return ( +
+
+ {paramType.name && {paramType.name}} + {paramType.type} +
+ {renderInput()} +
+ ); }; diff --git a/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx b/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx index 42013eddf..91bae120b 100644 --- a/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx +++ b/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx @@ -11,6 +11,7 @@ import { getFunctionInputKey, getInitialFormState, getParsedContractFunctionArgs, + transformAbiFunction, } from "~~/app/debug/_components/contract"; import { getParsedError, notification } from "~~/utils/scaffold-eth"; @@ -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 ( >>; + parentStateObjectKey: string; + parentForm: Record | undefined; +}; + +export const Tuple = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleProps) => { + const [form, setForm] = useState>(() => getInitalTupleFormState(abiTupleParameter)); + + useEffect(() => { + const values = Object.values(form); + const argsStruct: Record = {}; + 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 ( +
+
+ +
+

{abiTupleParameter.internalType}

+
+
+ {abiTupleParameter?.components?.map((param, index) => { + const key = getFunctionInputKey(abiTupleParameter.name || "tuple", param, index); + return ; + })} +
+
+
+ ); +}; diff --git a/packages/nextjs/app/debug/_components/contract/TupleArray.tsx b/packages/nextjs/app/debug/_components/contract/TupleArray.tsx new file mode 100644 index 000000000..69786ba32 --- /dev/null +++ b/packages/nextjs/app/debug/_components/contract/TupleArray.tsx @@ -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>>; + parentStateObjectKey: string; + parentForm: Record | undefined; +}; + +export const TupleArray = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleArrayProps) => { + const [form, setForm] = useState>(() => getInitalTupleArrayFormState(abiTupleParameter)); + const [additionalInputs, setAdditionalInputs] = useState>([ + 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>); + + let argsArray: Array> = []; + + Object.keys(groupedFields).forEach(key => { + const currentKeyValues = Object.values(groupedFields[key]); + + const argsStruct: Record = {}; + 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 ( +
+
+ +
+

{abiTupleParameter.internalType}

+
+
+ {additionalInputs.map((additionalInput, additionalIndex) => ( +
+

+ {depth > 1 ? `${additionalIndex}` : `tuple[${additionalIndex}]`} +

+
+ {additionalInput.map((param, index) => { + const key = getFunctionInputKey( + `${additionalIndex}_${abiTupleParameter.name || "tuple"}`, + param, + index, + ); + return ( + + ); + })} +
+
+ ))} +
+ + {additionalInputs.length > 0 && ( + + )} +
+
+
+
+ ); +}; diff --git a/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx b/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx index d7135018e..f15d6e1e0 100644 --- a/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx +++ b/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx @@ -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"; @@ -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 ( { + try { + JSON.parse(str); + return true; + } catch (e) { + return false; + } +}; + +// Recursive function to deeply parse JSON strings, correctly handling nested arrays and encoded JSON strings +const deepParseValues = (value: any): any => { + if (typeof value === "string") { + if (isJsonString(value)) { + const parsed = JSON.parse(value); + return deepParseValues(parsed); + } else { + // It's a string but not a JSON string, return as is + return value; + } + } else if (Array.isArray(value)) { + // If it's an array, recursively parse each element + return value.map(element => deepParseValues(element)); + } else if (typeof value === "object" && value !== null) { + // If it's an object, recursively parse each value + return Object.entries(value).reduce((acc: any, [key, val]) => { + acc[key] = deepParseValues(val); + return acc; + }, {}); + } + + // Handle boolean values represented as strings + if (value === "true" || value === "1" || value === "0x1" || value === "0x01" || value === "0x0001") { + return true; + } else if (value === "false" || value === "0" || value === "0x0" || value === "0x00" || value === "0x0000") { + return false; + } + + return value; +}; /** - * Parses form input with array support + * parses form input with array support */ const getParsedContractFunctionArgs = (form: Record) => { - const keys = Object.keys(form); - const parsedArguments = keys.map(key => { - try { - const keySplitArray = key.split("_"); - const baseTypeOfArg = keySplitArray[keySplitArray.length - 1]; - let valueOfArg = form[key]; - - if (ARRAY_TYPE_REGEX.test(baseTypeOfArg) || baseTypeOfArg === "tuple") { - valueOfArg = JSON.parse(valueOfArg); - } else if (baseTypeOfArg === "bool") { - if (["true", "1", "0x1", "0x01", "0x0001"].includes(valueOfArg)) { - valueOfArg = 1; - } else { - valueOfArg = 0; - } - } - return valueOfArg; - } catch (error: any) { - // ignore error, it will be handled when sending/reading from a function - } + return Object.keys(form).map(key => { + const valueOfArg = form[key]; + + // Attempt to deeply parse JSON strings + return deepParseValues(valueOfArg); }); - return parsedArguments; }; const getInitialFormState = (abiFunction: AbiFunction) => { @@ -49,4 +71,79 @@ const getInitialFormState = (abiFunction: AbiFunction) => { return initialForm; }; -export { getFunctionInputKey, getInitialFormState, getParsedContractFunctionArgs }; +const getInitalTupleFormState = (abiTupleParameter: AbiParameterTuple) => { + const initialForm: Record = {}; + if (abiTupleParameter.components.length === 0) return initialForm; + + abiTupleParameter.components.forEach((component, componentIndex) => { + const key = getFunctionInputKey(abiTupleParameter.name || "tuple", component, componentIndex); + initialForm[key] = ""; + }); + return initialForm; +}; + +const getInitalTupleArrayFormState = (abiTupleParameter: AbiParameterTuple) => { + const initialForm: Record = {}; + if (abiTupleParameter.components.length === 0) return initialForm; + abiTupleParameter.components.forEach((component, componentIndex) => { + const key = getFunctionInputKey("0_" + abiTupleParameter.name || "tuple", component, componentIndex); + initialForm[key] = ""; + }); + return initialForm; +}; + +const adjustInput = (input: AbiParameterTuple): AbiParameter => { + if (input.type.startsWith("tuple[")) { + const depth = (input.type.match(/\[\]/g) || []).length; + return { + ...input, + components: transformComponents(input.components, depth, { + internalType: input.internalType || "struct", + name: input.name, + }), + }; + } else if (input.components) { + return { + ...input, + components: input.components.map(value => adjustInput(value as AbiParameterTuple)), + }; + } + return input; +}; + +const transformComponents = ( + components: readonly AbiParameter[], + depth: number, + parentComponentData: { internalType?: string; name?: string }, +): AbiParameter[] => { + // Base case: if depth is 1 or no components, return the original components + if (depth === 1 || !components) { + return [...components]; + } + + // Recursive case: wrap components in an additional tuple layer + const wrappedComponents: AbiParameter = { + internalType: `${parentComponentData.internalType || "struct"}`.replace(/\[\]/g, "") + "[]".repeat(depth - 1), + name: `${parentComponentData.name || "tuple"}`, + type: `tuple${"[]".repeat(depth - 1)}`, + components: transformComponents(components, depth - 1, parentComponentData), + }; + + return [wrappedComponents]; +}; + +const transformAbiFunction = (abiFunction: AbiFunction): AbiFunction => { + return { + ...abiFunction, + inputs: abiFunction.inputs.map(value => adjustInput(value as AbiParameterTuple)), + }; +}; + +export { + getFunctionInputKey, + getInitialFormState, + getParsedContractFunctionArgs, + getInitalTupleFormState, + getInitalTupleArrayFormState, + transformAbiFunction, +}; diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 49c70229f..c77574eef 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -20,7 +20,7 @@ "@uniswap/sdk-core": "^4.0.1", "@uniswap/v2-sdk": "^3.0.1", "blo": "^1.0.1", - "daisyui": "^4.4.19", + "daisyui": "4.5.0", "next": "^14.0.4", "next-themes": "^0.2.1", "nprogress": "^0.2.0", diff --git a/packages/nextjs/utils/scaffold-eth/contract.ts b/packages/nextjs/utils/scaffold-eth/contract.ts index ac7fe4a39..f092d5a49 100644 --- a/packages/nextjs/utils/scaffold-eth/contract.ts +++ b/packages/nextjs/utils/scaffold-eth/contract.ts @@ -1,5 +1,6 @@ import { Abi, + AbiParameter, AbiParameterToPrimitiveType, AbiParametersToPrimitiveTypes, ExtractAbiEvent, @@ -290,3 +291,5 @@ export type UseScaffoldEventHistoryData< }[] > | undefined; + +export type AbiParameterTuple = Extract; diff --git a/yarn.lock b/yarn.lock index 64887abe9..cdd9ea40e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1871,7 +1871,7 @@ __metadata: "@uniswap/v2-sdk": ^3.0.1 autoprefixer: ^10.4.12 blo: ^1.0.1 - daisyui: ^4.4.19 + daisyui: 4.5.0 eslint: ^8.15.0 eslint-config-next: ^14.0.4 eslint-config-prettier: ^8.5.0 @@ -5448,7 +5448,7 @@ __metadata: languageName: node linkType: hard -"daisyui@npm:^4.4.19": +"daisyui@npm:4.5.0": version: 4.5.0 resolution: "daisyui@npm:4.5.0" dependencies: