From 27abb6cdb8fe93e090957f9cf1577f9c1afdcf79 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 10 Jun 2024 12:31:10 +0200 Subject: [PATCH 01/10] Add missing type on Dialog --- components/ui/dialog.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 3fac3bf..4bccb3c 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -105,6 +105,9 @@ const DialogDescription = React.forwardRef< )) DialogDescription.displayName = DialogPrimitive.Description.displayName +const DialogClose = DialogPrimitive.DialogClose; +DialogClose.displayName = DialogPrimitive.DialogClose.displayName + export { Dialog, DialogTrigger, @@ -113,4 +116,5 @@ export { DialogFooter, DialogTitle, DialogDescription, + DialogClose } From f7c4e39e4d7485b54bebf17e50de924786b10ec7 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 10 Jun 2024 12:31:37 +0200 Subject: [PATCH 02/10] Improve checkAddress. Add checkAddressOrPubkey --- lib/displayHelpers.ts | 60 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/lib/displayHelpers.ts b/lib/displayHelpers.ts index 287b261..67c270d 100644 --- a/lib/displayHelpers.ts +++ b/lib/displayHelpers.ts @@ -113,6 +113,25 @@ function examplePubkey(index: number): string { return toBase64(data); } +function digestAddressError(error: unknown) { + if (!(error instanceof Error)) { + return "Expected a bech32 address"; + } + + const msg = error.message.toLowerCase(); + + switch (true) { + case msg.includes("too short"): + return "Too short"; + case msg.includes("no separator character"): + return "No separator character found"; + case msg.includes("invalid checksum"): + return "Invalid checksum"; + default: + return error.message; + } +} + /** * Returns an error message for invalid addresses. * Returns null of there is no error. @@ -126,9 +145,8 @@ const checkAddress = (input: string, chainAddressPrefix: string | null) => { let prefix; try { ({ data, prefix } = fromBech32(input)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - return error.toString(); + } catch (error: unknown) { + return digestAddressError(error); } if (chainAddressPrefix) { @@ -146,6 +164,27 @@ const checkAddress = (input: string, chainAddressPrefix: string | null) => { return null; }; +/** + * Returns an error message for invalid addresses or pubkeys. + * Returns null of there is no error. + */ +const checkAddressOrPubkey = (input: string, chainAddressPrefix: string) => { + if (!input) { + return "Empty"; + } + + if (!input.startsWith(chainAddressPrefix)) { + try { + fromBase64(input); + return null; + } catch { + return "Public key should be valid Base64"; + } + } + + return checkAddress(input, chainAddressPrefix); +}; + /** * Returns a link to a transaction in an explorer if an explorer is configured * for transactions. Returns null otherwise. @@ -183,16 +222,17 @@ const trimStringsObj = >(obj: StringsO }; export { - thinSpace, capitalizeFirstLetter, + checkAddress, + checkAddressOrPubkey, ellideMiddle, + exampleAddress, + examplePubkey, + exampleValidatorAddress, + explorerLinkAccount, + explorerLinkTx, printableCoin, printableCoins, - exampleAddress, - exampleValidatorAddress, - examplePubkey, - checkAddress, - explorerLinkTx, - explorerLinkAccount, + thinSpace, trimStringsObj, }; From f8e715ef817fd2be2fb79894597678b902c17b1c Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 10 Jun 2024 12:32:00 +0200 Subject: [PATCH 03/10] Add create multisig form schema --- .../forms/CreateMultisigForm/formSchema.ts | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 components/forms/CreateMultisigForm/formSchema.ts diff --git a/components/forms/CreateMultisigForm/formSchema.ts b/components/forms/CreateMultisigForm/formSchema.ts new file mode 100644 index 0000000..47e9ded --- /dev/null +++ b/components/forms/CreateMultisigForm/formSchema.ts @@ -0,0 +1,130 @@ +import { ChainInfo } from "@/context/ChainsContext/types"; +import { pubkeyToAddress } from "@cosmjs/amino"; +import { StargateClient } from "@cosmjs/stargate"; +import { z } from "zod"; +import { checkAddressOrPubkey } from "../../../lib/displayHelpers"; + +export const getCreateMultisigSchema = (chain: ChainInfo) => + z + .object({ + members: z.array( + z.object({ + member: z + .string() + .trim() + .superRefine(async (member, ctx) => { + if (!member) { + return z.NEVER; + } + + const addressOrPubkeyError = checkAddressOrPubkey(member, chain.addressPrefix); + + if (addressOrPubkeyError) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: addressOrPubkeyError }); + } else { + try { + const address = member.startsWith(chain.addressPrefix) + ? member + : pubkeyToAddress( + { type: "tendermint/PubKeySecp256k1", value: member }, + chain.addressPrefix, + ); + + const client = await StargateClient.connect(chain.nodeAddress); + const accountOnChain = await client.getAccount(address); + + if (!accountOnChain || !accountOnChain.pubkey) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "This account needs to send a transaction to appear on chain", + }); + } + } catch { + return z.NEVER; + } + } + }), + }), + ), + threshold: z.coerce + .number({ invalid_type_error: "Threshold must be a number" }) + .int("Threshold can't have decimals") + .min(1, "Threshold must be at least 1"), + }) + .superRefine(({ members }, ctx) => { + if (members.length !== 2) { + return; + } + + const firstEmptyMemberIndex = members.findIndex(({ member }) => member.trim() === ""); + + if (firstEmptyMemberIndex !== -1) { + const issue = { + code: z.ZodIssueCode.custom, + message: "At least 2 members needed", + path: [`members.${firstEmptyMemberIndex}.member`], + }; + + ctx.addIssue(issue); + } + }) + .superRefine(({ members }, ctx) => { + const addresses = members.map(({ member }) => { + if (!member.startsWith(chain.addressPrefix)) { + try { + const address = pubkeyToAddress( + { type: "tendermint/PubKeySecp256k1", value: member }, + chain.addressPrefix, + ); + + return address; + } catch {} + } + + return member; + }); + + const dupedAddresses = addresses.filter((member, i) => addresses.indexOf(member) !== i); + + const dupedAddressesIndexes: number[][] = []; + + for (const dupedAddress of dupedAddresses) { + const dupedIndexes = []; + + for (let i = 0; i < addresses.length; ++i) { + const index = addresses.indexOf(dupedAddress, i); + if (index !== -1) { + dupedIndexes.push(index); + } + } + + dupedAddressesIndexes.push(dupedIndexes.sort()); + } + + if (dupedAddressesIndexes.length) { + for (const dupedIndexes of dupedAddressesIndexes) { + for (const duplicateIndex of dupedIndexes) { + const issue = { + code: z.ZodIssueCode.custom, + message: `Members cannot be duplicate (${dupedIndexes + .map((index) => `#${index + 1}`) + .join(", ")})`, + path: [`members.${duplicateIndex}.member`], + }; + + ctx.addIssue(issue); + } + } + } else { + return z.NEVER; + } + }) + .refine( + ({ members, threshold }) => threshold <= members.filter(({ member }) => member !== "").length, + ({ members }) => ({ + message: `Threshold can't be higher than the number of members (${ + members.filter(({ member }) => member !== "").length + })`, + path: ["threshold"], + }), + ); From 3645b4a80e181aa502246a8138bb083f4d75d43e Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 10 Jun 2024 12:32:40 +0200 Subject: [PATCH 04/10] Add new create multisig form --- components/forms/CreateMultisigForm/index.tsx | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 components/forms/CreateMultisigForm/index.tsx diff --git a/components/forms/CreateMultisigForm/index.tsx b/components/forms/CreateMultisigForm/index.tsx new file mode 100644 index 0000000..0be29c2 --- /dev/null +++ b/components/forms/CreateMultisigForm/index.tsx @@ -0,0 +1,172 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { toastError } from "@/lib/utils"; +import { StargateClient } from "@cosmjs/stargate"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; +import { useFieldArray, useForm, useWatch } from "react-hook-form"; +import { z } from "zod"; +import { useChains } from "../../../context/ChainsContext"; +import { createMultisigFromCompressedSecp256k1Pubkeys } from "../../../lib/multisigHelpers"; +import ConfirmCreateMultisig from "./ConfirmCreateMultisig"; +import MemberFormField from "./MemberFormField"; +import { getCreateMultisigSchema } from "./formSchema"; + +export default function CreateMultisigForm() { + const router = useRouter(); + const { chain } = useChains(); + + const createMultisigSchema = getCreateMultisigSchema(chain); + + const createMultisigForm = useForm>({ + resolver: zodResolver(createMultisigSchema), + defaultValues: { members: [{ member: "" }, { member: "" }], threshold: 2 }, + }); + + const { + fields: membersFields, + append: membersAppend, + remove: membersRemove, + } = useFieldArray({ name: "members", control: createMultisigForm.control }); + + const watchedMembers = useWatch({ control: createMultisigForm.control, name: "members" }); + + useEffect(() => { + if (watchedMembers.every(({ member }) => member !== "")) { + membersAppend({ member: "" }, { shouldFocus: false }); + createMultisigForm.trigger(); + } + + if ( + watchedMembers.length > 2 && + watchedMembers.filter(({ member }) => member === "").length > 1 + ) { + const memberToRemove = watchedMembers.findIndex(({ member }) => member === ""); + membersRemove(memberToRemove); + createMultisigForm.trigger(); + } + }, [createMultisigForm, membersAppend, membersRemove, watchedMembers]); + + useEffect(() => { + const numMembers = watchedMembers.filter(({ member }) => member !== "").length; + createMultisigForm.setValue("threshold", Math.max(2, numMembers)); + }, [createMultisigForm, watchedMembers]); + + const submitCreateMultisig = async () => { + // Caution: threshold is string instead of number + const { members, threshold } = createMultisigForm.getValues(); + + const pubkeys = await Promise.all( + members + .filter(({ member }) => member !== "") + .map(async ({ member }) => { + try { + if (!member.startsWith(chain.addressPrefix)) { + return member; + } + + const client = await StargateClient.connect(chain.nodeAddress); + const accountOnChain = await client.getAccount(member); + + if (!accountOnChain || !accountOnChain.pubkey) { + return member; + } + + return String(accountOnChain.pubkey.value); + } catch { + return member; + } + }), + ); + + try { + const multisigAddress = await createMultisigFromCompressedSecp256k1Pubkeys( + pubkeys, + Number(threshold), + chain.addressPrefix, + chain.chainId, + ); + + router.push(`/${chain.registryName}/${multisigAddress}`); + } catch (e) { + console.error("Failed to create multisig:", e); + toastError({ + description: "Failed to create multisig", + fullError: e instanceof Error ? e : undefined, + }); + } + }; + + return ( + <> + + + Create multisig + +

+ Fill the form to create a new multisig account on{" "} + {chain.chainDisplayName || "Cosmos Hub"}. +

+

+ You can paste several addresses on the first input if they are separated by whitespace + or commas. +

+
+
+ +
+ + {membersFields.map((arrayField, index) => ( + + ))} + ( + + Threshold + + Number of signatures needed to broadcast a transaction + + +
+ + + out of{" "} + + {watchedMembers.filter(({ member }) => member !== "").length} + {" "} + members + +
+
+ +
+ )} + /> + + + +
+
+ + ); +} From 5d87cc378f44ead0e70605123c9ea825f7123d89 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 10 Jun 2024 12:32:50 +0200 Subject: [PATCH 05/10] Add MemberFormField --- .../CreateMultisigForm/MemberFormField.tsx | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 components/forms/CreateMultisigForm/MemberFormField.tsx diff --git a/components/forms/CreateMultisigForm/MemberFormField.tsx b/components/forms/CreateMultisigForm/MemberFormField.tsx new file mode 100644 index 0000000..5c9f428 --- /dev/null +++ b/components/forms/CreateMultisigForm/MemberFormField.tsx @@ -0,0 +1,63 @@ +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { ClipboardEventHandler } from "react"; +import { UseFormReturn, useFieldArray } from "react-hook-form"; +import { useChains } from "../../../context/ChainsContext"; +import { exampleAddress, examplePubkey } from "../../../lib/displayHelpers"; + +interface MemberFormFieldProps { + readonly createMultisigForm: UseFormReturn<{ members: { member: string }[]; threshold: number }>; + readonly index: number; +} + +export default function MemberFormField({ createMultisigForm, index }: MemberFormFieldProps) { + const { chain } = useChains(); + const { replace } = useFieldArray({ name: "members", control: createMultisigForm.control }); + + const onPaste: ClipboardEventHandler = (ev) => { + const rawData = ev.clipboardData.getData("text"); + const csv = rawData.split(","); + const finalValues = + csv.length > 1 + ? csv.map((el) => el.trim()).filter((el) => el !== "") + : rawData + .replace(/\n/g, " ") + .split(" ") + .map((el) => el.trim()) + .filter((el) => el !== ""); + + replace(finalValues.map((el) => ({ member: el }))); + + ev.preventDefault(); + }; + + return ( + ( + + Member #{index + 1} + Address or public key + + + + + + )} + /> + ); +} From 988b79133734f1c9cc58cd9242110b94c589091b Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 10 Jun 2024 12:33:00 +0200 Subject: [PATCH 06/10] Add confirmation modal --- .../ConfirmCreateMultisig.tsx | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 components/forms/CreateMultisigForm/ConfirmCreateMultisig.tsx diff --git a/components/forms/CreateMultisigForm/ConfirmCreateMultisig.tsx b/components/forms/CreateMultisigForm/ConfirmCreateMultisig.tsx new file mode 100644 index 0000000..e5d0ceb --- /dev/null +++ b/components/forms/CreateMultisigForm/ConfirmCreateMultisig.tsx @@ -0,0 +1,77 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { useChains } from "@/context/ChainsContext"; +import { ReloadIcon } from "@radix-ui/react-icons"; +import { Info } from "lucide-react"; +import { UseFormReturn, useFormState } from "react-hook-form"; + +interface ConfirmCreateMultisigProps { + readonly createMultisigForm: UseFormReturn<{ members: { member: string }[]; threshold: number }>; +} + +export default function ConfirmCreateMultisig({ createMultisigForm }: ConfirmCreateMultisigProps) { + const { chain } = useChains(); + const { isValid, isSubmitting, isSubmitted } = useFormState(createMultisigForm); + const { members, threshold } = createMultisigForm.getValues(); + + const loading = isSubmitting || isSubmitted; + + return ( + + + + + + Create a new multisig on "{chain.registryName}"? +

Members

+
+ {members + .filter(({ member }) => member !== "") + .map(({ member }) => ( +
+
+

{member}

+
+
+ ))} +
+
+ +

+ {threshold} {threshold === 1 ? "signature" : "signatures"} needed to send a transaction. +

+
+
+ + + + +
+
+
+ ); +} From 5934757ea9f4fdd26cfa9da7983f675d2b10233a Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 10 Jun 2024 12:33:09 +0200 Subject: [PATCH 07/10] Remove old create multisig form --- components/forms/MultisigForm.tsx | 245 ------------------------------ 1 file changed, 245 deletions(-) delete mode 100644 components/forms/MultisigForm.tsx diff --git a/components/forms/MultisigForm.tsx b/components/forms/MultisigForm.tsx deleted file mode 100644 index 7f009f2..0000000 --- a/components/forms/MultisigForm.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { toastError } from "@/lib/utils"; -import { StargateClient } from "@cosmjs/stargate"; -import { NextRouter, withRouter } from "next/router"; -import { useState } from "react"; -import { useChains } from "../../context/ChainsContext"; -import { exampleAddress, examplePubkey } from "../../lib/displayHelpers"; -import { createMultisigFromCompressedSecp256k1Pubkeys } from "../../lib/multisigHelpers"; -import Button from "../inputs/Button"; -import Input from "../inputs/Input"; -import ThresholdInput from "../inputs/ThresholdInput"; -import StackableContainer from "../layout/StackableContainer"; - -const emptyPubKeyGroup = () => { - return { address: "", compressedPubkey: "", keyError: "", isPubkey: false }; -}; - -interface Props { - router: NextRouter; -} - -const MultiSigForm = (props: Props) => { - const { chain } = useChains(); - const [pubkeys, setPubkeys] = useState([emptyPubKeyGroup(), emptyPubKeyGroup()]); - const [threshold, setThreshold] = useState(2); - const [processing, setProcessing] = useState(false); - - const handleChangeThreshold = (e: React.ChangeEvent) => { - let newThreshold = parseInt(e.target.value, 10); - if (newThreshold > pubkeys.length || newThreshold <= 0) { - newThreshold = threshold; - } - setThreshold(newThreshold); - }; - - const handleKeyGroupChange = (index: number, e: React.ChangeEvent) => { - const tempPubkeys = [...pubkeys]; - if (e.target.name === "compressedPubkey") { - tempPubkeys[index].compressedPubkey = e.target.value; - } else if (e.target.name === "address") { - tempPubkeys[index].address = e.target.value; - } - setPubkeys(tempPubkeys); - }; - - const handleAddKey = () => { - const tempPubkeys = [...pubkeys]; - setPubkeys(tempPubkeys.concat(emptyPubKeyGroup())); - }; - - const handleRemove = (index: number) => { - const tempPubkeys = [...pubkeys]; - const oldLength = tempPubkeys.length; - tempPubkeys.splice(index, 1); - const newThreshold = threshold > tempPubkeys.length ? tempPubkeys.length : oldLength; - setPubkeys(tempPubkeys); - setThreshold(newThreshold); - }; - - const getPubkeyFromNode = async (address: string) => { - const client = await StargateClient.connect(chain.nodeAddress); - const accountOnChain = await client.getAccount(address); - console.log(accountOnChain); - if (!accountOnChain || !accountOnChain.pubkey) { - throw new Error( - "Account has no pubkey on chain, this address will need to send a transaction to appear on chain.", - ); - } - return accountOnChain.pubkey.value; - }; - - const handleKeyBlur = async (index: number, { target }: React.ChangeEvent) => { - try { - const tempPubkeys = [...pubkeys]; - let pubkey; - // use pubkey - console.log(tempPubkeys[index]); - if (tempPubkeys[index].isPubkey) { - pubkey = target.value; - if (pubkey.length !== 44) { - throw new Error("Invalid Secp256k1 pubkey"); - } - } else { - // use address to fetch pubkey - const address = target.value; - if (address.length > 0) { - pubkey = await getPubkeyFromNode(address); - } - } - - tempPubkeys[index].compressedPubkey = pubkey; - tempPubkeys[index].keyError = ""; - setPubkeys(tempPubkeys); - } catch (e) { - console.error("Invalid address or pubkey", e); - const tempPubkeys = [...pubkeys]; - tempPubkeys[index].keyError = e instanceof Error ? e.message : "Invalid address or pubkey"; - setPubkeys(tempPubkeys); - } - }; - - const handleCreate = async () => { - setProcessing(true); - const compressedPubkeys = pubkeys.map((item) => item.compressedPubkey); - let multisigAddress; - try { - multisigAddress = await createMultisigFromCompressedSecp256k1Pubkeys( - compressedPubkeys, - threshold, - chain.addressPrefix, - chain.chainId, - ); - props.router.push(`/${chain.registryName}/${multisigAddress}`); - } catch (e) { - console.error("Failed to create multisig:", e); - toastError({ - description: "Failed to create multisig", - fullError: e instanceof Error ? e : undefined, - }); - } - }; - - const togglePubkey = (index: number) => { - const tempPubkeys = [...pubkeys]; - tempPubkeys[index].isPubkey = !tempPubkeys[index].isPubkey; - setPubkeys(tempPubkeys); - }; - - return ( - <> - - -

Add the addresses that will make up this multisig.

-
- {pubkeys.map((pubkeyGroup, index) => { - return ( - -
- {pubkeys.length > 2 && ( - - )} -
- ) => { - handleKeyGroupChange(index, e); - }} - value={ - pubkeyGroup.isPubkey ? pubkeyGroup.compressedPubkey : pubkeyGroup.address - } - label={pubkeyGroup.isPubkey ? "Public Key (Secp256k1)" : "Address"} - name={pubkeyGroup.isPubkey ? "compressedPubkey" : "address"} - width="100%" - placeholder={`E.g. ${ - pubkeyGroup.isPubkey - ? examplePubkey(index) - : exampleAddress(index, chain.addressPrefix) - }`} - error={pubkeyGroup.keyError} - onBlur={(e: React.ChangeEvent) => { - handleKeyBlur(index, e); - }} - /> - -
-
-
- ); - })} - - - Create a new multisig on "{chain.registryName}"? + Create a new multisig on "{chain.chainDisplayName}"?

Members

{members From 5fc8c2c6f1f0f1d79783b9953667d5256bb285df Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 10 Jun 2024 18:10:52 +0200 Subject: [PATCH 10/10] Use shared field array for replace --- .../forms/CreateMultisigForm/MemberFormField.tsx | 15 +++++++++++---- components/forms/CreateMultisigForm/index.tsx | 2 ++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/components/forms/CreateMultisigForm/MemberFormField.tsx b/components/forms/CreateMultisigForm/MemberFormField.tsx index 5c9f428..ea364dc 100644 --- a/components/forms/CreateMultisigForm/MemberFormField.tsx +++ b/components/forms/CreateMultisigForm/MemberFormField.tsx @@ -8,18 +8,25 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { ClipboardEventHandler } from "react"; -import { UseFormReturn, useFieldArray } from "react-hook-form"; +import { UseFieldArrayReplace, UseFormReturn } from "react-hook-form"; import { useChains } from "../../../context/ChainsContext"; import { exampleAddress, examplePubkey } from "../../../lib/displayHelpers"; interface MemberFormFieldProps { readonly createMultisigForm: UseFormReturn<{ members: { member: string }[]; threshold: number }>; readonly index: number; + readonly membersReplace: UseFieldArrayReplace< + { members: { member: string }[]; threshold: number }, + "members" + >; } -export default function MemberFormField({ createMultisigForm, index }: MemberFormFieldProps) { +export default function MemberFormField({ + createMultisigForm, + index, + membersReplace, +}: MemberFormFieldProps) { const { chain } = useChains(); - const { replace } = useFieldArray({ name: "members", control: createMultisigForm.control }); const onPaste: ClipboardEventHandler = (ev) => { const rawData = ev.clipboardData.getData("text"); @@ -33,7 +40,7 @@ export default function MemberFormField({ createMultisigForm, index }: MemberFor .map((el) => el.trim()) .filter((el) => el !== ""); - replace(finalValues.map((el) => ({ member: el }))); + membersReplace(finalValues.map((el) => ({ member: el }))); ev.preventDefault(); }; diff --git a/components/forms/CreateMultisigForm/index.tsx b/components/forms/CreateMultisigForm/index.tsx index 0be29c2..5a91588 100644 --- a/components/forms/CreateMultisigForm/index.tsx +++ b/components/forms/CreateMultisigForm/index.tsx @@ -37,6 +37,7 @@ export default function CreateMultisigForm() { fields: membersFields, append: membersAppend, remove: membersRemove, + replace: membersReplace, } = useFieldArray({ name: "members", control: createMultisigForm.control }); const watchedMembers = useWatch({ control: createMultisigForm.control, name: "members" }); @@ -135,6 +136,7 @@ export default function CreateMultisigForm() { key={arrayField.id} createMultisigForm={createMultisigForm} index={index} + membersReplace={membersReplace} /> ))}