Improve multisig view

This commit is contained in:
abefernan
2024-06-10 18:18:05 +02:00
parent 2ebd85b847
commit 3d6d29386a
+110 -106
View File
@@ -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<HostedMultisig>();
const [multisigInfo, setMultisigInfo] = useState<MultisigInfo>({ 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() {
<CardTitle>Multisig info</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-5">
{multisigInfo.status === "success" ? (
<>
<div
onClick={async () => {
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"
>
<Copy className="w-5" />
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">Multisig address</p>
<p className="text-sm text-muted-foreground">{multisigInfo.address}</p>
</div>
{multisigAddress ? (
<div
onClick={async () => {
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"
>
<Copy className="w-5" />
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">Multisig address</p>
<p className="text-sm text-muted-foreground">{multisigAddress}</p>
</div>
{explorerLink ? (
<Button asChild variant="secondary">
<a href={explorerLink} target="_blank">
View in explorer <ArrowUpRightSquare className="ml-1" />
</a>
</Button>
) : null}
</div>
) : null}
{!hostedMultisig ? (
<div className="flex items-center gap-2">
<Loader2 className="animate-spin" />
<p>Loading multisig info</p>
</div>
) : null}
{explorerLink ? (
<Button asChild variant="secondary">
<a href={explorerLink} target="_blank">
View in explorer <ArrowUpRightSquare className="ml-1" />
</a>
</Button>
) : null}
{pubkey ? (
<>
<h4 className="font-bold">Members</h4>
<div className="mx-4 flex flex-col gap-2">
{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() {
</div>
<div className="flex items-center gap-1 text-sm text-secondary">
<Info className="text-secondary" />
<p>{multisigInfo.threshold} signatures needed to send a transaction.</p>
<p>
{pubkey.value.threshold}{" "}
{pubkey.value.threshold === "1" ? "signature" : "signatures"} needed to send a
transaction.
</p>
</div>
</>
) : null}
{multisigInfo.status === "error" ? (
<>
{multisigInfo.error === "account-not-found" ? (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
An account needs to be present on chain before creating a transaction. Send some
tokens to the address first.
</AlertDescription>
</Alert>
) : null}
{multisigInfo.error === "pubkeys-unavailable" ? (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
<p>
This multisig address's pubkeys are not available, and so it cannot be used
with this tool.
</p>
<p className="mt-2">
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.
</p>
</AlertDescription>
</Alert>
) : null}
</>
{hostedMultisig?.hosted === "nowhere" ? (
<Alert variant="warning">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{checkAddress(multisigAddress || "", chain.addressPrefix) ? (
<p>
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.
</p>
) : (
<p>
The multisig address was not found neither on the network nor on the tool's
database. You need to create it using this tool.
</p>
)}
<Button
asChild
className="mt-2 border border-black/50 bg-white text-amber-600 hover:bg-white"
>
<Link href={`/${chain.registryName}/create`}>Create new multisig</Link>
</Button>
</AlertDescription>
</Alert>
) : null}
{multisigInfo.status === "loading" ? (
<div className="flex items-center gap-2">
<Loader2 className="animate-spin" />
<p>Loading multisig info</p>
</div>
{hostedMultisig?.hosted === "db" ? (
<Alert variant="warning">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
After creating the multisig you need to send some tokens to its address so that it
appears on the network.
</AlertDescription>
</Alert>
) : null}
</CardContent>
</Card>
{multisigInfo.status === "success" ? (
{hostedMultisig?.hosted === "db+chain" && multisigAddress ? (
<>
<Button asChild variant="secondary" className="my-4">
<a href={`/${chain.registryName}/${multisigInfo.address}/transaction/new`}>
<a href={`/${chain.registryName}/${multisigAddress}/transaction/new`}>
Create new transaction
</a>
</Button>
@@ -195,7 +199,7 @@ export default function MultisigView() {
</CardDescription>
</CardHeader>
<CardContent>
<BalancesTable walletAddress={multisigInfo.address} />
<BalancesTable walletAddress={multisigAddress} />
</CardContent>
</Card>
</>