diff --git a/components/dataViews/MultisigView/index.tsx b/components/dataViews/MultisigView/index.tsx index 9f8703d..1c12641 100644 --- a/components/dataViews/MultisigView/index.tsx +++ b/components/dataViews/MultisigView/index.tsx @@ -1,13 +1,18 @@ -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Alert, AlertDescription } from "@/components/ui/alert"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { isChainInfoFilled } from "@/context/ChainsContext/helpers"; -import { explorerLinkAccount } from "@/lib/displayHelpers"; -import { getMultisigAccount } from "@/lib/multisigHelpers"; +import { checkAddress } from "@/lib/displayHelpers"; +import { + HostedMultisig, + createMultisigFromCompressedSecp256k1Pubkeys, + getHostedMultisig, +} from "@/lib/multisigHelpers"; import { toastError } from "@/lib/utils"; -import { SinglePubkey, isSecp256k1Pubkey, pubkeyToAddress } from "@cosmjs/amino"; -import { StargateClient } from "@cosmjs/stargate"; +import { isMultisigThresholdPubkey, isSecp256k1Pubkey, pubkeyToAddress } from "@cosmjs/amino"; +import { assert } from "@cosmjs/utils"; import copy from "copy-to-clipboard"; import { AlertCircle, ArrowUpRightSquare, Copy, Info, Loader2 } from "lucide-react"; +import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { toast } from "sonner"; @@ -15,68 +20,58 @@ import { useChains } from "../../../context/ChainsContext"; import { Button } from "../../ui/button"; import BalancesTable from "../BalancesTable"; -type MultisigInfo = - | { - readonly status: "success"; - readonly address: string; - readonly members: readonly SinglePubkey[]; - readonly threshold: string; - } - | { - readonly status: "error"; - readonly error: "account-not-found" | "pubkeys-unavailable"; - } - | { - readonly status: "loading"; - }; - export default function MultisigView() { const router = useRouter(); const { chain } = useChains(); + const [hostedMultisig, setHostedMultisig] = useState(); - const [multisigInfo, setMultisigInfo] = useState({ status: "loading" }); + const multisigAddress = typeof router.query.address === "string" ? router.query.address : null; useEffect(() => { - (async function fetchMultisigInfo() { + (async function updateHostedMultisig() { try { - const multisigAddress = - typeof router.query.address === "string" ? router.query.address : null; - - if (!multisigAddress || !chain.nodeAddress || !isChainInfoFilled(chain)) { + if (!multisigAddress || !isChainInfoFilled(chain) || !chain.nodeAddress) { return; } - const client = await StargateClient.connect(chain.nodeAddress); - const [pubkey, account] = await getMultisigAccount( - multisigAddress, - chain.addressPrefix, - client, - ); + const newHostedMultisig = await getHostedMultisig(multisigAddress, chain); - if (!account) { - setMultisigInfo({ status: "error", error: "account-not-found" }); - } else { - setMultisigInfo({ - status: "success", - address: account.address, - members: pubkey.value.pubkeys, - threshold: pubkey.value.threshold, - }); + // If the multisig is on chain and not on DB, automatically create it on DB and reload the view + if (newHostedMultisig.hosted === "chain" && newHostedMultisig.accountOnChain?.pubkey) { + assert( + isMultisigThresholdPubkey(newHostedMultisig.accountOnChain.pubkey), + "Pubkey on chain is not of type MultisigThreshold", + ); + + await createMultisigFromCompressedSecp256k1Pubkeys( + newHostedMultisig.accountOnChain.pubkey.value.pubkeys.map((p) => p.value), + Number(newHostedMultisig.accountOnChain.pubkey.value.threshold), + chain.addressPrefix, + chain.chainId, + ); + + router.reload(); } + + setHostedMultisig(newHostedMultisig); } catch (e) { console.error("Failed to find multisig:", e); - setMultisigInfo({ status: "error", error: "pubkeys-unavailable" }); toastError({ description: "Failed to find multisig", fullError: e instanceof Error ? e : undefined, }); } })(); - }, [chain, router.query.address]); + }, [chain, multisigAddress, router]); const explorerLink = - multisigInfo.status === "success" - ? explorerLinkAccount(chain.explorerLinks.account, multisigInfo.address) + hostedMultisig?.hosted === "chain" || hostedMultisig?.hosted === "db+chain" + ? hostedMultisig.explorerLink + : null; + + const pubkey = + hostedMultisig?.hosted === "db" || hostedMultisig?.hosted === "db+chain" + ? hostedMultisig.pubkeyOnDb : null; return ( @@ -86,31 +81,39 @@ export default function MultisigView() { Multisig info - {multisigInfo.status === "success" ? ( - <> -
{ - copy(multisigInfo.address); - toast(`Copied address to clipboard`, { description: multisigInfo.address }); - }} - className=" flex items-center space-x-4 rounded-md border p-4 transition-colors hover:cursor-pointer hover:bg-muted/50" - > - -
-

Multisig address

-

{multisigInfo.address}

-
+ {multisigAddress ? ( +
{ + copy(multisigAddress); + toast(`Copied address to clipboard`, { description: multisigAddress }); + }} + className=" flex items-center space-x-4 rounded-md border p-4 transition-colors hover:cursor-pointer hover:bg-muted/50" + > + +
+

Multisig address

+

{multisigAddress}

- {explorerLink ? ( - - ) : null} +
+ ) : null} + {!hostedMultisig ? ( +
+ +

Loading multisig info

+
+ ) : null} + {explorerLink ? ( + + ) : null} + {pubkey ? ( + <>

Members

- {multisigInfo.members.map((member) => { + {pubkey.value.pubkeys.map((member) => { const memberAddress = pubkeyToAddress(member, chain.addressPrefix); // simplePubkey is base64 encoded compressed secp256k1 in almost every case. The fallback is added to be safe though. const simplePubkey = isSecp256k1Pubkey(member) @@ -137,53 +140,54 @@ export default function MultisigView() {
-

{multisigInfo.threshold} signatures needed to send a transaction.

+

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

) : null} - {multisigInfo.status === "error" ? ( - <> - {multisigInfo.error === "account-not-found" ? ( - - - Error - - An account needs to be present on chain before creating a transaction. Send some - tokens to the address first. - - - ) : null} - {multisigInfo.error === "pubkeys-unavailable" ? ( - - - Error - -

- This multisig address's pubkeys are not available, and so it cannot be used - with this tool. -

-

- You can recreate it with this tool here, or sign and broadcast a transaction - with the tool you used to create it. Either option will make the pubkeys - accessible and will allow this tool to use this multisig fully. -

-
-
- ) : null} - + {hostedMultisig?.hosted === "nowhere" ? ( + + + + {checkAddress(multisigAddress || "", chain.addressPrefix) ? ( +

+ The multisig address does not look like it belongs to {chain.chainDisplayName}{" "} + and it was not found neither on the network nor on the tool's database. You need + to create it using this tool. +

+ ) : ( +

+ The multisig address was not found neither on the network nor on the tool's + database. You need to create it using this tool. +

+ )} + +
+
) : null} - {multisigInfo.status === "loading" ? ( -
- -

Loading multisig info

-
+ {hostedMultisig?.hosted === "db" ? ( + + + + After creating the multisig you need to send some tokens to its address so that it + appears on the network. + + ) : null} - {multisigInfo.status === "success" ? ( + {hostedMultisig?.hosted === "db+chain" && multisigAddress ? ( <> @@ -195,7 +199,7 @@ export default function MultisigView() { - +