From 12a7fd977e64c8800621ced05d8734d47eae51b6 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Fri, 5 Apr 2024 19:45:46 +0200 Subject: [PATCH 1/7] Move BalancesTable up --- components/dataViews/AccountView/index.tsx | 2 +- components/dataViews/{AccountView => }/BalancesTable.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename components/dataViews/{AccountView => }/BalancesTable.tsx (94%) diff --git a/components/dataViews/AccountView/index.tsx b/components/dataViews/AccountView/index.tsx index 735df54..6729061 100644 --- a/components/dataViews/AccountView/index.tsx +++ b/components/dataViews/AccountView/index.tsx @@ -9,7 +9,7 @@ import { useState } from "react"; import { toast } from "sonner"; import { useChains } from "../../../context/ChainsContext"; import { Button } from "../../ui/button"; -import BalancesTable from "./BalancesTable"; +import BalancesTable from "../BalancesTable"; import ButtonConnectWallet from "./ButtonConnectWallet"; export default function AccountView() { diff --git a/components/dataViews/AccountView/BalancesTable.tsx b/components/dataViews/BalancesTable.tsx similarity index 94% rename from components/dataViews/AccountView/BalancesTable.tsx rename to components/dataViews/BalancesTable.tsx index b52c1c3..d7fbaef 100644 --- a/components/dataViews/AccountView/BalancesTable.tsx +++ b/components/dataViews/BalancesTable.tsx @@ -4,8 +4,8 @@ import { toastError } from "@/lib/utils"; import { Coin } from "@cosmjs/amino"; import { StargateClient } from "@cosmjs/stargate"; import { useEffect, useState } from "react"; -import { useChains } from "../../../context/ChainsContext"; -import { Avatar, AvatarFallback, AvatarImage } from "../../ui/avatar"; +import { useChains } from "../../context/ChainsContext"; +import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; interface BalancesTableProps { readonly walletAddress: string; From 32c2eb14889a0167d8ab52bd770b1dbdbd969bbb Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Fri, 5 Apr 2024 19:46:27 +0200 Subject: [PATCH 2/7] Name home page --- pages/[chainName]/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/[chainName]/index.tsx b/pages/[chainName]/index.tsx index 843bdb2..60269e3 100644 --- a/pages/[chainName]/index.tsx +++ b/pages/[chainName]/index.tsx @@ -2,7 +2,7 @@ import FindMultisigForm from "@/components/forms/FindMultisigForm"; import Head from "@/components/head"; import { useChains } from "@/context/ChainsContext"; -const MultiPage = () => { +const FindMultisigPage = () => { const { chain } = useChains(); return ( @@ -13,4 +13,4 @@ const MultiPage = () => { ); }; -export default MultiPage; +export default FindMultisigPage; From c2697edf3e51a87ded09a171707ccf1740003d93 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Fri, 5 Apr 2024 19:46:45 +0200 Subject: [PATCH 3/7] Add new MultisigView dataview --- components/dataViews/MultisigView/index.tsx | 206 ++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 components/dataViews/MultisigView/index.tsx diff --git a/components/dataViews/MultisigView/index.tsx b/components/dataViews/MultisigView/index.tsx new file mode 100644 index 0000000..6020fd9 --- /dev/null +++ b/components/dataViews/MultisigView/index.tsx @@ -0,0 +1,206 @@ +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +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 { toastError } from "@/lib/utils"; +import { SinglePubkey, isSecp256k1Pubkey, pubkeyToAddress } from "@cosmjs/amino"; +import { StargateClient } from "@cosmjs/stargate"; +import copy from "copy-to-clipboard"; +import { AlertCircle, ArrowUpRightSquare, Copy, Info } from "lucide-react"; +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 [multisigInfo, setMultisigInfo] = useState({ status: "loading" }); + + useEffect(() => { + (async function fetchMultisigInfo() { + try { + const multisigAddress = + typeof router.query.address === "string" ? router.query.address : null; + + if (!multisigAddress || !chain.nodeAddress || !isChainInfoFilled(chain)) { + return; + } + + const client = await StargateClient.connect(chain.nodeAddress); + const [pubkey, account] = await getMultisigAccount( + multisigAddress, + chain.addressPrefix, + client, + ); + + if (!account) { + setMultisigInfo({ status: "error", error: "account-not-found" }); + } else { + setMultisigInfo({ + status: "success", + address: account.address, + members: pubkey.value.pubkeys, + threshold: pubkey.value.threshold, + }); + } + } 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]); + + const explorerLink = + multisigInfo.status === "success" + ? explorerLinkAccount(chain.explorerLinks.account, multisigInfo.address) + : null; + + return ( +
+ + + 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}

+
+
+ {explorerLink ? ( + + ) : null} +

Members

+
+ {multisigInfo.members.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) + ? member.value + : `${member.type} pubkey`; + + return ( +
{ + copy(memberAddress); + toast(`Copied address to clipboard`, { description: memberAddress }); + }} + className="flex items-center space-x-2 rounded-md border p-2 transition-colors hover:cursor-pointer hover:bg-muted/50" + > + +
+

{memberAddress}

+

{simplePubkey}

+
+
+ ); + })} +
+
+ Threshold = {multisigInfo.threshold} +
+
+ +

+ Transactions need to be signed by {multisigInfo.threshold} out of the{" "} + {multisigInfo.members.length} members. +

+
+ + ) : 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} + + ) : null} +
+
+ {multisigInfo.status === "success" ? ( + <> + + + + Holdings + + This multisig's list of tokens on {chain.chainDisplayName || "Cosmos Hub"} + + + + + + + + ) : null} +
+ ); +} From 7f55551de8b7cc8819c0732f1fdd1460a26ee49c Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Fri, 5 Apr 2024 19:47:09 +0200 Subject: [PATCH 4/7] Add new MultisigPage with dataview and breadcrumb --- pages/[chainName]/[address]/index.tsx | 160 +++++--------------------- 1 file changed, 31 insertions(+), 129 deletions(-) diff --git a/pages/[chainName]/[address]/index.tsx b/pages/[chainName]/[address]/index.tsx index 8aa237e..99d80b7 100644 --- a/pages/[chainName]/[address]/index.tsx +++ b/pages/[chainName]/[address]/index.tsx @@ -1,134 +1,36 @@ -import { isChainInfoFilled } from "@/context/ChainsContext/helpers"; -import { MultisigThresholdPubkey, SinglePubkey } from "@cosmjs/amino"; -import { Account, StargateClient } from "@cosmjs/stargate"; -import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; -import HashView from "../../../components/dataViews/HashView"; -import MultisigHoldings from "../../../components/dataViews/MultisigHoldings"; -import MultisigMembers from "../../../components/dataViews/MultisigMembers"; -import Button from "../../../components/inputs/Button"; -import Page from "../../../components/layout/Page"; -import StackableContainer from "../../../components/layout/StackableContainer"; -import { useChains } from "../../../context/ChainsContext"; -import { explorerLinkAccount } from "../../../lib/displayHelpers"; -import { getMultisigAccount } from "../../../lib/multisigHelpers"; +import MultisigView from "@/components/dataViews/MultisigView"; +import Head from "@/components/head"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { useChains } from "@/context/ChainsContext"; +import Link from "next/link"; -function participantPubkeysFromMultisig( - multisig: MultisigThresholdPubkey, -): readonly SinglePubkey[] { - return multisig.value.pubkeys; -} - -const Multipage = () => { - const router = useRouter(); +export default function MultisigPage() { const { chain } = useChains(); - const [holdings, setHoldings] = useState([]); - const [accountOnChain, setAccountOnChain] = useState(null); - const [pubkey, setPubkey] = useState(); - const [hasAccountError, setHasAccountError] = useState(false); - - const multisigAddress = router.query.address?.toString(); - const explorerLink = explorerLinkAccount(chain.explorerLinks.account, multisigAddress || ""); - - useEffect(() => { - (async function fetchMultisig() { - try { - if (!multisigAddress || !isChainInfoFilled(chain) || !chain.nodeAddress) { - return; - } - - const client = await StargateClient.connect(chain.nodeAddress); - - const tempHoldings = await client.getAllBalances(multisigAddress); - setHoldings(tempHoldings); - - const result = await getMultisigAccount(multisigAddress, chain.addressPrefix, client); - setPubkey(result[0]); - setAccountOnChain(result[1]); - setHasAccountError(false); - } catch (error: unknown) { - setHasAccountError(true); - console.error( - error instanceof Error ? error.message : "Multisig address could not be found", - ); - } - })(); - }, [chain, multisigAddress]); - return ( - - - - -

{multisigAddress ? : "No Address"}

- {explorerLink ? : null} -
- {pubkey ? ( - - ) : null} - - {hasAccountError || !accountOnChain ? ( - -
- {hasAccountError ? ( - <> -

- 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} - {!!accountOnChain ? ( -

- An account needs to be present on chain before creating a transaction. Send some - tokens to the address first. -

- ) : null} -
-
- ) : null} -