diff --git a/components/BadgeWithCopy.tsx b/components/BadgeWithCopy.tsx index bf3f6f3..2dff28f 100644 --- a/components/BadgeWithCopy.tsx +++ b/components/BadgeWithCopy.tsx @@ -1,7 +1,7 @@ import copy from "copy-to-clipboard"; import { Copy } from "lucide-react"; +import { toast } from "sonner"; import { Badge } from "./ui/badge"; -import { useToast } from "./ui/use-toast"; interface BadgeWithCopyProps { readonly name: string; @@ -9,13 +9,11 @@ interface BadgeWithCopyProps { } export default function BadgeWithCopy({ name, toCopy }: BadgeWithCopyProps) { - const { toast } = useToast(); - return ( { + onClick={async () => { copy(toCopy); - toast({ description: `Copied ${name} to clipboard` }); + toast(`Copied ${name} to clipboard`, { description: toCopy }); }} className="max-w-md self-start truncate hover:cursor-pointer" > diff --git a/components/Header.tsx b/components/Header.tsx index 47e08f7..35840aa 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -9,7 +9,7 @@ export default function Header() { const { chain } = useChains(); return ( -
+
>; } -export default function BalancesList({ walletAddress, setError }: BalancesListProps) { +export default function BalancesList({ walletAddress }: BalancesListProps) { const { chain } = useChains(); const [balances, setBalances] = useState([]); @@ -24,12 +24,15 @@ export default function BalancesList({ walletAddress, setError }: BalancesListPr const client = await StargateClient.connect(chain.nodeAddress); const newBalances = await client.getAllBalances(walletAddress); setBalances(newBalances); - } catch (e: unknown) { - setError(e instanceof Error ? e.message : "Failed to get balances"); - console.error("Get balances error:", e); + } catch (e) { + console.error("Failed to get balances:", e); + toastError({ + description: "Failed to get balances", + fullError: e instanceof Error ? e : undefined, + }); } })(); - }, [chain.nodeAddress, setError, walletAddress]); + }, [chain.nodeAddress, walletAddress]); return balances.length ? ( diff --git a/components/dataViews/AccountView/ButtonConnectWallet.tsx b/components/dataViews/AccountView/ButtonConnectWallet.tsx index 02e23fa..54bb960 100644 --- a/components/dataViews/AccountView/ButtonConnectWallet.tsx +++ b/components/dataViews/AccountView/ButtonConnectWallet.tsx @@ -1,3 +1,4 @@ +import { toastError } from "@/lib/utils"; import { LoadingStates, WalletInfo, WalletType } from "@/types/signing"; import { makeCosmoshubPath } from "@cosmjs/amino"; import { toBase64 } from "@cosmjs/encoding"; @@ -16,20 +17,17 @@ interface ButtonConnectWalletProps { WalletInfo | null | undefined, Dispatch>, ]; - readonly setError: Dispatch>; } export default function ButtonConnectWallet({ walletType, walletInfoState: [walletInfo, setWalletInfo], - setError, }: ButtonConnectWalletProps) { const { chain } = useChains(); const [loading, setLoading] = useState({}); const connectKeplr = useCallback(async () => { try { - setError(""); setLoading((oldLoading) => ({ ...oldLoading, keplr: true })); await window.keplr.enable(chain.chainId); @@ -44,12 +42,16 @@ export default function ButtonConnectWallet({ setWalletInfo({ type: "Keplr", address, pubKey }); } catch (e) { - console.error(e); - setError(getConnectError(e)); + const connectError = getConnectError(e); + console.error(connectError, e); + toastError({ + description: connectError, + fullError: e instanceof Error ? e : undefined, + }); } finally { setLoading((newLoading) => ({ ...newLoading, keplr: false })); } - }, [chain.chainId, setError, setWalletInfo]); + }, [chain.chainId, setWalletInfo]); useLayoutEffect(() => { if (!walletInfo?.address) { @@ -67,7 +69,6 @@ export default function ButtonConnectWallet({ const connectLedger = async () => { try { - setError(""); setLoading((newLoading) => ({ ...newLoading, ledger: true })); const ledgerTransport = await TransportWebUSB.create(120000, 120000); @@ -81,8 +82,12 @@ export default function ButtonConnectWallet({ setWalletInfo({ type: "Ledger", address, pubKey }); } catch (e) { - console.error(e); - setError(getConnectError(e)); + const connectError = getConnectError(e); + console.error(connectError, e); + toastError({ + description: connectError, + fullError: e instanceof Error ? e : undefined, + }); } finally { setLoading((newLoading) => ({ ...newLoading, ledger: false })); } diff --git a/components/dataViews/AccountView/index.tsx b/components/dataViews/AccountView/index.tsx index bd7b40f..3217ac0 100644 --- a/components/dataViews/AccountView/index.tsx +++ b/components/dataViews/AccountView/index.tsx @@ -1,9 +1,8 @@ import BadgeWithCopy from "@/components/BadgeWithCopy"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { explorerLinkAccount } from "@/lib/displayHelpers"; import { WalletInfo } from "@/types/signing"; -import { AlertCircle, Unplug } from "lucide-react"; +import { Unplug } from "lucide-react"; import Image from "next/image"; import { useState } from "react"; import { useChains } from "../../../context/ChainsContext"; @@ -16,7 +15,6 @@ export default function AccountView() { const walletInfoState = useState(); const [walletInfo, setWalletInfo] = walletInfoState; - const [error, setError] = useState(""); const explorerLink = explorerLinkAccount(chain.explorerLinks.account, walletInfo?.address || "") || ""; @@ -43,7 +41,7 @@ export default function AccountView() {
{explorerLink ? ( - ) : (
- - + +
)} {walletInfo?.address ? ( - + ) : null}
); diff --git a/components/forms/CreateTxForm/index.tsx b/components/forms/CreateTxForm/index.tsx index 388c30b..f5aca87 100644 --- a/components/forms/CreateTxForm/index.tsx +++ b/components/forms/CreateTxForm/index.tsx @@ -1,9 +1,11 @@ import { loadValidators } from "@/context/ChainsContext/helpers"; +import { toastError, toastSuccess } from "@/lib/utils"; import { EncodeObject } from "@cosmjs/proto-signing"; import { Account, calculateFee } from "@cosmjs/stargate"; import { assert } from "@cosmjs/utils"; import { NextRouter, withRouter } from "next/router"; import { useRef, useState } from "react"; +import { toast } from "sonner"; import { useChains } from "../../../context/ChainsContext"; import { requestJson } from "../../../lib/request"; import { exportMsgToJson, gasOfTx } from "../../../lib/txMsgHelpers"; @@ -39,7 +41,6 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro const [memo, setMemo] = useState(""); const [gasLimit, setGasLimit] = useState(gasOfTx([])); const [gasLimitError, setGasLimitError] = useState(""); - const [showCreateTxError, setShowTxError] = useState(false); const addMsgType = (newMsgType: MsgTypeUrl) => { setMsgKeys((oldMsgKeys) => [...oldMsgKeys, crypto.randomUUID()]); @@ -59,9 +60,9 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro }; const createTx = async () => { - try { - setShowTxError(false); + const loadingToastId = toast.loading("Creating transaction"); + try { assert(typeof accountOnChain.accountNumber === "number", "accountNumber missing"); assert(msgGetters.current.length, "form filled incorrectly"); @@ -90,13 +91,17 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro const { transactionID } = await requestJson("/api/transaction", { body: { dataJSON: JSON.stringify(tx) }, }); - + toastSuccess("Transaction created with ID", transactionID); router.push(`/${chain.registryName}/${senderAddress}/transaction/${transactionID}`); - } catch (error) { - console.error("Creat transaction error:", error); - setShowTxError(true); + } catch (e) { + console.error("Failed to create transaction:", e); + toastError({ + description: "Failed to create transaction", + fullError: e instanceof Error ? e : undefined, + }); } finally { setProcessing(false); + toast.dismiss(loadingToastId); } }; @@ -252,13 +257,6 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro - {showCreateTxError ? ( - -

- Error when creating the transaction. See console for more details. -

-
- ) : null} + + + + + +
); }; diff --git a/components/forms/MultisigForm.tsx b/components/forms/MultisigForm.tsx index 9f1babb..7f009f2 100644 --- a/components/forms/MultisigForm.tsx +++ b/components/forms/MultisigForm.tsx @@ -1,3 +1,4 @@ +import { toastError } from "@/lib/utils"; import { StargateClient } from "@cosmjs/stargate"; import { NextRouter, withRouter } from "next/router"; import { useState } from "react"; @@ -67,20 +68,20 @@ const MultiSigForm = (props: Props) => { return accountOnChain.pubkey.value; }; - const handleKeyBlur = async (index: number, e: React.ChangeEvent) => { + const handleKeyBlur = async (index: number, { target }: React.ChangeEvent) => { try { const tempPubkeys = [...pubkeys]; let pubkey; // use pubkey console.log(tempPubkeys[index]); if (tempPubkeys[index].isPubkey) { - pubkey = e.target.value; + pubkey = target.value; if (pubkey.length !== 44) { throw new Error("Invalid Secp256k1 pubkey"); } } else { // use address to fetch pubkey - const address = e.target.value; + const address = target.value; if (address.length > 0) { pubkey = await getPubkeyFromNode(address); } @@ -89,11 +90,10 @@ const MultiSigForm = (props: Props) => { tempPubkeys[index].compressedPubkey = pubkey; tempPubkeys[index].keyError = ""; setPubkeys(tempPubkeys); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - console.log(error); + } catch (e) { + console.error("Invalid address or pubkey", e); const tempPubkeys = [...pubkeys]; - tempPubkeys[index].keyError = error.message; + tempPubkeys[index].keyError = e instanceof Error ? e.message : "Invalid address or pubkey"; setPubkeys(tempPubkeys); } }; @@ -110,8 +110,12 @@ const MultiSigForm = (props: Props) => { chain.chainId, ); props.router.push(`/${chain.registryName}/${multisigAddress}`); - } catch (error) { - console.log("Failed to creat multisig: ", error); + } catch (e) { + console.error("Failed to create multisig:", e); + toastError({ + description: "Failed to create multisig", + fullError: e instanceof Error ? e : undefined, + }); } }; diff --git a/components/forms/TransactionSigning.tsx b/components/forms/TransactionSigning.tsx index f28bd4e..bdc4c3d 100644 --- a/components/forms/TransactionSigning.tsx +++ b/components/forms/TransactionSigning.tsx @@ -1,3 +1,4 @@ +import { toastError, toastSuccess } from "@/lib/utils"; import { LoadingStates, SigningStatus } from "@/types/signing"; import { MultisigThresholdPubkey, makeCosmoshubPath } from "@cosmjs/amino"; import { createWasmAminoConverters, wasmTypes } from "@cosmjs/cosmwasm-stargate"; @@ -13,6 +14,7 @@ import { import { assert } from "@cosmjs/utils"; import TransportWebUSB from "@ledgerhq/hw-transport-webusb"; import { useCallback, useLayoutEffect, useState } from "react"; +import { toast } from "sonner"; import { useChains } from "../../context/ChainsContext"; import { getConnectError } from "../../lib/errorHelpers"; import { requestJson } from "../../lib/request"; @@ -34,8 +36,6 @@ const TransactionSigning = (props: TransactionSigningProps) => { const { chain } = useChains(); const [walletAccount, setWalletAccount] = useState(); - const [sigError, setSigError] = useState(""); - const [connectError, setConnectError] = useState(""); const [signing, setSigning] = useState("not_signed"); const [walletType, setWalletType] = useState<"Keplr" | "Ledger">(); const [ledgerSigner, setLedgerSigner] = useState({}); @@ -68,10 +68,13 @@ const TransactionSigning = (props: TransactionSigningProps) => { } setWalletType("Keplr"); - setConnectError(""); } catch (e) { - console.error(e); - setConnectError(getConnectError(e)); + const connectError = getConnectError(e); + console.error(connectError, e); + toastError({ + description: connectError, + fullError: e instanceof Error ? e : undefined, + }); } finally { setLoading((newLoading) => ({ ...newLoading, keplr: false })); } @@ -124,16 +127,21 @@ const TransactionSigning = (props: TransactionSigningProps) => { setLedgerSigner(offlineSigner); setWalletType("Ledger"); - setConnectError(""); } catch (e) { - console.error(e); - setConnectError(getConnectError(e)); + const connectError = getConnectError(e); + console.error(connectError, e); + toastError({ + description: connectError, + fullError: e instanceof Error ? e : undefined, + }); } finally { setLoading((newLoading) => ({ ...newLoading, ledger: false })); } }; const signTransaction = async () => { + const loadingToastId = toast.loading("Signing transaction"); + try { setLoading((newLoading) => ({ ...newLoading, signing: true })); @@ -172,21 +180,27 @@ const TransactionSigning = (props: TransactionSigningProps) => { ); if (prevSigMatch > -1) { - setSigError("This account has already signed."); - } else { - const signature = { - bodyBytes: bases64EncodedBodyBytes, - signature: bases64EncodedSignature, - address: signerAddress, - }; - await requestJson(`/api/transaction/${props.transactionID}/signature`, { body: signature }); - props.addSignature(signature); - setSigning("signed"); + throw new Error("This account has already signed"); } + + const signature = { + bodyBytes: bases64EncodedBodyBytes, + signature: bases64EncodedSignature, + address: signerAddress, + }; + await requestJson(`/api/transaction/${props.transactionID}/signature`, { body: signature }); + toastSuccess("Transaction signed by", signerAddress); + props.addSignature(signature); + setSigning("signed"); } catch (e) { - console.log("signing err: ", e); + console.error("Failed to sign the tx:", e); + toastError({ + description: "Failed to sign the tx", + fullError: e instanceof Error ? e : undefined, + }); } finally { setLoading((newLoading) => ({ ...newLoading, signing: false })); + toast.dismiss(loadingToastId); } }; @@ -242,20 +256,6 @@ const TransactionSigning = (props: TransactionSigningProps) => { )} ) : null} - {sigError ? ( - -
-

This account has already signed this transaction

-
-
- ) : null} - {connectError ? ( - -
-

{connectError}

-
-
- ) : null} ); }; diff --git a/pages/[chainName]/index.tsx b/pages/[chainName]/index.tsx index 4edf110..843bdb2 100644 --- a/pages/[chainName]/index.tsx +++ b/pages/[chainName]/index.tsx @@ -1,22 +1,15 @@ import FindMultisigForm from "@/components/forms/FindMultisigForm"; -import Page from "@/components/layout/Page"; -import StackableContainer from "@/components/layout/StackableContainer"; +import Head from "@/components/head"; import { useChains } from "@/context/ChainsContext"; const MultiPage = () => { const { chain } = useChains(); return ( - - - -

- {chain.chainDisplayName} Multisig Manager -

-
- -
-
+
+ + +
); }; diff --git a/pages/_app.tsx b/pages/_app.tsx index f0d96ac..8627ad5 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,19 +1,38 @@ import Header from "@/components/Header"; -import { Toaster } from "@/components/ui/toaster"; +import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import ThemeProvider from "@/context/ThemesContext"; -import "@/styles/globals.css"; import type { AppProps } from "next/app"; import { ChainsProvider } from "../context/ChainsContext"; +import "@/styles/globals.css"; export default function MultisigApp({ Component, pageProps }: AppProps) { return ( -
- - +
+
+ +
+ diff --git a/styles/globals.css b/styles/globals.css index 72e69fc..28687eb 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -4,66 +4,27 @@ @layer base { :root { - --background: 0 0% 100%; - --foreground: 240 10% 3.9%; - - --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; - - --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --ring: 240 10% 3.9%; - + --background: 296.76 90.24% 16.08%; + --foreground: 0 0% 100%; + --muted: 299.15 27.84% 50%; + --muted-foreground: 0 0% 83.14%; + --popover: 298.26 40.83% 33.14%; + --popover-foreground: 0 0% 100%; + --card: 298.26 40.83% 33.14%; + --card-foreground: 0 0% 100%; + --border: 297 0% 100%; + --input: 299.15 0% 100%; + --primary: 297 90% 16%; + --primary-foreground: 300 100% 85.69%; + --secondary: 300 100% 85.69%; + --secondary-foreground: 296.76 90.24% 16.08%; + --accent: 299.15 27.84% 50%; + --accent-foreground: 327 0% 100%; + --destructive: 0 84.24% 60.2%; + --destructive-foreground: 0 0% 100%; + --ring: 357.74 0% 100%; --radius: 0.5rem; } - - .dark { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; - - --destructive: 0 72% 51%; - --destructive-foreground: 0 0% 98%; - - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 240 4.9% 83.9%; - } } @layer base {