Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve multisig view #214

Merged
merged 5 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 110 additions & 106 deletions components/dataViews/MultisigView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,77 @@
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";
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 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,
);

if (!account) {
setMultisigInfo({ status: "error", error: "account-not-found" });
} else {
setMultisigInfo({
status: "success",
address: account.address,
members: pubkey.value.pubkeys,
threshold: pubkey.value.threshold,
});
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 (
Expand All @@ -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)
Expand All @@ -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>
Expand All @@ -195,7 +199,7 @@ export default function MultisigView() {
</CardDescription>
</CardHeader>
<CardContent>
<BalancesTable walletAddress={multisigInfo.address} />
<BalancesTable walletAddress={multisigAddress} />
</CardContent>
</Card>
</>
Expand Down
5 changes: 2 additions & 3 deletions components/forms/FindMultisigForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ import { useForm } from "react-hook-form";
import { z } from "zod";
import { useChains } from "../../context/ChainsContext";
import { exampleAddress } from "../../lib/displayHelpers";
import { getMultisigAccount } from "../../lib/multisigHelpers";

const existsMultisigAccount = async (chain: ChainInfo, address: string) => {
try {
const client = await StargateClient.connect(chain.nodeAddress);
const [, account] = await getMultisigAccount(address, chain.addressPrefix, client);
return account !== null;
const accountOnChain = await client.getAccount(address);
return accountOnChain !== null;
} catch {
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion components/inputs/ButtonWithConfirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function ButtonWithConfirm({
<Button
size="sm"
variant={toConfirm ? "destructive" : "default"}
className={cn(toConfirm ? "" : "bg-yellow-300 hover:bg-yellow-300")}
className={cn(toConfirm ? "" : "bg-amber-400 text-black hover:bg-amber-400")}
onClick={
toConfirm
? onClick
Expand Down
1 change: 1 addition & 0 deletions components/ui/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const alertVariants = cva(
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
warning: "border-black/50 bg-amber-400 text-black [&>svg]:text-black",
},
},
defaultVariants: {
Expand Down
Loading
Loading