cosmos-multisig-ui/components/forms/CreateMultisigForm/index.tsx
ishavenikar 822c72094d Add support for zenith account (#4)
Part of https://www.notion.so/Implement-stacks-1b5a6b22d472806a82f5dafed6955138

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: #4
Co-authored-by: ishavenikar <ishavenikar@noreply.git.vdb.to>
Co-committed-by: ishavenikar <ishavenikar@noreply.git.vdb.to>
2025-06-04 09:43:21 +00:00

180 lines
6.2 KiB
TypeScript

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 { getKeplrKey } from "@/lib/keplr";
import { toastError } from "@/lib/utils";
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";
import { accountFromAny, SigningZenithClient } from "@/utils/cosmos-client.ts";
export default function CreateMultisigForm() {
const router = useRouter();
const { chain } = useChains();
const createMultisigSchema = getCreateMultisigSchema(chain);
const createMultisigForm = useForm<z.infer<typeof createMultisigSchema>>({
resolver: zodResolver(createMultisigSchema),
defaultValues: { members: [{ member: "" }, { member: "" }], threshold: 2 },
});
const {
fields: membersFields,
append: membersAppend,
remove: membersRemove,
replace: membersReplace,
} = 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 () => {
try {
// Caution: threshold is string instead of number
const { members, threshold } = createMultisigForm.getValues();
const pubkeys = await Promise.all(
members
.filter(({ member }) => member !== "")
.map(async ({ member }) => {
if (!member.startsWith(chain.addressPrefix)) {
return member;
}
const client = await SigningZenithClient.connect(chain.nodeAddress, {
accountParser: accountFromAny
});
const accountOnChain = await client.getAccount(member);
if (!accountOnChain || !accountOnChain.pubkey) {
throw new Error(
`Member "${member}" is not a pubkey and is not on chain. It needs to send a transaction to appear on chain or you can provide its pubkey`,
);
}
return String(accountOnChain.pubkey.value);
}),
);
const { bech32Address: address } = await getKeplrKey(chain.chainId);
const multisigAddress = await createMultisigFromCompressedSecp256k1Pubkeys(
pubkeys,
Number(threshold),
chain.addressPrefix,
chain.chainId,
address,
);
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 (
<>
<Card>
<CardHeader>
<CardTitle>Create multisig</CardTitle>
<CardDescription>
<p className="mt-2">
Fill the form to create a new multisig account on{" "}
{chain.chainDisplayName || "Cosmos Hub"}.
</p>
<p className="mt-2">
You can paste several addresses on the first input if they are separated by whitespace
or commas.
</p>
</CardDescription>
</CardHeader>
<CardContent>
<Form {...createMultisigForm}>
<form
id="create-multisig-form"
onSubmit={createMultisigForm.handleSubmit(submitCreateMultisig)}
className="space-y-4"
>
{membersFields.map((arrayField, index) => (
<MemberFormField
key={arrayField.id}
createMultisigForm={createMultisigForm}
index={index}
membersReplace={membersReplace}
/>
))}
<FormField
control={createMultisigForm.control}
name="threshold"
render={({ field }) => (
<FormItem>
<FormLabel>Threshold</FormLabel>
<FormDescription>
Number of signatures needed to broadcast a transaction
</FormDescription>
<FormControl className="">
<div className="flex items-center gap-2">
<Input className="w-20" placeholder="2" {...field} />
<span className="text-sm text-muted-foreground">
out of{" "}
<em className="text-base font-bold not-italic text-white">
{watchedMembers.filter(({ member }) => member !== "").length}
</em>{" "}
members
</span>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<ConfirmCreateMultisig createMultisigForm={createMultisigForm} />
</form>
</Form>
</CardContent>
</Card>
</>
);
}