Skip to content

Commit

Permalink
Custom action form should work
Browse files Browse the repository at this point in the history
  • Loading branch information
brickpop committed Mar 6, 2024
1 parent aee7bcb commit a83bd0a
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 123 deletions.
54 changes: 44 additions & 10 deletions components/actions/action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { Action } from "@/utils/types";
import { useAction } from "@/hooks/useAction";
import {
AbiFunction,
AbiParameter,
Address,
Hex,
formatEther,
toFunctionSignature,
toHex,
} from "viem";
import { compactNumber } from "@/utils/numbers";
import { decodeCamelCase } from "@/utils/case";
import { InputValue } from "@/utils/input-values";

type ActionCardProps = {
action: Action;
Expand All @@ -26,7 +27,9 @@ type CallParameterFieldType =
| bigint
| Address
| Hex
| boolean;
| boolean
| CallParameterFieldType[]
| { [k: string]: CallParameterFieldType };

export const ActionCard = function ({ action, idx }: ActionCardProps) {
const { isLoading, args, functionName, functionAbi } = useAction(action);
Expand Down Expand Up @@ -158,22 +161,47 @@ const CallParameterField = ({
<InputText
className="w-full"
addon={decodeCamelCase(addon)}
value={resolveValue(value, functionAbi.inputs?.[idx].type)}
value={resolveValue(value, functionAbi.inputs?.[idx])}
readOnly
addonPosition="left"
/>
);
};

function resolveValue(value: CallParameterFieldType, abiType?: string): string {
if (!abiType) return value.toString();
else if (abiType === "address") {
function resolveValue(
value: CallParameterFieldType,
abi?: AbiParameter
): string {
if (!abi?.type) {
if (Array.isArray(value)) return value.join(", ");
return value.toString();
} else if (abiType === "bytes32") {
return toHex(value);
} else if (abiType.startsWith("uint") || abiType.startsWith("int")) {
} else if (abi.type === "tuple[]") {
const abiClone = Object.assign({}, { ...abi });
abiClone.type = abiClone.type.replace(/\[\]$/, "");

const items = (value as any as any[]).map((item) =>
resolveValue(item, abiClone)
);
return items.join(", ");
} else if (abi.type === "tuple") {
const result = {} as Record<string, string>;
const components: AbiParameter[] = (abi as any).components || [];

for (let i = 0; i < components.length; i++) {
const k = components[i].name!;
result[k] = resolveValue((value as any)[k], components[i]);
}

return getReadableJson(result);
} else if (abi.type.endsWith("[]")) {
return (value as any as any[]).join(", ");
} else if (abi.type === "address") {
return value as string;
} else if (abi.type === "bytes32") {
return value as string;
} else if (abi.type.startsWith("uint") || abi.type.startsWith("int")) {
return value.toString();
} else if (abiType.startsWith("bool")) {
} else if (abi.type.startsWith("bool")) {
return value ? "Yes" : "No";
}
return value.toString();
Expand Down Expand Up @@ -202,3 +230,9 @@ function resolveAddon(
}
return (idx + 1).toString();
}

function getReadableJson(value: Record<string, InputValue>): string {
const items = Object.keys(value).map((k) => k + ": " + value[k]);

return "{ " + items.join(", ") + " }";
}
59 changes: 20 additions & 39 deletions components/input/function-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { Hex, encodeFunctionData } from "viem";
import { Button, InputText } from "@aragon/ods";
import { AbiFunction } from "abitype";
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";
import { InputValue } from "@/utils/input-values";

interface IFunctionSelectorProps {
abi: AbiFunction[];
Expand All @@ -23,64 +23,45 @@ export const FunctionSelector = ({
const [inputValues, setInputValues] = useState<InputValue[]>([]);
const [value, setValue] = useState<string>("");

useEffect(() => {
// Clean up if another function is selected
setInputValues([]);
}, [abi]);

const onParameterChange = (paramIdx: number, value: InputValue) => {
const newInputValues = [...inputValues];
newInputValues[paramIdx] = value;
setInputValues(newInputValues);
};

const onAddAction = () => {
// Validate params
if (!abi || !selectedAbiItem) return;

let invalidParams = false;
if (!abi?.length) invalidParams = true;
else if (!selectedAbiItem?.name) invalidParams = true;
else if (selectedAbiItem.inputs.length !== inputValues.length)
invalidParams = true;
// The values we have now are the result of
// validation having happened at the specific components

for (const i in selectedAbiItem.inputs) {
const item = selectedAbiItem.inputs[i];
if (!isValidValue(inputValues[i], item)) {
invalidParams = true;
break;
for (let i = 0; i < selectedAbiItem.inputs.length; i++) {
if (inputValues[i] === null || inputValues[i] === undefined) {
return addAlert("Invalid parameters", {
description:
"Make sure that you have filled all the parameters and that they contain valid values",
type: "error",
});
}
}
invalidParams = invalidParams || !/^[0-9]*$/.test(value);

if (invalidParams) {
addAlert("Invalid parameters", {
description: "Check that the parameters you entered are correct",
type: "error",
});
return;
}

if (["pure", "view"].includes(selectedAbiItem.stateMutability)) {
addAlert("Read only function", {
description: "The action you have added will have no effect",
timeout: 11 * 1000,
});
}

try {
const booleanIdxs = selectedAbiItem.inputs
.map((inp, i) => (inp.type === "bool" ? i : -1))
.filter((v) => v >= 0);
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;
}

const data = encodeFunctionData({
abi,
functionName: selectedAbiItem.name,
args,
args: inputValues,
});
actionEntered(data, BigInt(value ?? "0"));

setInputValues([]);

// Clean up the form
setSelectedAbiItem(undefined);
} catch (err) {
console.error(err);
addAlert("Invalid parameters", {
Expand Down
2 changes: 1 addition & 1 deletion components/input/input-parameter-tuple-array.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const InputParameterTupleArray = ({
};

const components: AbiParameter[] = (abi as any).components || [];
const someMissingName = true || components.some((c) => !c.name);
const someMissingName = components.some((c) => !c.name);

return (
<div>
Expand Down
2 changes: 1 addition & 1 deletion components/input/input-parameter-tuple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const InputParameterTuple = ({
};

const components: AbiParameter[] = (abi as any).components || [];
const someMissingName = true || components.some((c) => !c.name);
const someMissingName = components.some((c) => !c.name);

return (
<div>
Expand Down
2 changes: 1 addition & 1 deletion plugins/dualGovernance/hooks/useProposal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function useProposal(
setMetadata(log.args.metadata);
})
.catch((err) => {
console.error("Could not fetch the proposal defailt", err);
console.error("Could not fetch the proposal details", err);
return null;
});
}, [proposalData?.vetoTally]);
Expand Down
2 changes: 1 addition & 1 deletion plugins/tokenVoting/hooks/useProposal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function useProposal(proposalId: string, autoRefresh = false) {
setMetadata(log.args.metadata);
})
.catch((err) => {
console.error("Could not fetch the proposal defailt", err);
console.error("Could not fetch the proposal details", err);
});
}, [proposalData?.tally]);

Expand Down
71 changes: 1 addition & 70 deletions utils/input-values.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { AbiParameter } from "viem";
import { decodeCamelCase } from "./case";

export type InputValue =
Expand All @@ -9,74 +8,6 @@ export type InputValue =
| Array<InputValue>
| { [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 need to be checked in a higher order function. Got: " +
paramType
);
}

export function isValidStringValue(value: string, paramType: string): boolean {
if (!value || !paramType) return false;

Expand All @@ -102,7 +33,7 @@ export function isValidStringValue(value: string, paramType: string): boolean {

if (paramType.match(/^bytes[0-9]{1,2}$/)) {
const len = parseInt(paramType.replace(/^bytes/, ""));
if (value.length !== len * 2) return false;
if (value.length !== 2 + len * 2) return false;
return /^0x[0-9a-fA-F]+$/.test(value);
} else if (paramType.match(/^uint[0-9]+$/)) {
return /^[0-9]*$/.test(value);
Expand Down

0 comments on commit a83bd0a

Please sign in to comment.