From 678e803054dd4db015711dc7243ad712aea9db96 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Wed, 19 Jun 2024 12:38:32 +0200 Subject: [PATCH] Add ListMultisigTxs --- components/dataViews/ListMultisigTxs.tsx | 262 ++++++++++++++++++++ components/dataViews/MultisigView/index.tsx | 7 + 2 files changed, 269 insertions(+) create mode 100644 components/dataViews/ListMultisigTxs.tsx diff --git a/components/dataViews/ListMultisigTxs.tsx b/components/dataViews/ListMultisigTxs.tsx new file mode 100644 index 0000000..a482fd9 --- /dev/null +++ b/components/dataViews/ListMultisigTxs.tsx @@ -0,0 +1,262 @@ +import { useChains } from "@/context/ChainsContext"; +import { ellideMiddle } from "@/lib/displayHelpers"; +import { getConnectError } from "@/lib/errorHelpers"; +import { requestJson } from "@/lib/request"; +import { msgTypeCountsFromJson } from "@/lib/txMsgHelpers"; +import { cn, toastError } from "@/lib/utils"; +import { DbNonce, DbTransaction } from "@/types"; +import { WalletInfo } from "@/types/signing"; +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"; + +// Show pending default. Toggle con show already broadcasted too + +interface ListMultisigTxsProps { + readonly multisigAddress: string; + readonly multisigThreshold: number; +} + +export default function ListMultisigTxs({ + multisigAddress, + multisigThreshold, +}: ListMultisigTxsProps) { + const { chain } = useChains(); + + const [loading, setLoading] = useState(false); + const [walletInfo, setWalletInfo] = useState | null>(null); + const [showBroadcasted, setShowBroadcasted] = useState(false); + const [transactions, setTransactions] = useState(null); + + const pendingTxs = transactions?.filter(({ txHash }) => !txHash) ?? 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 fetchTransactions = useCallback( + async (address: string) => { + try { + const signature = await getSignature(address); + + const transactions: readonly DbTransaction[] = await requestJson(`/api/transaction/list`, { + body: { signature, chain, multisigAddress }, + }); + + setTransactions(transactions); + } catch (e: unknown) { + console.error("Failed to fetch transactions:", e); + toastError({ + description: "Failed to fetch transactions", + fullError: e instanceof Error ? e : undefined, + }); + } + }, + [chain, getSignature, multisigAddress], + ); + + 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 fetchTransactions(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, fetchTransactions]); + + useLayoutEffect(() => { + if (!walletInfo?.address) { + return; + } + + const accountChangeKey = "keplr_keystorechange"; + window.addEventListener(accountChangeKey, connectWallet); + + return () => { + window.removeEventListener(accountChangeKey, connectWallet); + }; + }, [connectWallet, walletInfo?.address]); + + return ( + + + Transactions + + The list of transactions created by this multisig. Verify your identity with Keplr by + signing a message for free. + + + + {!walletInfo && !transactions ? ( + + ) : null} + {walletInfo && !transactions ? ( +
+ +

Loading transactions

+
+ ) : null} + {!showBroadcasted && pendingTxs && !pendingTxs.length + ? "No pending transactions found" + : null} + {showBroadcasted && transactions && !transactions.length ? "No transactions found" : null} + {transactions?.length || pendingTxs?.length ? ( + <> + {transactions?.length !== pendingTxs?.length ? ( +
+ { + setShowBroadcasted(checked); + }} + /> + +
+ ) : null} +
+ {((showBroadcasted ? transactions : pendingTxs) ?? []).map((tx) => { + const msgTypeCounts = msgTypeCountsFromJson(tx.dataJSON); + const hasSigned = Boolean( + tx.signatures.find(({ address }) => address === walletInfo?.address), + ); + + return ( + + + + + {tx.signatures.length}/{multisigThreshold} + + + +
+

signatures: {tx.signatures.length}

+

threshold: {multisigThreshold}

+

{hasSigned ? "Signed by you" : "Not signed by you"}

+

+ {tx.txHash + ? `Broadcasted with hash: ${ellideMiddle(tx.txHash, 12)}` + : "Not broadcasted"} +

+
+
+
+
+
+ {msgTypeCounts.map(({ msgType, count }) => ( + + {msgType} + {count > 1 ? ` (${count})` : ""} + + ))} +
+
+

{tx.id}

+
+
+ + + ); + })} +
+ + ) : null} +
+
+ ); +} diff --git a/components/dataViews/MultisigView/index.tsx b/components/dataViews/MultisigView/index.tsx index 7b31297..2bb2695 100644 --- a/components/dataViews/MultisigView/index.tsx +++ b/components/dataViews/MultisigView/index.tsx @@ -19,6 +19,7 @@ import { toast } from "sonner"; import { useChains } from "../../../context/ChainsContext"; import { Button } from "../../ui/button"; import BalancesTable from "../BalancesTable"; +import ListMultisigTxs from "../ListMultisigTxs"; export default function MultisigView() { const router = useRouter(); @@ -212,6 +213,12 @@ export default function MultisigView() { ) : null} + {hostedMultisig?.hosted === "db+chain" && multisigAddress ? ( + + ) : null} ); }