diff --git a/apps/hub/src/app/governance/[genre]/create/components/UpdateVaultWhitelistStatus.tsx b/apps/hub/src/app/governance/[genre]/create/components/UpdateVaultWhitelistStatus.tsx index d236d0c5e..51e609b04 100644 --- a/apps/hub/src/app/governance/[genre]/create/components/UpdateVaultWhitelistStatus.tsx +++ b/apps/hub/src/app/governance/[genre]/create/components/UpdateVaultWhitelistStatus.tsx @@ -52,11 +52,11 @@ export const UpdateVaultWhitelistStatus = ({ label="Reward Vault Address" value={gauge?.vault} error={ - errors.vault === ProposalErrorCodes.REQUIRED + errors?.vault === ProposalErrorCodes.REQUIRED ? "A Vault Must Be Chosen" - : errors.vault === ProposalErrorCodes.INVALID_ADDRESS + : errors?.vault === ProposalErrorCodes.INVALID_ADDRESS ? "Invalid Vault address." - : errors.vault + : errors?.vault } onChange={async (e) => { setAction({ diff --git a/apps/hub/src/app/governance/[genre]/create/components/abi-input.tsx b/apps/hub/src/app/governance/[genre]/create/components/abi-input.tsx index 09c8dfa0d..9c59443ca 100644 --- a/apps/hub/src/app/governance/[genre]/create/components/abi-input.tsx +++ b/apps/hub/src/app/governance/[genre]/create/components/abi-input.tsx @@ -2,6 +2,10 @@ import { AbiParameter } from "viem"; import { InputWithLabel } from "@bera/ui/input"; import { useEffect, useState } from "react"; import { Button } from "@bera/ui/button"; +import { Label } from "@bera/ui/label"; +import { Switch } from "@bera/ui/switch"; +import { FormError } from "@bera/ui/form-error"; +import { ProposalErrorCodes } from "~/app/governance/types"; export function AbiInput({ input, @@ -22,6 +26,9 @@ export function AbiInput({ if ("components" in input && typeof value !== "object") { onChange({ [input.name!]: {} }); } + if (input.type === "string" && typeof value !== "string") { + onChange(""); + } }, [value, input]); if ("components" in input) { @@ -46,6 +53,7 @@ export function AbiInput({ } /> ))} + {errors} ); } @@ -54,18 +62,57 @@ export function AbiInput({ ); } + + const error = + errors && typeof errors === "object" && input.name + ? errors[input.name] + : errors; + if (input.type === "bool") { + return ( +
+ +
+ { + onChange(!!e); + }} + /> + {value ? ( + + ) : ( + + )} +
+ + {error === ProposalErrorCodes.INVALID_AMOUNT + ? "Please enter a valid boolean" + : error} + +
+ ); + } return ( onChange(e.target.value)} diff --git a/apps/hub/src/app/governance/[genre]/create/components/custom-action.tsx b/apps/hub/src/app/governance/[genre]/create/components/custom-action.tsx index 0778663c1..0038e75c9 100644 --- a/apps/hub/src/app/governance/[genre]/create/components/custom-action.tsx +++ b/apps/hub/src/app/governance/[genre]/create/components/custom-action.tsx @@ -12,7 +12,7 @@ import { useEffect, useState, } from "react"; -import { Abi, AbiFunction, isAddress, parseAbiItem } from "viem"; +import { Abi, AbiFunction, parseAbiItem } from "viem"; import { CustomProposalActionErrors, ProposalAction, @@ -144,11 +144,11 @@ export function CustomAction({ variant="black" label="Enter Target" error={ - errors.target === ProposalErrorCodes.REQUIRED + errors?.target === ProposalErrorCodes.REQUIRED ? "Target is required." - : errors.target === ProposalErrorCodes.INVALID_ADDRESS + : errors?.target === ProposalErrorCodes.INVALID_ADDRESS ? "Invalid target address." - : errors.target + : errors?.target } id={`proposal-target--${idx}`} placeholder={truncateHash("0x00000000000000")} @@ -161,15 +161,15 @@ export function CustomAction({ } /> { setValue(e.target.value); try { - setAction((prev) => ({ - ...prev, - value: BigInt(e.target.value), - })); + const valueBN = BigInt(e.target.value); + if (valueBN < 0n) { + setErrors((e) => ({ + ...e, + value: ProposalErrorCodes.NEGATIVE_AMOUNT, + })); + } else { + setAction((prev) => ({ + ...prev, + value: valueBN, + })); + setErrors((e) => ({ + ...e, + value: null, + })); + } } catch (error) { setErrors((e) => ({ ...e, @@ -194,11 +206,11 @@ export function CustomAction({ label="Enter ABI" variant="black" error={ - errors.ABI === ProposalErrorCodes.REQUIRED + errors?.ABI === ProposalErrorCodes.REQUIRED ? "ABI is required." - : errors.ABI === ProposalErrorCodes.INVALID_ABI + : errors?.ABI === ProposalErrorCodes.INVALID_ABI ? "Invalid ABI" - : errors.ABI + : errors?.ABI } id={`proposal-abi--${idx}`} placeholder={JSON.stringify( @@ -258,16 +270,16 @@ export function CustomAction({ } /> - {errors.functionSignature === ProposalErrorCodes.REQUIRED + {errors?.functionSignature === ProposalErrorCodes.REQUIRED ? "A Method Must Be chosen" - : errors.functionSignature} + : errors?.functionSignature} {abiItems?.inputs?.map((input, i) => { return ( - {errors.target === ProposalErrorCodes.REQUIRED + {errors?.target === ProposalErrorCodes.REQUIRED ? "A Token Must Be Chosen" - : errors.amount === ProposalErrorCodes.REQUIRED + : errors?.amount === ProposalErrorCodes.REQUIRED ? "Amount must be filled" - : errors.amount === ProposalErrorCodes.INVALID_AMOUNT + : errors?.amount === ProposalErrorCodes.INVALID_AMOUNT ? "Invalid amount." - : errors.amount} + : errors?.amount} {warning && (
diff --git a/apps/hub/src/app/governance/types.ts b/apps/hub/src/app/governance/types.ts index 20e0a44df..d469a94ef 100755 --- a/apps/hub/src/app/governance/types.ts +++ b/apps/hub/src/app/governance/types.ts @@ -41,7 +41,7 @@ export type CustomProposalActionErrors = { isFriend?: null | ProposalErrorCodes; to?: null | ProposalErrorCodes; amount?: null | ProposalErrorCodes; -}; +} | null; export type CustomProposalErrors = { title?: null | ProposalErrorCodes; diff --git a/apps/hub/src/hooks/useCreateProposal.ts b/apps/hub/src/hooks/useCreateProposal.ts index 90d96f901..d3c073379 100755 --- a/apps/hub/src/hooks/useCreateProposal.ts +++ b/apps/hub/src/hooks/useCreateProposal.ts @@ -54,6 +54,7 @@ interface CheckProposalField { fieldOrType: | "address" | "abi" + | "string" | "bool" | `uint${string}` | `int${string}` @@ -83,7 +84,13 @@ export const checkProposalField: CheckProposalField = ({ baseUrl, components, }) => { - if (required && !value) { + const notRequiredAbiTypes = ["bool", "string"]; + + if ( + !notRequiredAbiTypes.includes(fieldOrType) && + required && + (value === undefined || value === null || value === "") + ) { return ProposalErrorCodes.REQUIRED; } @@ -106,10 +113,16 @@ export const checkProposalField: CheckProposalField = ({ } switch (fieldOrType) { + case "string": + if (value !== undefined && typeof value !== "string") { + return ProposalErrorCodes.INVALID_AMOUNT; + } + return null; + case "bool": - // if (value !== "true" && value !== "false") { - // return ProposalErrorCodes.INVALID_AMOUNT; - // } + if (typeof value !== "boolean") { + return ProposalErrorCodes.INVALID_AMOUNT; + } return null; case "title": @@ -291,114 +304,114 @@ export const useCreateProposal = ({ const actions: Address[] = []; - e.actions = proposal.actions - .map((action, idx): CustomProposalActionErrors => { - const errors: CustomProposalActionErrors = {}; - errors.target = checkProposalField({ - fieldOrType: "address", - value: action.target, + e.actions = proposal.actions.map((action, idx) => { + const errors: CustomProposalActionErrors = {}; + errors.target = checkProposalField({ + fieldOrType: "address", + value: action.target, + }); + + if (action.type === ProposalTypeEnum.CUSTOM_PROPOSAL) { + errors.ABI = checkProposalField({ + fieldOrType: "abi", + value: action.ABI, }); - - if (action.type === ProposalTypeEnum.CUSTOM_PROPOSAL) { - errors.ABI = checkProposalField({ - fieldOrType: "abi", - value: action.ABI, - }); - if (!action.functionSignature) { - errors.functionSignature = ProposalErrorCodes.REQUIRED; - } else { - try { - const parsedSignatureAbi = parseAbiItem( - action.functionSignature, + if (!action.functionSignature) { + errors.functionSignature = ProposalErrorCodes.REQUIRED; + } else { + try { + const parsedSignatureAbi = parseAbiItem(action.functionSignature); + if (parsedSignatureAbi.type !== "function") { + console.error( + "parsedSignatureAbi is not a function", + parsedSignatureAbi, ); - if (parsedSignatureAbi.type !== "function") { - console.error( - "parsedSignatureAbi is not a function", - parsedSignatureAbi, - ); - - errors.functionSignature = ProposalErrorCodes.INVALID_ABI; - } else { - errors.calldata = parsedSignatureAbi.inputs.map( - (input, index) => { - try { - if ("components" in input) { - return checkProposalField({ - // @ts-expect-error this is not typed, will throw if not valid - fieldOrType: input.type, - value: action.calldata?.[index], - components: input.components, - }); - } + errors.functionSignature = ProposalErrorCodes.INVALID_ABI; + } else { + errors.calldata = parsedSignatureAbi.inputs.map( + (input, index) => { + try { + if ("components" in input) { return checkProposalField({ // @ts-expect-error this is not typed, will throw if not valid fieldOrType: input.type, value: action.calldata?.[index], + components: input.components, }); - } catch (error) { - return null; } - }, - ); - - actions[idx] = encodeFunctionData({ - abi: [parsedSignatureAbi], - args: action.calldata, - }); - } - } catch (error) { - errors.functionSignature = ProposalErrorCodes.INVALID_ABI; + + return checkProposalField({ + // @ts-expect-error this is not typed, will throw if not valid + fieldOrType: input.type, + value: action.calldata?.[index], + }); + } catch (error) { + return null; + } + }, + ); + + actions[idx] = encodeFunctionData({ + abi: [parsedSignatureAbi], + args: action.calldata, + }); } + } catch (error) { + errors.functionSignature = ProposalErrorCodes.INVALID_ABI; } - } else if ( - action.type === ProposalTypeEnum.WHITELIST_REWARD_VAULT || - action.type === ProposalTypeEnum.BLACKLIST_REWARD_VAULT - ) { - errors.vault = checkProposalField({ - fieldOrType: "address", - value: action.vault, - }); - errors.isFriend = null; //checkProposalField("bool", action.isFriend); - const whiteList = - action.type === ProposalTypeEnum.WHITELIST_REWARD_VAULT - ? true - : false; - if (!errors.vault) { - actions[idx] = encodeFunctionData({ - abi: BERA_CHEF_ABI, - functionName: "setVaultWhitelistedStatus", - args: [action.vault!, whiteList, action.metadata ?? ""], // TODO: A third param was added for metadata. It is optional but we should include it in our action - }); - } - } else if (action.type === ProposalTypeEnum.ERC20_TRANSFER) { - errors.amount = checkProposalField({ - fieldOrType: "uint256", - value: action.amount, + } + } else if ( + action.type === ProposalTypeEnum.WHITELIST_REWARD_VAULT || + action.type === ProposalTypeEnum.BLACKLIST_REWARD_VAULT + ) { + errors.vault = checkProposalField({ + fieldOrType: "address", + value: action.vault, + }); + errors.isFriend = null; //checkProposalField("bool", action.isFriend); + const whiteList = + action.type === ProposalTypeEnum.WHITELIST_REWARD_VAULT + ? true + : false; + if (!errors.vault) { + actions[idx] = encodeFunctionData({ + abi: BERA_CHEF_ABI, + functionName: "setVaultWhitelistedStatus", + args: [action.vault!, whiteList, action.metadata ?? ""], // TODO: A third param was added for metadata. It is optional but we should include it in our action }); - errors.to = checkProposalField({ - fieldOrType: "address", - value: action.to, + } + } else if (action.type === ProposalTypeEnum.ERC20_TRANSFER) { + errors.amount = checkProposalField({ + fieldOrType: "uint256", + value: action.amount, + }); + errors.to = checkProposalField({ + fieldOrType: "address", + value: action.to, + }); + if (!errors.amount && !errors.to) { + actions[idx] = encodeFunctionData({ + abi: erc20Abi, + functionName: "transfer", + args: [action.to!, BigInt(action.amount!)], }); - if (!errors.amount && !errors.to) { - actions[idx] = encodeFunctionData({ - abi: erc20Abi, - functionName: "transfer", - args: [action.to!, BigInt(action.amount!)], - }); - } } - return errors; - }) - .filter((e) => - Object.values(e).some((v) => { - if (Array.isArray(v)) { - return v.filter((v) => v).length > 0; - } + } - return !!v; - }), - ); + const hasErrors = Object.values(e).some((v) => { + if (Array.isArray(v)) { + return v.filter((v) => v).length > 0; + } + + return !!v; + }); + + if (!hasErrors) { + return null; + } + return errors; + }); onError?.(e); @@ -407,7 +420,11 @@ export const useCreateProposal = ({ .map((name) => e[name as keyof typeof e]) .some((v) => { if (Array.isArray(v)) { - return v.length > 0; + return v.filter((v) => v).length > 0; + } + + if (v === null) { + return false; } return !!v;