diff --git a/components/dataViews/ListUserMultisigs.tsx b/components/dataViews/ListUserMultisigs.tsx new file mode 100644 index 0000000..54ea7be --- /dev/null +++ b/components/dataViews/ListUserMultisigs.tsx @@ -0,0 +1,234 @@ +import { useChains } from "@/context/ChainsContext"; +import { getConnectError } from "@/lib/errorHelpers"; +import { MultisigFromQuery } from "@/lib/graphqlHelpers"; +import { requestJson } from "@/lib/request"; +import { toastError } from "@/lib/utils"; +import { DbNonce } from "@/types"; +import { WalletInfo } from "@/types/signing"; +import { MultisigThresholdPubkey } from "@cosmjs/amino"; +import { toBase64 } from "@cosmjs/encoding"; +import { StargateClient } from "@cosmjs/stargate"; +import { Loader2, MoveRightIcon } from "lucide-react"; +import Image from "next/image"; +import Link from "next/link"; +import { useCallback, useLayoutEffect, useState } from "react"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { Label } from "../ui/label"; +import { Switch } from "../ui/switch"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; + +type FetchedMultisigs = { + readonly created: readonly MultisigFromQuery[]; + readonly belonged: readonly MultisigFromQuery[]; +}; + +export default function ListUserMultisigs() { + const { chain } = useChains(); + const [loading, setLoading] = useState(false); + const [walletInfo, setWalletInfo] = useState | null>(null); + const [showBelonged, setShowBelonged] = useState(false); + const [multisigs, setMultisigs] = useState(null); + + const getSignature = useCallback( + async (address: string) => { + const client = await StargateClient.connect(chain.nodeAddress); + const accountOnChain = await client.getAccount(address); + + if (!accountOnChain) { + throw new Error(`Account not found on chain for ${address}`); + } + + const { nonce }: DbNonce = await requestJson( + `/api/chain/${chain.chainId}/nonce/${accountOnChain.address}`, + ); + + const { signature } = await window.keplr.signAmino(chain.chainId, accountOnChain.address, { + chain_id: "", + account_number: "0", + sequence: "0", + fee: { gas: "0", amount: [] }, + msgs: [ + { + type: "sign/MsgSignData", + value: { + signer: accountOnChain.address, + data: toBase64( + new Uint8Array( + Buffer.from( + JSON.stringify({ + title: `Keplr Login to ${chain.chainDisplayName}`, + description: "Sign this no fee transaction to login with your Keplr wallet", + nonce, + }), + ), + ), + ), + }, + }, + ], + memo: "", + }); + + return signature; + }, + [chain.chainDisplayName, chain.chainId, chain.nodeAddress], + ); + + const fetchMultisigs = useCallback( + async (address: string) => { + try { + const signature = await getSignature(address); + + const newMultisigs: FetchedMultisigs = await requestJson( + `/api/chain/${chain.chainId}/multisig/list`, + { + body: { signature, chain }, + }, + ); + + setMultisigs(newMultisigs); + } catch (e: unknown) { + console.error("Failed to fetch multisigs:", e); + toastError({ + description: "Failed to fetch multisigs", + fullError: e instanceof Error ? e : undefined, + }); + } + }, + [chain, getSignature], + ); + + const connectWallet = useCallback(async () => { + try { + setLoading(true); + + await window.keplr.enable(chain.chainId); + window.keplr.defaultOptions = { + sign: { preferNoSetFee: true, preferNoSetMemo: true, disableBalanceCheck: true }, + }; + + const { bech32Address: address, pubKey: pubKeyArray } = await window.keplr.getKey( + chain.chainId, + ); + + const pubKey = toBase64(pubKeyArray); + + setWalletInfo({ address, pubKey }); + + await fetchMultisigs(address); + } catch (e) { + const connectError = getConnectError(e); + console.error(connectError, e); + toastError({ + description: connectError, + fullError: e instanceof Error ? e : undefined, + }); + } finally { + setLoading(false); + } + }, [chain.chainId, fetchMultisigs]); + + useLayoutEffect(() => { + if (!walletInfo?.address) { + return; + } + + const accountChangeKey = "keplr_keystorechange"; + window.addEventListener(accountChangeKey, connectWallet); + + return () => { + window.removeEventListener(accountChangeKey, connectWallet); + }; + }, [connectWallet, walletInfo?.address]); + + return ( + + + Multisigs + + Your list of created multisigs on {chain.chainDisplayName}. Verify your identity with + Keplr by signing a message for free. + + + + {!walletInfo && !multisigs ? ( + + ) : null} + {walletInfo && !multisigs ? ( +
+ +

Loading multisigs

+
+ ) : null} + {!showBelonged && multisigs && !multisigs.created.length + ? "You have not created any multisig" + : null} + {showBelonged && multisigs && !multisigs.belonged.length + ? "You are not a member of any multisig" + : null} + {multisigs?.created.length || multisigs?.belonged.length ? ( + <> + {multisigs.created.length !== multisigs.belonged.length ? ( +
+ { + setShowBelonged(checked); + }} + /> + +
+ ) : null} +
+ {(showBelonged ? multisigs.belonged : multisigs.created).map((multisig) => { + const pubkey: MultisigThresholdPubkey = JSON.parse(multisig.pubkeyJSON); + + return ( + + + + + {pubkey.value.threshold} / {pubkey.value.pubkeys.length} + + + +
+

threshold: {pubkey.value.threshold}

+

members: {pubkey.value.pubkeys.length}

+
+
+
+
+

{multisig.address}

+
+ + + ); + })} +
+ + ) : null} +
+
+ ); +} diff --git a/pages/[chainName]/index.tsx b/pages/[chainName]/index.tsx index 60269e3..21ed8e3 100644 --- a/pages/[chainName]/index.tsx +++ b/pages/[chainName]/index.tsx @@ -1,3 +1,4 @@ +import ListUserMultisigs from "@/components/dataViews/ListUserMultisigs"; import FindMultisigForm from "@/components/forms/FindMultisigForm"; import Head from "@/components/head"; import { useChains } from "@/context/ChainsContext"; @@ -9,6 +10,7 @@ const FindMultisigPage = () => {
+
); };