From bd5dc11fc7283ed6b24b03f5fbbb987dd7784fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= Date: Tue, 5 Mar 2024 17:47:18 +0100 Subject: [PATCH 1/9] WIP: Making the action composer support arrays and tuples --- components/input/function-call-form.tsx | 65 +++++++ ...ncoding-form.tsx => function-selector.tsx} | 161 ++++-------------- components/input/input-parameter-array.tsx | 71 ++++++++ components/input/input-parameter-text.tsx | 47 +++++ .../input/input-parameter-tuple-array.tsx | 69 ++++++++ components/input/input-parameter-tuple.tsx | 47 +++++ components/input/input-parameter.tsx | 28 +++ plugins/dualGovernance/pages/new.tsx | 6 +- plugins/tokenVoting/pages/new.tsx | 6 +- utils/input-values.ts | 141 +++++++++++++++ 10 files changed, 504 insertions(+), 137 deletions(-) create mode 100644 components/input/function-call-form.tsx rename components/input/{function-encoding-form.tsx => function-selector.tsx} (52%) create mode 100644 components/input/input-parameter-array.tsx create mode 100644 components/input/input-parameter-text.tsx create mode 100644 components/input/input-parameter-tuple-array.tsx create mode 100644 components/input/input-parameter-tuple.tsx create mode 100644 components/input/input-parameter.tsx create mode 100644 utils/input-values.ts diff --git a/components/input/function-call-form.tsx b/components/input/function-call-form.tsx new file mode 100644 index 00000000..5a7d3d05 --- /dev/null +++ b/components/input/function-call-form.tsx @@ -0,0 +1,65 @@ +import { FC, useState } from "react"; +import { Address, Hex } from "viem"; +import { InputText } from "@aragon/ods"; +import { PleaseWaitSpinner } from "@/components/please-wait"; +import { isAddress } from "@/utils/evm"; +import { Action } from "@/utils/types"; +import { Else, ElseIf, If, Then } from "@/components/if"; +import { useAbi } from "@/hooks/useAbi"; +import { FunctionSelector } from "./function-selector"; + +interface FunctionCallFormProps { + onAddAction: (action: Action) => any; +} +export const FunctionCallForm: FC = ({ + onAddAction, +}) => { + const [targetContract, setTargetContract] = useState(""); + const { abi, isLoading: loadingAbi } = useAbi(targetContract as Address); + + const actionEntered = (data: Hex, value: bigint) => { + onAddAction({ + to: targetContract, + value, + data, + }); + }; + + return ( +
+
+ setTargetContract(e.target.value || "")} + /> +
+ + +
+ +
+
+ +

Enter the address of the contract to interact with

+
+ +

The address of the contract is not valid

+
+ +

The ABI of the contract is not publicly available

+
+ + + +
+
+ ); +}; diff --git a/components/input/function-encoding-form.tsx b/components/input/function-selector.tsx similarity index 52% rename from components/input/function-encoding-form.tsx rename to components/input/function-selector.tsx index f340581a..7cde5680 100644 --- a/components/input/function-encoding-form.tsx +++ b/components/input/function-selector.tsx @@ -1,89 +1,32 @@ -import { FC, useState } from "react"; -import { Address, Hex, encodeFunctionData } from "viem"; +import { useState } from "react"; +import { Hex, encodeFunctionData } from "viem"; import { Button, InputText } from "@aragon/ods"; import { AbiFunction } from "abitype"; -import { PleaseWaitSpinner } from "@/components/please-wait"; -import { isAddress } from "@/utils/evm"; -import { Action } from "@/utils/types"; -import { Else, ElseIf, If, Then } from "@/components/if"; -import { useAbi } from "@/hooks/useAbi"; +import { Else, If, Then } from "@/components/if"; import { decodeCamelCase } from "@/utils/case"; import { useAlertContext } from "@/context/AlertContext"; +import { InputParameter } from "./input-parameter"; +import { InputValue, isValidValue } from "@/utils/input-values"; -interface FunctionEncodingFormProps { - onAddAction: (action: Action) => any; +interface IFunctionSelectorProps { + abi: AbiFunction[]; + actionEntered: (calldata: Hex, value: bigint) => void; } -export const FunctionEncodingForm: FC = ({ - onAddAction, -}) => { - const [targetContract, setTargetContract] = useState(""); - const { abi, isLoading: loadingAbi } = useAbi(targetContract as Address); - - const actionEntered = (data: Hex, value: bigint) => { - onAddAction({ - to: targetContract, - value, - data, - }); - }; - - return ( -
-
- setTargetContract(e.target.value || "")} - /> -
- - -
- -
-
- -

Enter the address of the contract to interact with

-
- -

The address of the contract is not valid

-
- -

The ABI of the contract is not publicly available

-
- - - -
-
- ); -}; - -const FunctionSelector = ({ +export const FunctionSelector = ({ abi, actionEntered, -}: { - abi: AbiFunction[]; - actionEntered: (calldata: Hex, value: bigint) => void; -}) => { +}: IFunctionSelectorProps) => { const { addAlert } = useAlertContext(); const [selectedAbiItem, setSelectedAbiItem] = useState< AbiFunction | undefined >(); - const [abiInputValues, setAbiInputValues] = useState([]); + const [inputValues, setInputValues] = useState([]); const [value, setValue] = useState(""); - const onFunctionParameterChange = (idx: number, value: string) => { - const newInputValues = [...abiInputValues]; - newInputValues[idx] = value; - setAbiInputValues(newInputValues); + const onParameterChange = (paramIdx: number, value: InputValue) => { + const newInputValues = [...inputValues]; + newInputValues[paramIdx] = value; + setInputValues(newInputValues); }; const onAddAction = () => { @@ -93,12 +36,12 @@ const FunctionSelector = ({ let invalidParams = false; if (!abi?.length) invalidParams = true; else if (!selectedAbiItem?.name) invalidParams = true; - else if (selectedAbiItem.inputs.length !== abiInputValues.length) + else if (selectedAbiItem.inputs.length !== inputValues.length) invalidParams = true; for (const i in selectedAbiItem.inputs) { const item = selectedAbiItem.inputs[i]; - if (hasTypeError(abiInputValues[i], item.type)) { + if (!isValidValue(inputValues[i], item)) { invalidParams = true; break; } @@ -124,7 +67,7 @@ const FunctionSelector = ({ const booleanIdxs = selectedAbiItem.inputs .map((inp, i) => (inp.type === "bool" ? i : -1)) .filter((v) => v >= 0); - const args: any[] = [].concat(abiInputValues as any) as string[]; + const args: any[] = [].concat(inputValues as any) as string[]; for (const i of booleanIdxs) { if (["false", "False", "no", "No"].includes(args[i])) args[i] = false; else args[i] = true; @@ -137,7 +80,7 @@ const FunctionSelector = ({ }); actionEntered(data, BigInt(value ?? "0")); - setAbiInputValues([]); + setInputValues([]); } catch (err) { console.error(err); addAlert("Invalid parameters", { @@ -193,31 +136,21 @@ const FunctionSelector = ({ font-size: 1rem; } `} - {selectedAbiItem?.inputs.map((argument, i) => ( + {selectedAbiItem?.inputs.map((paramAbi, i) => (
- - onFunctionParameterChange(i, e.target.value) - } +
))} - +
); }; - -function hasTypeError(value: string, solidityType: string): boolean { - if (!value || !solidityType) return false; - - switch (solidityType) { - case "address": - return !/^0x[0-9a-fA-F]{40}$/.test(value); - case "bytes": - return value.length % 2 !== 0 || !/^0x[0-9a-fA-F]*$/.test(value); - case "string": - return false; - case "bool": - return ![ - "true", - "false", - "True", - "False", - "yes", - "no", - "Yes", - "No", - ].includes(value); - case "tuple": - return false; - } - if (solidityType.match(/^bytes[0-9]{1,2}$/)) { - return value.length % 2 !== 0 || !/^0x[0-9a-fA-F]+$/.test(value); - } else if (solidityType.match(/^uint[0-9]+$/)) { - return value.length % 2 !== 0 || !/^[0-9]*$/.test(value); - } else if (solidityType.match(/^int[0-9]+$/)) { - return value.length % 2 !== 0 || !/^-?[0-9]*$/.test(value); - } - return false; -} diff --git a/components/input/input-parameter-array.tsx b/components/input/input-parameter-array.tsx new file mode 100644 index 00000000..2f424358 --- /dev/null +++ b/components/input/input-parameter-array.tsx @@ -0,0 +1,71 @@ +import { useState } from "react"; +import { Button, InputText } from "@aragon/ods"; +import { AbiParameter } from "viem"; +import { decodeCamelCase } from "@/utils/case"; +import { + InputValue, + handleStringValue, + isValidStringValue, +} from "@/utils/input-values"; + +interface IInputParameterArrayProps { + abi: AbiParameter; + idx: number; + onChange: (paramIdx: number, value: InputValue) => any; +} + +export const InputParameterArray = ({ + abi, + idx, + onChange, +}: IInputParameterArrayProps) => { + const [value, setValue] = useState([""]); + + const onItemChange = (i: number, newVal: string) => { + const newArray = ([] as string[]).concat(value); + newArray[i] = newVal; + setValue(newArray); + + const transformedItems = newArray.map((item) => + handleStringValue(item, abi.type) + ); + if (transformedItems.some((item) => item === null)) return; + + onChange(idx, transformedItems as InputValue[]); + }; + + const addMore = () => { + const newValue = [...value, ""]; + setValue(newValue); + }; + + return ( +
+ {value.map((item, i) => ( + 0 ? "mt-3" : ""} + label={ + i === 0 + ? (abi.name + ? decodeCamelCase(abi.name) + : "Parameter " + (i + 1)) + " (list)" + : undefined + } + placeholder={ + abi.type?.replace(/\[\]$/, "") || decodeCamelCase(abi.name) || "" + } + variant={ + isValidStringValue(item[i], abi.type) ? "default" : "critical" + } + value={item[i] || ""} + onChange={(e) => onItemChange(i, e.target.value)} + /> + ))} +
+ +
+
+ ); +}; diff --git a/components/input/input-parameter-text.tsx b/components/input/input-parameter-text.tsx new file mode 100644 index 00000000..85500375 --- /dev/null +++ b/components/input/input-parameter-text.tsx @@ -0,0 +1,47 @@ +import { InputText } from "@aragon/ods"; +import { AbiParameter } from "viem"; +import { decodeCamelCase } from "@/utils/case"; +import { + InputValue, + isValidStringValue, + handleStringValue, +} from "@/utils/input-values"; +import { useState } from "react"; + +interface IInputParameterTextProps { + abi: AbiParameter; + idx: number; + onChange: (paramIdx: number, value: InputValue) => any; +} + +export const InputParameterText = ({ + abi, + idx, + onChange, +}: IInputParameterTextProps) => { + const [value, setvalue] = useState(""); + + const handleValue = (val: string) => { + setvalue(val); + + const parsedValue = handleStringValue(val, abi.type); + if (parsedValue === null) return; + + onChange(idx, parsedValue); + }; + + return ( + handleValue(e.target.value)} + /> + ); +}; diff --git a/components/input/input-parameter-tuple-array.tsx b/components/input/input-parameter-tuple-array.tsx new file mode 100644 index 00000000..da84ec76 --- /dev/null +++ b/components/input/input-parameter-tuple-array.tsx @@ -0,0 +1,69 @@ +import { useState } from "react"; +import { Button, Tag } from "@aragon/ods"; +import { AbiParameter } from "viem"; +import { + InputValue, + handleStringValue, + isValidStringValue, +} from "@/utils/input-values"; +import { InputParameterTuple } from "./input-parameter-tuple"; +import { decodeCamelCase } from "@/utils/case"; + +interface IInputParameterTupleArrayProps { + abi: AbiParameter; + idx: number; + onChange: (paramIdx: number, value: InputValue) => any; +} + +export const InputParameterTupleArray = ({ + abi, + idx, + onChange, +}: IInputParameterTupleArrayProps) => { + const [value, setValue] = useState([[]]); + + const onItemChange = (i: number, newVal: InputValue) => { + console.log(i, newVal); + + // const newArray = ([] as string[][]).concat(value); + // newArray[i] = newVal; + // setValue(newArray); + // const transformedItems = newArray.map((item) => + // handleStringValue(item, abi.type) + // ); + // if (transformedItems.some((item) => item === null)) return; + // onChange(idx, transformedItems as InputValue[]); + }; + + const addMore = () => { + const newValue = [...value, []]; + setValue(newValue); + }; + + return ( +
+ {value.map((_, i) => ( +
+
+

+ {abi.name ? decodeCamelCase(abi.name) : "Parameter " + (idx + 1)} +

+ +
+ + +
+ ))} +
+ +
+
+ ); +}; diff --git a/components/input/input-parameter-tuple.tsx b/components/input/input-parameter-tuple.tsx new file mode 100644 index 00000000..b3eeb915 --- /dev/null +++ b/components/input/input-parameter-tuple.tsx @@ -0,0 +1,47 @@ +import { useState } from "react"; +import { AbiParameter } from "viem"; +import { decodeCamelCase } from "@/utils/case"; +import { InputParameter } from "./input-parameter"; +import { InputValue } from "@/utils/input-values"; +import { If } from "../if"; + +interface IInputParameterTupleProps { + abi: AbiParameter; + idx: number; + onChange: (paramIdx: number, value: InputValue) => any; + hideTitle?: boolean; +} + +export const InputParameterTuple = ({ + abi, + idx, + onChange, + hideTitle, +}: IInputParameterTupleProps) => { + const [value, setValue] = useState>({}); + // const ohChange = (idx: number, value: string) => { + // const newInputValues = [...abiInputValues]; + // newInputValues[idx] = value; + // setAbiInputValues(newInputValues); + // }; + + const components: AbiParameter[] = (abi as any).components || []; + + return ( +
+ +

+ {abi.name ? decodeCamelCase(abi.name) : "Parameter " + (idx + 1)} +

+
+ +
+ {components.map((item, i) => ( +
+ +
+ ))} +
+
+ ); +}; diff --git a/components/input/input-parameter.tsx b/components/input/input-parameter.tsx new file mode 100644 index 00000000..07e62fd1 --- /dev/null +++ b/components/input/input-parameter.tsx @@ -0,0 +1,28 @@ +import { AbiParameter } from "viem"; +import { InputParameterTupleArray } from "./input-parameter-tuple-array"; +import { InputParameterTuple } from "./input-parameter-tuple"; +import { InputParameterArray } from "./input-parameter-array"; +import { InputParameterText } from "./input-parameter-text"; +import { InputValue } from "@/utils/input-values"; + +interface IInputParameterProps { + abi: AbiParameter; + idx: number; + onChange: (paramIdx: number, value: InputValue) => any; +} + +export const InputParameter = ({ + abi, + idx, + onChange, +}: IInputParameterProps) => { + if (abi.type === "tuple[]") { + return ; + } else if (abi.type.endsWith("[]")) { + return ; + } else if (abi.type === "tuple") { + return ; + } else { + return ; + } +}; diff --git a/plugins/dualGovernance/pages/new.tsx b/plugins/dualGovernance/pages/new.tsx index 4c1f1671..a9d1b87b 100644 --- a/plugins/dualGovernance/pages/new.tsx +++ b/plugins/dualGovernance/pages/new.tsx @@ -13,7 +13,7 @@ import { toHex } from "viem"; import { OptimisticTokenVotingPluginAbi } from "@/plugins/dualGovernance/artifacts/OptimisticTokenVotingPlugin.sol"; import { useAlertContext } from "@/context/AlertContext"; import WithdrawalInput from "@/components/input/withdrawal"; -import { FunctionEncodingForm } from "@/components/input/function-encoding-form"; +import { FunctionCallForm } from "@/components/input/function-call-form"; import { Action } from "@/utils/types"; import { getPlainText } from "@/utils/html"; import { useRouter } from "next/router"; @@ -245,7 +245,7 @@ export default function Create() { ? "text-primary-400" : "text-neutral-400") } - icon={IconType.APP_PROPOSALS} + icon={IconType.BLOCKCHAIN_BLOCKCHAIN} size="lg" /> @@ -258,7 +258,7 @@ export default function Create() { )} {actionType === ActionType.Custom && ( - setActions(actions.concat([action]))} /> )} diff --git a/plugins/tokenVoting/pages/new.tsx b/plugins/tokenVoting/pages/new.tsx index 35787127..8cf0a771 100644 --- a/plugins/tokenVoting/pages/new.tsx +++ b/plugins/tokenVoting/pages/new.tsx @@ -13,7 +13,7 @@ import { toHex } from "viem"; import { TokenVotingAbi } from "@/plugins/tokenVoting/artifacts/TokenVoting.sol"; import { useAlertContext } from "@/context/AlertContext"; import WithdrawalInput from "@/components/input/withdrawal"; -import { FunctionEncodingForm } from "@/components/input/function-encoding-form"; +import { FunctionCallForm } from "@/components/input/function-call-form"; import { Action } from "@/utils/types"; import { getPlainText } from "@/utils/html"; import { useRouter } from "next/router"; @@ -244,7 +244,7 @@ export default function Create() { ? "text-primary-400" : "text-neutral-400") } - icon={IconType.APP_PROPOSALS} + icon={IconType.BLOCKCHAIN_BLOCKCHAIN} size="lg" /> @@ -257,7 +257,7 @@ export default function Create() { )} {actionType === ActionType.Custom && ( - setActions(actions.concat([action]))} /> )} diff --git a/utils/input-values.ts b/utils/input-values.ts new file mode 100644 index 00000000..0960030c --- /dev/null +++ b/utils/input-values.ts @@ -0,0 +1,141 @@ +import { AbiParameter } from "viem"; + +export type InputValue = + | string + | boolean + | number + | bigint + | Array + | { [k: string]: InputValue }; + +export function isValidValue( + value: InputValue, + paramType: string, + components?: AbiParameter[] +): boolean { + if (!value || !paramType) return false; + + // Recursize cases + + if (paramType === "tuple[]") { + // struct array + if (!Array.isArray(value)) return false; + else if (!components) + throw new Error("The components parameter is required for tuples"); + + return !value.some((item, idx) => { + const abi = (paramAbi as any)["components"][idx]; + return !isValidValue(item, abi); + }); + } else if (paramType.endsWith("[]")) { + // plain array + if (!Array.isArray(value)) return false; + const baseType = paramType.replace(/\[\]$/, ""); + + return !value.some((item) => !isValidValue(item, baseType)); + } else if (paramType === "tuple[]") { + // struct + if (!Array.isArray(value)) return false; + else if (!components) + throw new Error("The components parameter is required for tuples"); + + return !value.some((item, idx) => { + const abi = (paramAbi as any)["components"][idx]; + return !isValidValue(item, abi); + }); + } + + // Simple cases + + switch (paramType) { + case "address": + if (typeof value !== "string") return false; + return /^0x[0-9a-fA-F]{40}$/.test(value); + case "bytes": + if (typeof value !== "string") return false; + return value.length % 2 === 0 && /^0x[0-9a-fA-F]*$/.test(value); + case "string": + return typeof value === "string"; + case "bool": + return typeof value === "boolean"; + } + + if (paramType.match(/^bytes[0-9]{1,2}$/)) { + if (typeof value !== "string") return false; + return value.length % 2 === 0 && /^0x[0-9a-fA-F]+$/.test(value); + } else if ( + paramType.match(/^uint[0-9]+$/) || + paramType.match(/^int[0-9]+$/) + ) { + return typeof value === "bigint"; + } + + throw new Error("Complex types should be checked directly"); +} + +export function isValidStringValue(value: string, paramType: string): boolean { + if (!value || !paramType) return false; + + switch (paramType) { + case "address": + return /^0x[0-9a-fA-F]{40}$/.test(value); + case "bytes": + return value.length % 2 === 0 && /^0x[0-9a-fA-F]*$/.test(value); + case "string": + return typeof value === "string"; + case "bool": + return [ + "True", + "true", + "Yes", + "yes", + "False", + "false", + "No", + "no", + ].includes(value); + } + + if (paramType.match(/^bytes[0-9]{1,2}$/)) { + return value.length % 2 === 0 && /^0x[0-9a-fA-F]+$/.test(value); + } else if (paramType.match(/^uint[0-9]+$/)) { + return value.length % 2 === 0 && /^[0-9]*$/.test(value); + } else if (paramType.match(/^int[0-9]+$/)) { + return value.length % 2 === 0 && /^-?[0-9]*$/.test(value); + } + throw new Error( + "Complex types need to be checked in a higher order function" + ); +} + +export function handleStringValue( + value: string, + paramType: string +): InputValue | null { + if (!isValidStringValue(value, paramType)) return null; + + switch (paramType) { + case "address": + case "bytes": + case "string": + return value; + case "bool": + return !["False", "false", "No", "no"].includes(value); + } + + if (paramType.match(/^bytes[0-9]{1,2}$/)) { + return value; + } else if ( + ["uint8", "uint16", "uint32", "int8", "int16", "int32"].includes(paramType) + ) { + return parseInt(value); + } else if ( + paramType.match(/^uint[0-9]+$/) || + paramType.match(/^int[0-9]+$/) + ) { + return BigInt(value); + } + throw new Error( + "Complex types need to be checked in a higher order function" + ); +} From aaa00b8f2bb1baaa259735a90d95fb49a41190aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= Date: Tue, 5 Mar 2024 18:14:36 +0100 Subject: [PATCH 2/9] WIP --- components/input/function-selector.tsx | 28 ++++++++------ components/input/input-parameter-array.tsx | 37 +++++++++---------- components/input/input-parameter-text.tsx | 26 +++++++------ .../input/input-parameter-tuple-array.tsx | 4 +- components/input/input-parameter-tuple.tsx | 2 +- 5 files changed, 51 insertions(+), 46 deletions(-) diff --git a/components/input/function-selector.tsx b/components/input/function-selector.tsx index 7cde5680..5302548d 100644 --- a/components/input/function-selector.tsx +++ b/components/input/function-selector.tsx @@ -94,22 +94,26 @@ export const FunctionSelector = ({ return (
{/* Side bar */} -
+
    - {abi?.map((targetFunc, index) => ( + {abi?.map((fn, index) => (
  • setSelectedAbiItem(targetFunc)} - className={`w-full text-left font-sm hover:bg-neutral-100 py-2 px-3 rounded-xl hover:cursor-pointer ${targetFunc.name === selectedAbiItem?.name && "bg-neutral-100 font-semibold"}`} + onClick={() => + !["pure", "view"].includes(fn.stateMutability) && + setSelectedAbiItem(fn) + } + className={`w-full text-left font-sm hover:bg-neutral-100 py-2 px-3 rounded-xl hover:cursor-pointer ${fn.name === selectedAbiItem?.name && "bg-neutral-100 font-semibold"}`} > - {decodeCamelCase(targetFunc.name)} - -
    - (read only) + + {decodeCamelCase(fn.name)} + + + {decodeCamelCase(fn.name)} + +
    + (read only) +
  • ))} diff --git a/components/input/input-parameter-array.tsx b/components/input/input-parameter-array.tsx index 2f424358..568108df 100644 --- a/components/input/input-parameter-array.tsx +++ b/components/input/input-parameter-array.tsx @@ -40,26 +40,25 @@ export const InputParameterArray = ({ }; return ( -
    +
    +

    + {abi.name ? decodeCamelCase(abi.name) : "Parameter " + (idx + 1)} +

    {value.map((item, i) => ( - 0 ? "mt-3" : ""} - label={ - i === 0 - ? (abi.name - ? decodeCamelCase(abi.name) - : "Parameter " + (i + 1)) + " (list)" - : undefined - } - placeholder={ - abi.type?.replace(/\[\]$/, "") || decodeCamelCase(abi.name) || "" - } - variant={ - isValidStringValue(item[i], abi.type) ? "default" : "critical" - } - value={item[i] || ""} - onChange={(e) => onItemChange(i, e.target.value)} - /> +
    + 0 ? "mt-3" : ""} + addon={(i + 1).toString()} + placeholder={ + abi.type?.replace(/\[\]$/, "") || decodeCamelCase(abi.name) || "" + } + variant={ + isValidStringValue(item[i], abi.type) ? "default" : "critical" + } + value={item[i] || ""} + onChange={(e) => onItemChange(i, e.target.value)} + /> +
    ))}
    diff --git a/components/input/input-parameter-text.tsx b/components/input/input-parameter-text.tsx index d75c8c78..b941103d 100644 --- a/components/input/input-parameter-text.tsx +++ b/components/input/input-parameter-text.tsx @@ -5,8 +5,9 @@ import { InputValue, isValidStringValue, handleStringValue, + readableTypeName, } from "@/utils/input-values"; -import { useState } from "react"; +import { useEffect, useState } from "react"; interface IInputParameterTextProps { abi: AbiParameter; @@ -19,10 +20,14 @@ export const InputParameterText = ({ idx, onChange, }: IInputParameterTextProps) => { - const [value, setvalue] = useState(""); + const [value, setValue] = useState(null); + + useEffect(() => { + setValue(null); + }, [abi]); const handleValue = (val: string) => { - setvalue(val); + setValue(val); const parsedValue = handleStringValue(val, abi.type); if (parsedValue === null) return; @@ -37,11 +42,17 @@ export const InputParameterText = ({ "abi-input-" + idx + "-" + (abi.name || abi.internalType || abi.type) } addon={abi.name ? decodeCamelCase(abi.name) : "Parameter " + (idx + 1)} - placeholder={abi.type || decodeCamelCase(abi.name) || ""} + placeholder={ + abi.type + ? readableTypeName(abi.type) + : decodeCamelCase(abi.name) || "" + } variant={ - !value || isValidStringValue(value, abi.type) ? "default" : "critical" + value === null || isValidStringValue(value, abi.type) + ? "default" + : "critical" } - value={value} + value={value || ""} onChange={(e) => handleValue(e.target.value)} />
    diff --git a/components/input/input-parameter-tuple-array.tsx b/components/input/input-parameter-tuple-array.tsx index 3407b79b..a9fb6ecd 100644 --- a/components/input/input-parameter-tuple-array.tsx +++ b/components/input/input-parameter-tuple-array.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Button, Tag } from "@aragon/ods"; import { AbiParameter } from "viem"; import { @@ -22,6 +22,10 @@ export const InputParameterTupleArray = ({ }: IInputParameterTupleArrayProps) => { const [value, setValue] = useState([[]]); + useEffect(() => { + setValue([[]]); + }, [abi]); + const onItemChange = (i: number, newVal: InputValue) => { console.log(i, newVal); @@ -61,7 +65,7 @@ export const InputParameterTupleArray = ({ ))}
diff --git a/components/input/input-parameter-tuple.tsx b/components/input/input-parameter-tuple.tsx index f51dda93..de16adb4 100644 --- a/components/input/input-parameter-tuple.tsx +++ b/components/input/input-parameter-tuple.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { AbiParameter } from "viem"; import { decodeCamelCase } from "@/utils/case"; import { InputParameter } from "./input-parameter"; @@ -19,6 +19,11 @@ export const InputParameterTuple = ({ hideTitle, }: IInputParameterTupleProps) => { const [value, setValue] = useState>({}); + + useEffect(() => { + setValue({}); + }, [abi]); + // const ohChange = (idx: number, value: string) => { // const newInputValues = [...abiInputValues]; // newInputValues[idx] = value; diff --git a/utils/input-values.ts b/utils/input-values.ts index 0960030c..1ddef3ac 100644 --- a/utils/input-values.ts +++ b/utils/input-values.ts @@ -1,4 +1,5 @@ import { AbiParameter } from "viem"; +import { decodeCamelCase } from "./case"; export type InputValue = | string @@ -70,7 +71,10 @@ export function isValidValue( return typeof value === "bigint"; } - throw new Error("Complex types should be checked directly"); + throw new Error( + "Complex types need to be checked in a higher order function. Got: " + + paramType + ); } export function isValidStringValue(value: string, paramType: string): boolean { @@ -97,14 +101,17 @@ export function isValidStringValue(value: string, paramType: string): boolean { } if (paramType.match(/^bytes[0-9]{1,2}$/)) { - return value.length % 2 === 0 && /^0x[0-9a-fA-F]+$/.test(value); + const len = parseInt(paramType.replace(/^bytes/, "")); + if (value.length !== len * 2) return false; + return /^0x[0-9a-fA-F]+$/.test(value); } else if (paramType.match(/^uint[0-9]+$/)) { - return value.length % 2 === 0 && /^[0-9]*$/.test(value); + return /^[0-9]*$/.test(value); } else if (paramType.match(/^int[0-9]+$/)) { - return value.length % 2 === 0 && /^-?[0-9]*$/.test(value); + return /^-?[0-9]*$/.test(value); } throw new Error( - "Complex types need to be checked in a higher order function" + "Complex types need to be checked in a higher order function. Got: " + + paramType ); } @@ -136,6 +143,30 @@ export function handleStringValue( return BigInt(value); } throw new Error( - "Complex types need to be checked in a higher order function" + "Complex types need to be checked in a higher order function. Got: " + + paramType ); } + +export function readableTypeName(paramType: string): string { + switch (paramType) { + case "address": + return "Address"; + case "bytes": + return "Hexadecimal value"; + case "string": + return "Text"; + case "bool": + return "Yes or no"; + } + + if (paramType.match(/^bytes[0-9]{1,2}$/)) { + return "Hexadecimal value"; + } else if (paramType.match(/^uint[0-9]+$/)) { + return "Positive number (in wei)"; + } else if (paramType.match(/^int[0-9]+$/)) { + return "Number (in wei)"; + } + + return decodeCamelCase(paramType.replace(/\[\]/, "")); +} From 395dec3e8ee0755f14aa42b11be4ab65d2a90e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= Date: Wed, 6 Mar 2024 13:42:28 +0100 Subject: [PATCH 4/9] Form receiving values --- components/input/function-call-form.tsx | 6 +- components/input/function-selector.tsx | 2 +- components/input/input-parameter-array.tsx | 3 +- components/input/input-parameter-text.tsx | 3 +- .../input/input-parameter-tuple-array.tsx | 106 +++++++++++------- components/input/input-parameter-tuple.tsx | 65 ++++++++--- components/input/withdrawal.tsx | 4 +- .../components/UserDelegateCard.tsx | 2 +- plugins/dualGovernance/pages/new.tsx | 2 +- plugins/tokenVoting/pages/new.tsx | 2 +- utils/input-values.ts | 5 +- 11 files changed, 128 insertions(+), 72 deletions(-) diff --git a/components/input/function-call-form.tsx b/components/input/function-call-form.tsx index 5a7d3d05..90f1cd4e 100644 --- a/components/input/function-call-form.tsx +++ b/components/input/function-call-form.tsx @@ -47,13 +47,13 @@ export const FunctionCallForm: FC = ({
- +

Enter the address of the contract to interact with

- +

The address of the contract is not valid

- +

The ABI of the contract is not publicly available

diff --git a/components/input/function-selector.tsx b/components/input/function-selector.tsx index 5302548d..65f5a951 100644 --- a/components/input/function-selector.tsx +++ b/components/input/function-selector.tsx @@ -105,7 +105,7 @@ export const FunctionSelector = ({ } className={`w-full text-left font-sm hover:bg-neutral-100 py-2 px-3 rounded-xl hover:cursor-pointer ${fn.name === selectedAbiItem?.name && "bg-neutral-100 font-semibold"}`} > - + {decodeCamelCase(fn.name)} diff --git a/components/input/input-parameter-array.tsx b/components/input/input-parameter-array.tsx index 246901ef..91549ea6 100644 --- a/components/input/input-parameter-array.tsx +++ b/components/input/input-parameter-array.tsx @@ -24,8 +24,9 @@ export const InputParameterArray = ({ const baseType = abi.type.replace(/\[\]$/, ""); useEffect(() => { + // Clean up if another function is selected setValue([null]); - }, [abi]); + }, [abi, idx]); const onItemChange = (i: number, newVal: string) => { const newArray = ([] as Array).concat(value); diff --git a/components/input/input-parameter-text.tsx b/components/input/input-parameter-text.tsx index b941103d..a666e381 100644 --- a/components/input/input-parameter-text.tsx +++ b/components/input/input-parameter-text.tsx @@ -23,8 +23,9 @@ export const InputParameterText = ({ const [value, setValue] = useState(null); useEffect(() => { + // Clean up if another function is selected setValue(null); - }, [abi]); + }, [abi, idx]); const handleValue = (val: string) => { setValue(val); diff --git a/components/input/input-parameter-tuple-array.tsx b/components/input/input-parameter-tuple-array.tsx index a9fb6ecd..84fd6e19 100644 --- a/components/input/input-parameter-tuple-array.tsx +++ b/components/input/input-parameter-tuple-array.tsx @@ -1,18 +1,15 @@ import { useEffect, useState } from "react"; -import { Button, Tag } from "@aragon/ods"; +import { AlertInline, Button, Tag } from "@aragon/ods"; import { AbiParameter } from "viem"; -import { - InputValue, - handleStringValue, - isValidStringValue, -} from "@/utils/input-values"; +import { InputValue } from "@/utils/input-values"; import { InputParameterTuple } from "./input-parameter-tuple"; import { decodeCamelCase } from "@/utils/case"; +import { Else, If, Then } from "../if"; interface IInputParameterTupleArrayProps { abi: AbiParameter; idx: number; - onChange: (paramIdx: number, value: InputValue) => any; + onChange: (paramIdx: number, value: Array>) => any; } export const InputParameterTupleArray = ({ @@ -20,54 +17,79 @@ export const InputParameterTupleArray = ({ idx, onChange, }: IInputParameterTupleArrayProps) => { - const [value, setValue] = useState([[]]); + const [values, setValues] = useState< + Array | undefined> + >([undefined]); useEffect(() => { - setValue([[]]); - }, [abi]); + // Clean up if another function is selected + setValues([undefined]); + }, [abi, idx]); - const onItemChange = (i: number, newVal: InputValue) => { - console.log(i, newVal); + const onItemChange = (i: number, newVal: Record) => { + const newValues = [...values]; + newValues[i] = newVal; + setValues(newValues); - // const newArray = ([] as string[][]).concat(value); - // newArray[i] = newVal; - // setValue(newArray); - // const transformedItems = newArray.map((item) => - // handleStringValue(item, abi.type) - // ); - // if (transformedItems.some((item) => item === null)) return; - // onChange(idx, transformedItems as InputValue[]); + if (newValues.some((item) => !item)) return; + + onChange(idx, newValues as Array>); }; const addMore = () => { - const newValue = [...value, []]; - setValue(newValue); + const newValues = [...values, undefined]; + setValues(newValues); }; + const components: AbiParameter[] = (abi as any).components || []; + const someMissingName = true || components.some((c) => !c.name); + return (
- {value.map((_, i) => ( -
-
-

- {abi.name ? decodeCamelCase(abi.name) : "Parameter " + (idx + 1)} -

- -
+ + + {values.map((_, i) => ( +
+
+

+ {abi.name + ? decodeCamelCase(abi.name) + : "Parameter " + (idx + 1)} +

+ +
- +
+ ))} +
+ +
+
+ +

+ {abi.name ? decodeCamelCase(abi.name) : "Parameter " + (idx + 1)} +

+ -
- ))} -
- -
+ +
); }; diff --git a/components/input/input-parameter-tuple.tsx b/components/input/input-parameter-tuple.tsx index de16adb4..b0a212d0 100644 --- a/components/input/input-parameter-tuple.tsx +++ b/components/input/input-parameter-tuple.tsx @@ -3,12 +3,13 @@ import { AbiParameter } from "viem"; import { decodeCamelCase } from "@/utils/case"; import { InputParameter } from "./input-parameter"; import { InputValue } from "@/utils/input-values"; -import { If } from "../if"; +import { Else, If, Then } from "../if"; +import { AlertInline } from "@aragon/ods"; interface IInputParameterTupleProps { abi: AbiParameter; idx: number; - onChange: (paramIdx: number, value: InputValue) => any; + onChange: (paramIdx: number, value: Record) => any; hideTitle?: boolean; } @@ -18,35 +19,65 @@ export const InputParameterTuple = ({ onChange, hideTitle, }: IInputParameterTupleProps) => { - const [value, setValue] = useState>({}); + const [values, setValues] = useState>([]); useEffect(() => { - setValue({}); - }, [abi]); + // Clean up if another function is selected + setValues([]); + }, [abi, idx]); - // const ohChange = (idx: number, value: string) => { - // const newInputValues = [...abiInputValues]; - // newInputValues[idx] = value; - // setAbiInputValues(newInputValues); - // }; + const onItemChange = (i: number, newVal: InputValue) => { + const newValues = [...values]; + newValues[i] = newVal; + setValues(newValues); + + // Report up + const result: Record = {}; + + for (let i = 0; i < components.length; i++) { + // Skip if incomplete + if (newValues[i] === undefined || newValues[i] === null) return; + // Arrange as an object + result[components[i].name!] = newValues[i]!; + } + + onChange(idx, result); + }; const components: AbiParameter[] = (abi as any).components || []; + const someMissingName = true || components.some((c) => !c.name); return (
- +

{abi.name ? decodeCamelCase(abi.name) : "Parameter " + (idx + 1)}

-
- {components.map((item, i) => ( -
- + + +
+ {components.map((item, i) => ( +
+ +
+ ))}
- ))} -
+ + + + +
); }; diff --git a/components/input/withdrawal.tsx b/components/input/withdrawal.tsx index 53a230a0..02ad24d3 100644 --- a/components/input/withdrawal.tsx +++ b/components/input/withdrawal.tsx @@ -34,11 +34,11 @@ const WithdrawalInput: FC = ({ setActions }) => { value={to} onChange={handleTo} /> - +

Enter the address to transfer to

- +

The address you entered is not valid

diff --git a/plugins/delegateAnnouncer/components/UserDelegateCard.tsx b/plugins/delegateAnnouncer/components/UserDelegateCard.tsx index 7f7c3946..d1e5b639 100644 --- a/plugins/delegateAnnouncer/components/UserDelegateCard.tsx +++ b/plugins/delegateAnnouncer/components/UserDelegateCard.tsx @@ -96,7 +96,7 @@ export const SelfDelegationProfileCard = ({ address, tokenAddress, message, load
- +