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"], + }), + );