From baa9d8698cea64ec7ace90b53ff7f3d155feb2ab Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Wed, 21 Feb 2024 12:16:01 +0530 Subject: [PATCH 1/3] add all tuple utils and components from SE-2 --- .../scaffold-eth/Contract/ContractInput.tsx | 25 ++- .../Contract/ReadOnlyFunctionForm.tsx | 4 +- .../scaffold-eth/Contract/Tuple.tsx | 44 ++++ .../scaffold-eth/Contract/TupleArray.tsx | 139 +++++++++++++ .../Contract/WriteOnlyFunctionForm.tsx | 4 +- .../scaffold-eth/Contract/utilsContract.tsx | 190 +++++++++++++----- .../nextjs/utils/scaffold-eth/contract.ts | 3 + 7 files changed, 356 insertions(+), 53 deletions(-) create mode 100644 packages/nextjs/components/scaffold-eth/Contract/Tuple.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/TupleArray.tsx diff --git a/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx b/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx index 89f0ea5c..efb6c90b 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx @@ -1,4 +1,6 @@ import { Dispatch, SetStateAction } from "react"; +import { Tuple } from "./Tuple"; +import { TupleArray } from "./TupleArray"; import { AbiParameter } from "abitype"; import { AddressInput, @@ -8,6 +10,7 @@ import { IntegerInput, IntegerVariant, } from "~~/components/scaffold-eth"; +import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract"; type ContractInputProps = { setForm: Dispatch>>; @@ -32,17 +35,35 @@ export const ContractInput = ({ setForm, form, stateObjectKey, paramType }: Cont const renderInput = () => { switch (paramType.type) { case "address": - const addressInputProps = { ...inputProps, placeholder: "address or ENS" }; - return ; + 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 ; } diff --git a/packages/nextjs/components/scaffold-eth/Contract/ReadOnlyFunctionForm.tsx b/packages/nextjs/components/scaffold-eth/Contract/ReadOnlyFunctionForm.tsx index 29b6d12b..de1a8e74 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/ReadOnlyFunctionForm.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/ReadOnlyFunctionForm.tsx @@ -10,6 +10,7 @@ import { getInitialFormState, getParsedContractFunctionArgs, getParsedError, + transformAbiFunction, } from "~~/components/scaffold-eth"; import { useAbiNinjaState } from "~~/services/store/store"; import { notification } from "~~/utils/scaffold-eth"; @@ -44,7 +45,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/components/scaffold-eth/Contract/TupleArray.tsx b/packages/nextjs/components/scaffold-eth/Contract/TupleArray.tsx new file mode 100644 index 00000000..1eb23c25 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/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/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx b/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx index 33686f34..81f4b4cd 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx @@ -12,6 +12,7 @@ import { getInitialFormState, getParsedContractFunctionArgs, getParsedError, + transformAbiFunction, } from "~~/components/scaffold-eth"; import { useTransactor } from "~~/hooks/scaffold-eth"; import { useAbiNinjaState } from "~~/services/store/store"; @@ -75,7 +76,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 ( { const name = input?.name || `input_${inputIndex}_`; return functionName + "_" + name + "_" + input.internalType + "_" + input.type; }; +const isJsonString = (str: string) => { + 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 + */ +const getParsedContractFunctionArgs = (form: Record) => { + return Object.keys(form).map(key => { + const valueOfArg = form[key]; + + // Attempt to deeply parse JSON strings + return deepParseValues(valueOfArg); + }); +}; + +const getInitialFormState = (abiFunction: AbiFunction) => { + const initialForm: Record = {}; + if (!abiFunction.inputs) return initialForm; + abiFunction.inputs.forEach((input, inputIndex) => { + const key = getFunctionInputKey(abiFunction.name, input, inputIndex); + initialForm[key] = ""; + }); + return initialForm; +}; + +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)), + }; +}; + /** - * Parses an error to get a displayable string + * Parses an viem/wagmi error to get a displayable string * @param e - error object - * @returns {string} parsed error string + * @returns parsed error string */ const getParsedError = (e: any): string => { let message: string = e.message ?? "An unknown error occurred"; @@ -43,47 +170,12 @@ const getParsedError = (e: any): string => { return message; }; -// This regex is used to identify array types in the form of `type[size]` -const ARRAY_TYPE_REGEX = /\[.*\]$/; - -/** - * Parses form input with array support - * @param {Record} form - form object containing key value pairs - * @returns parsed error string - */ -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 parsedArguments; -}; - -const getInitialFormState = (abiFunction: AbiFunction) => { - const initialForm: Record = {}; - if (!abiFunction.inputs) return initialForm; - abiFunction.inputs.forEach((input, inputIndex) => { - const key = getFunctionInputKey(abiFunction.name, input, inputIndex); - initialForm[key] = ""; - }); - return initialForm; +export { + getFunctionInputKey, + getInitialFormState, + getParsedContractFunctionArgs, + getInitalTupleFormState, + getInitalTupleArrayFormState, + transformAbiFunction, + getParsedError, }; - -export { getFunctionInputKey, getInitialFormState, getParsedContractFunctionArgs, getParsedError }; diff --git a/packages/nextjs/utils/scaffold-eth/contract.ts b/packages/nextjs/utils/scaffold-eth/contract.ts index e9ad461f..d155187e 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, @@ -272,3 +273,5 @@ export type UseScaffoldEventHistoryData< }[] > | undefined; + +export type AbiParameterTuple = Extract; From 20158187b61512410800cbcf6289dc381c84dff5 Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Wed, 21 Feb 2024 12:55:34 +0530 Subject: [PATCH 2/3] minro tweaks to styles --- .../components/scaffold-eth/Contract/ContractInput.tsx | 8 ++++---- .../nextjs/components/scaffold-eth/Contract/Tuple.tsx | 4 ++-- .../components/scaffold-eth/Contract/TupleArray.tsx | 6 +++--- .../scaffold-eth/Contract/WriteOnlyFunctionForm.tsx | 10 +++++----- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx b/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx index efb6c90b..95f77768 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx @@ -71,10 +71,10 @@ export const ContractInput = ({ setForm, form, stateObjectKey, paramType }: Cont }; return ( -
-
- {paramType.name && {paramType.name}} - {paramType.type} +
+
+ {paramType.name && {paramType.name}} + {paramType.type}
{renderInput()}
diff --git a/packages/nextjs/components/scaffold-eth/Contract/Tuple.tsx b/packages/nextjs/components/scaffold-eth/Contract/Tuple.tsx index 0e3175db..6000d262 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/Tuple.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/Tuple.tsx @@ -27,9 +27,9 @@ export const Tuple = ({ abiTupleParameter, setParentForm, parentStateObjectKey } return (
-
+
-
+

{abiTupleParameter.internalType}

diff --git a/packages/nextjs/components/scaffold-eth/Contract/TupleArray.tsx b/packages/nextjs/components/scaffold-eth/Contract/TupleArray.tsx index 1eb23c25..01f30322 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/TupleArray.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/TupleArray.tsx @@ -97,15 +97,15 @@ export const TupleArray = ({ abiTupleParameter, setParentForm, parentStateObject return (
-
+
-
+

{abiTupleParameter.internalType}

{additionalInputs.map((additionalInput, additionalIndex) => (
- + {depth > 1 ? `${additionalIndex}` : `tuple[${additionalIndex}]`}
diff --git a/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx b/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx index 81f4b4cd..6311c642 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx @@ -103,10 +103,10 @@ export const WriteOnlyFunctionForm = ({

{inputs} {abiFunction.stateMutability === "payable" ? ( -
-
- value - (wei) +
+
+ payable value + wei
) : null} From b6feb5ae63d88d34f2d6edcddbd8ac539ae3c5b8 Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Wed, 21 Feb 2024 14:06:07 +0530 Subject: [PATCH 3/3] add overflow-x-auto to Tuple nd TupleArray --- packages/nextjs/components/scaffold-eth/Contract/Tuple.tsx | 2 +- packages/nextjs/components/scaffold-eth/Contract/TupleArray.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/components/scaffold-eth/Contract/Tuple.tsx b/packages/nextjs/components/scaffold-eth/Contract/Tuple.tsx index 6000d262..68b8d893 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/Tuple.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/Tuple.tsx @@ -27,7 +27,7 @@ export const Tuple = ({ abiTupleParameter, setParentForm, parentStateObjectKey } return (
-
+

{abiTupleParameter.internalType}

diff --git a/packages/nextjs/components/scaffold-eth/Contract/TupleArray.tsx b/packages/nextjs/components/scaffold-eth/Contract/TupleArray.tsx index 01f30322..a7fb53d6 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/TupleArray.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/TupleArray.tsx @@ -97,7 +97,7 @@ export const TupleArray = ({ abiTupleParameter, setParentForm, parentStateObject return (
-
+

{abiTupleParameter.internalType}