From 6634890bd60c829766e1144f67efa4a3640c6338 Mon Sep 17 00:00:00 2001 From: iain nash Date: Thu, 21 Dec 2023 14:47:40 -0500 Subject: [PATCH] update ether formatting and url sharing --- src/app/NewSafeProposal.tsx | 99 ++++++--------------------- src/components/GenericField.tsx | 1 - src/hooks/useLoadProposalFromQuery.ts | 34 +++++++++ src/hooks/useSetParamsFromQuery.ts | 19 +++++ src/schemas/proposal.ts | 40 +++++++++++ src/utils/etherFormatting.ts | 22 ++++++ 6 files changed, 136 insertions(+), 79 deletions(-) create mode 100644 src/hooks/useLoadProposalFromQuery.ts create mode 100644 src/hooks/useSetParamsFromQuery.ts create mode 100644 src/schemas/proposal.ts create mode 100644 src/utils/etherFormatting.ts diff --git a/src/app/NewSafeProposal.tsx b/src/app/NewSafeProposal.tsx index 24abca8..fc927ad 100644 --- a/src/app/NewSafeProposal.tsx +++ b/src/app/NewSafeProposal.tsx @@ -8,17 +8,27 @@ import { useEffect, useState, } from "react"; -import { Address, Hex } from "viem"; -import { validateAddress, validateETH, yupAddress } from "../utils/validators"; +import { Address, Hex, formatEther } from "viem"; +import { validateAddress, validateETH } from "../utils/validators"; import { GenericField } from "../components/GenericField"; -import { useSearchParams } from "react-router-dom"; -import { InferType, array, number, object, string } from "yup"; import { DataActionPreview } from "../components/DataActionPreview"; import Safe, { EthersAdapter } from "@safe-global/protocol-kit"; import { WalletProviderContext } from "./Root"; import { ethers } from "ethers"; import { contractNetworks } from "../chains"; import { SafeInformationContext } from "./ViewSafe"; +import { + DEFAULT_ACTION_ITEM, + DEFAULT_PROPOSAL, + Proposal, + proposalSchema, +} from "../schemas/proposal"; +import { useSetParamsFromQuery } from "../hooks/useSetParamsFromQuery"; +import { useLoadProposalFromQuery } from "../hooks/useLoadProposalFromQuery"; +import { + transformValuesFromWei, + transformValuesToWei, +} from "../utils/etherFormatting"; const FormActionItem = ({ name, @@ -51,44 +61,6 @@ const FormActionItem = ({ ); }; -const proposalSchema = object({ - nonce: number().nullable(), - actions: array( - object({ - to: yupAddress, - value: string() - .default("0") - .matches( - /^[0-9]+(\.[0-9]+)?$/, - "Needs to be a ETH price (0, 1, or 0.23)" - ) - .required(), - data: string() - .default("0x") - .matches( - /^0x(?:[0-9A-Za-z][0-9A-Za-z])*$/, - "Data is required to match hex format" - ) - .required(), - }) - ), -}); - -interface Proposal extends InferType { - // using interface instead of type generally gives nicer editor feedback -} - -const DEFAULT_ACTION_ITEM = { - to: "0x", - value: "0", - data: "0x", -}; - -const DEFAULT_PROPOSAL = { - nonce: null, - actions: [DEFAULT_ACTION_ITEM], -}; - const createSafeAdapter = async ({ provider, safeAddress, @@ -194,37 +166,6 @@ const useSafe = ({ return safe; }; -const useLoadProposalFromQuery = () => { - const [proposal, setProposal] = useState(); - const [params] = useSearchParams(); - - useEffect(() => { - const targets = params.get("targets")?.split("|"); - const calldatas = params.get("calldatas")?.split("|"); - const values = params.get("values")?.split("|"); - if (targets && calldatas) { - // ensure the 3 lengths are the same. check if values also has the same length if its not empty - // check the inverse of the above, if inverse is true, return: - if ( - targets.length !== calldatas.length || - (values?.length && values?.length !== targets.length) - ) { - console.log("invalid lengths"); - return; - } - - const actions = targets.map((target, index) => ({ - to: target, - data: calldatas[index]!, - value: (values && values[index]) || "0", - })); - setProposal({ actions }); - } - }, [params, setProposal]); - - return proposal; -}; - const useGetSafeTxApprovals = ({ proposal }: { proposal: Proposal }) => { const safeInformation = useContext(SafeInformationContext); @@ -235,7 +176,6 @@ const useGetSafeTxApprovals = ({ proposal }: { proposal: Proposal }) => { const loadApprovers = useCallback(async () => { if (!safeSdk || !safeSdk2) return; - // const { safeSdk, safeSdk2 } = await getSafeSDK(safeAddress); const txn = await createSafeTransaction({ proposal, safe: safeSdk, @@ -380,7 +320,7 @@ const ViewProposal = ({ <> Proposal #{indx} To: {action.to as Address} - Value: {action.value} + Value: {formatEther(BigInt(action.value))} {action.data ? ( <> Data: {action.data} @@ -433,10 +373,13 @@ const EditProposal = ({ setProposal: (result: Proposal) => void; setIsEditing: (editing: boolean) => void; }) => { + const setProposalParams = useSetParamsFromQuery(); const onSubmit = useCallback( (result: Proposal) => { - setProposal(result); - // setParams({ proposal: JSON.stringify(result) }); + setProposal(transformValuesToWei(result)); + if (proposal) { + setProposalParams(proposal); + } setIsEditing(false); }, [setIsEditing, setProposal] @@ -449,7 +392,7 @@ const EditProposal = ({ {({ handleSubmit, values, isValid }) => ( diff --git a/src/components/GenericField.tsx b/src/components/GenericField.tsx index 4ccc532..c3466de 100644 --- a/src/components/GenericField.tsx +++ b/src/components/GenericField.tsx @@ -10,7 +10,6 @@ export const GenericField = ({ }) => { return ({ field: { name, value, onChange }, form: { errors } }: any) => { const error = get(errors, name); - // console.log({ error, errors, value, name }); return ( diff --git a/src/hooks/useLoadProposalFromQuery.ts b/src/hooks/useLoadProposalFromQuery.ts new file mode 100644 index 0000000..a21696c --- /dev/null +++ b/src/hooks/useLoadProposalFromQuery.ts @@ -0,0 +1,34 @@ +import { useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; +import { Proposal } from "../schemas/proposal"; + +export const useLoadProposalFromQuery = () => { + const [proposal, setProposal] = useState(); + const [params] = useSearchParams(); + + useEffect(() => { + const targets = params.get("targets")?.split("|"); + const calldatas = params.get("calldatas")?.split("|"); + const values = params.get("values")?.split("|"); + if (targets && calldatas) { + // ensure the 3 lengths are the same. check if values also has the same length if its not empty + // check the inverse of the above, if inverse is true, return: + if ( + targets.length !== calldatas.length || + (values?.length && values?.length !== targets.length) + ) { + console.log("invalid lengths"); + return; + } + + const actions = targets.map((target, index) => ({ + to: target, + data: calldatas[index]!, + value: (values && values[index]) || "0", + })); + setProposal({ actions }); + } + }, [params, setProposal]); + + return proposal; + }; \ No newline at end of file diff --git a/src/hooks/useSetParamsFromQuery.ts b/src/hooks/useSetParamsFromQuery.ts new file mode 100644 index 0000000..c1b293b --- /dev/null +++ b/src/hooks/useSetParamsFromQuery.ts @@ -0,0 +1,19 @@ +import { useCallback } from "react"; +import { useSearchParams } from "react-router-dom"; +import { Proposal } from "../schemas/proposal"; + +export const useSetParamsFromQuery = () => { + const [_, setParams] = useSearchParams(); + + return useCallback((proposal: Proposal) => { + if (!proposal.actions?.length) { + return; + } + console.log('setting params', proposal.actions); + setParams({ + targets: proposal.actions!.map((action) => action.to).join('|'), + data: proposal.actions!.map((action) => action.data).join('|'), + value: proposal.actions!.map((action) => action.value).join('|'), + }) + }, [setParams]); + } \ No newline at end of file diff --git a/src/schemas/proposal.ts b/src/schemas/proposal.ts new file mode 100644 index 0000000..639cf8c --- /dev/null +++ b/src/schemas/proposal.ts @@ -0,0 +1,40 @@ +import { InferType, array, number, object, string } from "yup"; +import { yupAddress } from "../utils/validators"; + +export const proposalSchema = object({ + nonce: number().nullable(), + actions: array( + object({ + to: yupAddress, + value: string() + .default("0") + .matches( + /^[0-9]+(\.[0-9]+)?$/, + "Needs to be a ETH price (0, 1, or 0.23)" + ) + .required(), + data: string() + .default("0x") + .matches( + /^0x(?:[0-9A-Za-z][0-9A-Za-z])*$/, + "Data is required to match hex format" + ) + .required(), + }) + ), +}); + +export interface Proposal extends InferType { + // using interface instead of type generally gives nicer editor feedback +} + +export const DEFAULT_ACTION_ITEM = { + to: "0x", + value: "0", + data: "0x", +}; + +export const DEFAULT_PROPOSAL = { + nonce: null, + actions: [DEFAULT_ACTION_ITEM], +}; diff --git a/src/utils/etherFormatting.ts b/src/utils/etherFormatting.ts new file mode 100644 index 0000000..f9bbdc3 --- /dev/null +++ b/src/utils/etherFormatting.ts @@ -0,0 +1,22 @@ +import { formatEther, parseEther } from "viem"; +import { Proposal } from "../schemas/proposal"; + +export function transformValuesToWei(proposal: Proposal): Proposal { + return { + ...proposal, + actions: proposal.actions?.map((action) => ({ + ...action, + value: parseEther(action.value).toString(), + })), + }; +} + +export function transformValuesFromWei(proposal: Proposal): Proposal { + return { + ...proposal, + actions: proposal.actions?.map((action) => ({ + ...action, + value: formatEther(BigInt(action.value)), + })), + }; +}