Merge pull request #201 from cosmos/feat/add-generated-theme

Add new theme, tweak home and errors
This commit is contained in:
Abel Fernández
2024-03-14 11:54:52 +01:00
committed by GitHub
24 changed files with 628 additions and 790 deletions
+3 -5
View File
@@ -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 (
<Badge
onClick={() => {
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"
>
+1 -1
View File
@@ -9,7 +9,7 @@ export default function Header() {
const { chain } = useChains();
return (
<header className="flex flex-row items-center justify-between gap-4 bg-fuchsia-900 px-3">
<header className="flex w-full flex-row items-center justify-between gap-4 bg-fuchsia-900 px-3">
<ChainConnect />
<Link
href={
@@ -1,16 +1,16 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toastError } from "@/lib/utils";
import { Coin } from "@cosmjs/amino";
import { StargateClient } from "@cosmjs/stargate";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useChains } from "../../../context/ChainsContext";
import BalancePill from "./BalancePill";
interface BalancesListProps {
readonly walletAddress: string;
readonly setError: Dispatch<SetStateAction<string>>;
}
export default function BalancesList({ walletAddress, setError }: BalancesListProps) {
export default function BalancesList({ walletAddress }: BalancesListProps) {
const { chain } = useChains();
const [balances, setBalances] = useState<readonly Coin[]>([]);
@@ -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 ? (
<Card className="bg-fuchsia-850 w-full max-w-md border-transparent">
@@ -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<SetStateAction<WalletInfo | null | undefined>>,
];
readonly setError: Dispatch<SetStateAction<string>>;
}
export default function ButtonConnectWallet({
walletType,
walletInfoState: [walletInfo, setWalletInfo],
setError,
}: ButtonConnectWalletProps) {
const { chain } = useChains();
const [loading, setLoading] = useState<LoadingStates>({});
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 }));
}
+6 -26
View File
@@ -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<WalletInfo | null>();
const [walletInfo, setWalletInfo] = walletInfoState;
const [error, setError] = useState("");
const explorerLink =
explorerLinkAccount(chain.explorerLinks.account, walletInfo?.address || "") || "";
@@ -43,7 +41,7 @@ export default function AccountView() {
<div className="flex flex-col gap-4">
<BadgeWithCopy name="address" toCopy={walletInfo.address} />
{explorerLink ? (
<Button asChild className="self-center">
<Button asChild variant="secondary" className="self-center">
<a href={explorerLink} target="_blank">
View in explorer
</a>
@@ -57,15 +55,9 @@ export default function AccountView() {
</CardContent>
) : null}
<CardFooter className="my-8 flex w-full flex-col gap-4 p-0">
{error ? (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : null}
{walletInfo?.type ? (
<Button
variant="secondary"
onClick={() => {
setWalletInfo(null);
}}
@@ -75,26 +67,14 @@ export default function AccountView() {
</Button>
) : (
<div className="flex w-full flex-col gap-4">
<ButtonConnectWallet
walletType="Keplr"
walletInfoState={walletInfoState}
setError={setError}
/>
<ButtonConnectWallet
walletType="Ledger"
walletInfoState={walletInfoState}
setError={setError}
/>
<ButtonConnectWallet walletType="Keplr" walletInfoState={walletInfoState} />
<ButtonConnectWallet walletType="Ledger" walletInfoState={walletInfoState} />
</div>
)}
</CardFooter>
</Card>
{walletInfo?.address ? (
<BalancesList
key={walletInfo.address}
walletAddress={walletInfo.address}
setError={setError}
/>
<BalancesList key={walletInfo.address} walletAddress={walletInfo.address} />
) : null}
</div>
);
+12 -14
View File
@@ -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
</ul>
</div>
</div>
{showCreateTxError ? (
<StackableContainer lessMargin lessPadding>
<p className="multisig-error">
Error when creating the transaction. See console for more details.
</p>
</StackableContainer>
) : null}
<Button
label="Create Transaction"
onClick={createTx}
+82 -78
View File
@@ -1,96 +1,100 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ChainInfo } from "@/context/ChainsContext/types";
import { StargateClient } from "@cosmjs/stargate";
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { NextRouter, withRouter } from "next/router";
import { useEffect, useState } from "react";
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";
import Button from "../inputs/Button";
import Input from "../inputs/Input";
import StackableContainer from "../layout/StackableContainer";
interface Props {
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;
} catch {
return false;
}
};
interface FindMultisigFormProps {
router: NextRouter;
}
const FindMultisigForm = (props: Props) => {
const FindMultisigForm = ({ router }: FindMultisigFormProps) => {
const { chain } = useChains();
const [address, setAddress] = useState("");
const [multisigError, setMultisigError] = useState("");
const handleSearch = () => {
props.router.push(`/${chain.registryName}/${address}`);
};
const findMultisigSchema = z.object({
address: z
.string()
.trim()
.min(1, "Required")
.startsWith(chain.addressPrefix, `Invalid prefix for ${chain.chainDisplayName}`)
.refine(async (address) => await existsMultisigAccount(chain, address), {
message: "Multisig not found",
}),
});
useEffect(() => {
(async function () {
if (!address) {
setMultisigError("");
return;
}
const findMultisigForm = useForm<z.infer<typeof findMultisigSchema>>({
resolver: zodResolver(findMultisigSchema),
defaultValues: { address: "" },
});
try {
const client = await StargateClient.connect(chain.nodeAddress);
await getMultisigAccount(address, chain.addressPrefix, client);
setMultisigError("");
} catch (error) {
if (error instanceof Error) {
setMultisigError(error.message);
} else {
setMultisigError("Multisig error");
}
console.error("Multisig error:", error);
}
})();
}, [address, chain.addressPrefix, chain.nodeAddress]);
const submitFindMultisig = ({ address }: z.infer<typeof findMultisigSchema>) =>
router.push(`/${chain.registryName}/${address}`);
return (
<StackableContainer>
<StackableContainer lessPadding>
<p>
Already have a multisig address? Enter it below. If its a valid address, you will be able
to view its transactions and create new ones.
</p>
</StackableContainer>
<StackableContainer lessPadding lessMargin>
<Input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setAddress(e.target.value)}
value={address}
label="Multisig Address"
name="address"
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
error={multisigError}
/>
<Button
label="Use this Multisig"
onClick={handleSearch}
primary
disabled={!address || !!multisigError}
/>
</StackableContainer>
<StackableContainer lessPadding>
<p className="create-help">Don't have a multisig?</p>
<Button
label="Create New Multisig"
onClick={() => props.router.push(`${chain.registryName}/create`)}
/>
</StackableContainer>
<style jsx>{`
.multisig-form {
display: flex;
flex-direction: column;
align-items: center;
}
.error {
color: coral;
font-size: 0.8em;
text-align: left;
margin: 0.5em 0;
}
.create-help {
text-align: center;
}
`}</style>
</StackableContainer>
<Card>
<CardHeader>
<CardTitle>Already have a {chain.chainDisplayName || "Cosmos Hub"} multisig?</CardTitle>
<CardDescription>
Enter its address below to view its transactions and create new ones.
</CardDescription>
</CardHeader>
<CardContent>
<Form {...findMultisigForm}>
<form onSubmit={findMultisigForm.handleSubmit(submitFindMultisig)} className="space-y-8">
<FormField
control={findMultisigForm.control}
name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Multisig address</FormLabel>
<FormControl>
<Input
placeholder={`E.g. "${exampleAddress(0, chain.addressPrefix)}"`}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-wrap items-center gap-4">
<Button type="submit">Use this multisig</Button>
<Button asChild variant="link" className="p-0 text-secondary">
<Link href={chain.registryName ? `/${chain.registryName}/create` : ""} className="">
I don't have a multisig
</Link>
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
);
};
+13 -9
View File
@@ -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<HTMLInputElement>) => {
const handleKeyBlur = async (index: number, { target }: React.ChangeEvent<HTMLInputElement>) => {
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,
});
}
};
+33 -42
View File
@@ -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<WalletAccount>();
const [sigError, setSigError] = useState("");
const [connectError, setConnectError] = useState("");
const [signing, setSigning] = useState<SigningStatus>("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 ? (
<StackableContainer lessPadding lessRadius lessMargin>
<div className="signature-error">
<p>This account has already signed this transaction</p>
</div>
</StackableContainer>
) : null}
{connectError ? (
<StackableContainer lessPadding lessRadius lessMargin>
<div className="signature-error">
<p>{connectError}</p>
</div>
</StackableContainer>
) : null}
</StackableContainer>
<style jsx>{`
p {
@@ -273,15 +273,6 @@ const TransactionSigning = (props: TransactionSigningProps) => {
padding: 0;
margin: 0;
}
.signature-error p {
max-width: 550px;
color: red;
font-size: 16px;
line-height: 1.4;
}
.signature-error p:first-child {
margin-top: 0;
}
.confirmation {
display: flex;
justify-content: center;
+1 -1
View File
@@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
+29
View File
@@ -0,0 +1,29 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }
+2 -2
View File
@@ -9,7 +9,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-muted data-[state=unchecked]:bg-input",
className
)}
{...props}
@@ -17,7 +17,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
"pointer-events-none block h-5 w-5 rounded-full bg-primary shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
-127
View File
@@ -1,127 +0,0 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
-38
View File
@@ -1,38 +0,0 @@
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider >
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props} className="bg-fuchsia-900" style={
{
"--accent": "0, 100%, 100%",
"--border": "0, 100%, 100%",
} as React.CSSProperties
}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}
-192
View File
@@ -1,192 +0,0 @@
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_VALUE
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }
+6 -1
View File
@@ -1,4 +1,5 @@
import { getAllValidators } from "@/lib/staking";
import { toastError } from "@/lib/utils";
import { ReactNode, createContext, useContext, useEffect, useReducer } from "react";
import { emptyChain, isChainInfoFilled, setChain, setChains, setChainsError } from "./helpers";
import { getChain, getNodeFromArray, useChainsFromRegistry } from "./service";
@@ -99,7 +100,11 @@ export const ChainsProvider = ({ children }: ChainsProviderProps) => {
const validators = await getAllValidators(state.chain.nodeAddress);
dispatch({ type: "setValidatorState", payload: { validators, status: "done" } });
} catch (e) {
console.error(e instanceof Error ? e.message : "Failed to load validators");
console.error("Failed to load validators:", e);
toastError({
description: "Failed to load validators",
fullError: e instanceof Error ? e : undefined,
});
dispatch({ type: "setValidatorState", payload: { validators: [], status: "error" } });
}
}
+7 -6
View File
@@ -1,4 +1,5 @@
import { getChainsFromRegistry, getShaFromRegistry } from "@/lib/chainRegistry";
import { toastError } from "@/lib/utils";
import { StargateClient } from "@cosmjs/stargate";
import { useEffect, useState } from "react";
import { emptyChain, isChainInfoFilled } from "./helpers";
@@ -56,12 +57,12 @@ export const useChainsFromRegistry = () => {
return;
}
if (e instanceof Error) {
console.error(e.message);
setChainItemsError(e.message);
} else {
setChainItemsError("Failed to get chains from registry");
}
console.error("Failed to get chains from registry:", e);
setChainItemsError(e instanceof Error ? e.message : "Failed to get chains from registry");
toastError({
description: "Failed to get chains from registry",
fullError: e instanceof Error ? e : undefined,
});
}
})();
}, [chainItems.mainnets.size, chainItems.testnets.size]);
+40 -1
View File
@@ -1,6 +1,45 @@
import { type ClassValue, clsx } from "clsx";
import { clsx, type ClassValue } from "clsx";
import copy from "copy-to-clipboard";
import { toast } from "sonner";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
interface ToastErrorProps {
readonly title?: string;
readonly description?: string;
readonly fullError?: Error;
}
export function toastError(
{ title, description, fullError }: ToastErrorProps = { title: "An error ocurred" },
) {
toast.error(title, {
description,
action:
fullError && fullError.message
? { label: "Copy full error", onClick: () => copy(fullError.message) }
: undefined,
classNames: {
closeButton:
"group-[.toaster]:!bg-destructive group-[.toaster]:!text-destructive-foreground [&>svg]:!stroke-[4px]",
},
actionButtonStyle: {
backgroundColor: "hsl(var(--destructive-foreground))",
color: "hsl(var(--destructive))",
fontWeight: "bold",
},
});
}
export function toastSuccess(title: string, description?: string) {
toast.success(title, {
description,
classNames: {
closeButton:
"group-[.toaster]:!bg-green-500 group-[.toaster]:!text-white [&>svg]:!stroke-[4px]",
},
});
}
+300 -117
View File
@@ -44,7 +44,6 @@
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@testing-library/jest-dom": "^6.1.3",
@@ -78,11 +77,12 @@
"react-hook-form": "^7.47.0",
"react-select": "^5.7.4",
"recharts": "^2.8.0",
"sonner": "^1.4.3",
"tailwind-merge": "^1.14.0",
"tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.2.2",
"vanilla-jsoneditor": "^0.18.3",
"vanilla-jsoneditor": "^0.22.0",
"zod": "^3.22.2"
}
},
@@ -757,6 +757,91 @@
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="
},
"node_modules/@codemirror/autocomplete": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.13.0.tgz",
"integrity": "sha512-SuDrho1klTINfbcMPnyro1ZxU9xJtwDMtb62R8TjL/tOl71IoOsvBo1a9x+hDvHhIzkTcJHy2VC+rmpGgYkRSw==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
},
"peerDependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.3.3.tgz",
"integrity": "sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-json": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz",
"integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.5.0.tgz",
"integrity": "sha512-+5YyicIaaAZKU8K43IQi8TBy6mF6giGeWAH7N96Z5LC30Wm5JMjqxOYIE9mxwMG1NbhT2mA3l9hA4uuKUM3E5g==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.5.6",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz",
"integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
"integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A=="
},
"node_modules/@codemirror/view": {
"version": "6.25.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.25.1.tgz",
"integrity": "sha512-2LXLxsQnHDdfGzDvjzAwZh2ZviNJm7im6tGpa0IONIDnFd8RZ80D2SNi8PDi6YjKcMoMRK20v6OmKIdsrwsyoQ==",
"dependencies": {
"@codemirror/state": "^6.4.0",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@confio/ics23": {
"version": "0.6.8",
"resolved": "https://registry.npmjs.org/@confio/ics23/-/ics23-0.6.8.tgz",
@@ -1143,21 +1228,33 @@
"integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw=="
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
"integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==",
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
"integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz",
"integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==",
"node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz",
"integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.4.2"
"@fortawesome/fontawesome-common-types": "6.5.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
"integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.5.1"
},
"engines": {
"node": ">=6"
@@ -1657,6 +1754,37 @@
"resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.10.1.tgz",
"integrity": "sha512-z+ILK8Q3y+nfUl43ctCPuR4Y2bIxk/ooCQFwZxhtci1EhAtMDzMAx2W25qx8G1PPL9UUOdnUax19+F0OjXoj4w=="
},
"node_modules/@lezer/common": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
"integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ=="
},
"node_modules/@lezer/highlight": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz",
"integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/json": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz",
"integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz",
"integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@next/env": {
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.19.tgz",
@@ -3367,90 +3495,6 @@
}
}
},
"node_modules/@radix-ui/react-toast": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz",
"integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-collection": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1",
"@radix-ui/react-visually-hidden": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
"integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-escape-keydown": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
"integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz",
@@ -3746,6 +3790,16 @@
"@babel/runtime": "^7.13.10"
}
},
"node_modules/@replit/codemirror-indentation-markers": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@replit/codemirror-indentation-markers/-/codemirror-indentation-markers-6.5.0.tgz",
"integrity": "sha512-5RgeuQ6erfROi1EVI2X7G4UR+KByjb07jhYMynvpvlrV22JlnARifmKMGEUKy0pKcxBNfwbFqoUlTYHPgyZNlg==",
"peerDependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/@rushstack/eslint-patch": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz",
@@ -3772,6 +3826,11 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@sphinxxxx/color-conversion": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz",
"integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw=="
},
"node_modules/@swc/helpers": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz",
@@ -4009,9 +4068,9 @@
"integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g=="
},
"node_modules/@types/estree": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA=="
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
},
"node_modules/@types/graceful-fs": {
"version": "4.1.6",
@@ -5383,6 +5442,16 @@
"periscopic": "^3.1.0"
}
},
"node_modules/codemirror-wrapped-line-indent": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/codemirror-wrapped-line-indent/-/codemirror-wrapped-line-indent-1.0.5.tgz",
"integrity": "sha512-T6C18nEhWb+k3JD7BhNaYFp9dzpjXM4Oq3jymSYPugZOe5nN64DNWRilkr6iox2sqaQ3PH0D4RVg+Qcv6u1OBg==",
"peerDependencies": {
"@codemirror/language": "^6.9.0",
"@codemirror/state": "^6.2.1",
"@codemirror/view": "^6.17.1"
}
},
"node_modules/collect-v8-coverage": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
@@ -5474,6 +5543,11 @@
"resolved": "https://registry.npmjs.org/cosmjs-types/-/cosmjs-types-0.9.0.tgz",
"integrity": "sha512-MN/yUe6mkJwHnCFfsNPeCfXVhyxHYW6c/xDUzrSbBycYzw++XvWDMJArXp2pLdgD6FQ8DW79vkPjeNKVrXaHeQ=="
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -7192,10 +7266,15 @@
"node": ">= 4"
}
},
"node_modules/immutable": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
"integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw=="
},
"node_modules/immutable-json-patch": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/immutable-json-patch/-/immutable-json-patch-5.1.3.tgz",
"integrity": "sha512-95AsF9hJTPpwtBGAnHmw57PASL672tb+vGHR5xLhH2VPuHSsLho7grjlfgQ65DIhHP+UmLCjdmuuA6L1ndJbZg=="
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/immutable-json-patch/-/immutable-json-patch-6.0.1.tgz",
"integrity": "sha512-BHL/cXMjwFZlTOffiWNdY8ZTvNyYLrutCnWxrcKPHr5FqpAb6vsO6WWSPnVSys3+DruFN6lhHJJPHi8uELQL5g=="
},
"node_modules/import-fresh": {
"version": "3.3.0",
@@ -7524,9 +7603,9 @@
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="
},
"node_modules/is-reference": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.1.tgz",
"integrity": "sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
"dependencies": {
"@types/estree": "*"
}
@@ -8309,6 +8388,14 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/jmespath": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
"integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -8395,6 +8482,11 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/json-source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/json-source-map/-/json-source-map-0.6.1.tgz",
"integrity": "sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg=="
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -8411,6 +8503,14 @@
"json5": "lib/cli.js"
}
},
"node_modules/jsonrepair": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.6.0.tgz",
"integrity": "sha512-ZvOmoq35LhlDaf1W3uT7e17Bh2dYbln1+pdJ1KUIMkRAoUC4mvXX+dbr9Ih6dDmYvB0mdijAucyPk4xX1cEjww==",
"bin": {
"jsonrepair": "bin/cli.js"
}
},
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -8566,6 +8666,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -8615,9 +8720,9 @@
}
},
"node_modules/magic-string": {
"version": "0.30.3",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz",
"integrity": "sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==",
"version": "0.30.8",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
"integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
@@ -8783,6 +8888,11 @@
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
},
"node_modules/natural-compare-lite": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g=="
},
"node_modules/next": {
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/next/-/next-13.4.19.tgz",
@@ -10201,6 +10311,22 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sass": {
"version": "1.71.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
"integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@@ -10284,6 +10410,15 @@
"node": ">=8"
}
},
"node_modules/sonner": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.3.tgz",
"integrity": "sha512-SArYlHbkjqRuLiR0iGY2ZSr09oOrxw081ZZkQPfXrs8aZQLIBOLOdzTYxGJB5yIZ7qL56UEPmrX1YqbODwG0Lw==",
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -10499,6 +10634,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
},
"node_modules/styled-jsx": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
@@ -10589,28 +10729,37 @@
}
},
"node_modules/svelte": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.0.tgz",
"integrity": "sha512-kVsdPjDbLrv74SmLSUzAsBGquMs4MPgWGkGLpH+PjOYnFOziAvENVzgJmyOCV2gntxE32aNm8/sqNKD6LbIpeQ==",
"version": "4.2.12",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz",
"integrity": "sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==",
"dependencies": {
"@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@jridgewell/trace-mapping": "^0.3.18",
"@types/estree": "^1.0.1",
"acorn": "^8.9.0",
"aria-query": "^5.3.0",
"axobject-query": "^3.2.1",
"axobject-query": "^4.0.0",
"code-red": "^1.0.3",
"css-tree": "^2.3.1",
"estree-walker": "^3.0.3",
"is-reference": "^3.0.1",
"locate-character": "^3.0.0",
"magic-string": "^0.30.0",
"magic-string": "^0.30.4",
"periscopic": "^3.1.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/svelte/node_modules/axobject-query": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz",
"integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==",
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/symbol-observable": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz",
@@ -11057,14 +11206,35 @@
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/vanilla-jsoneditor": {
"version": "0.18.3",
"resolved": "https://registry.npmjs.org/vanilla-jsoneditor/-/vanilla-jsoneditor-0.18.3.tgz",
"integrity": "sha512-T3noS1HVVLEhuNgVmk9/FJFhxfv9TO736UBPuj9laMqq9ywinb09VDF5isQmY5hB7a6erg2GdaUrRUEcLmZrzw==",
"version": "0.22.0",
"resolved": "https://registry.npmjs.org/vanilla-jsoneditor/-/vanilla-jsoneditor-0.22.0.tgz",
"integrity": "sha512-r6AN3NAWyVFb9pH6iNIa2Q1YiSGai4PGhFN+YnR4OJzXE72hu/Xu/MUbVyGvJxchhPcWj6bIPB1Ox5KSOspmFA==",
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@codemirror/autocomplete": "^6.13.0",
"@codemirror/commands": "^6.3.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/language": "^6.10.1",
"@codemirror/lint": "^6.5.0",
"@codemirror/search": "^6.5.6",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.24.1",
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@lezer/highlight": "^1.2.0",
"@replit/codemirror-indentation-markers": "^6.5.0",
"ajv": "^8.12.0",
"immutable-json-patch": "^5.1.3",
"svelte": "^4.2.0"
"codemirror-wrapped-line-indent": "^1.0.5",
"diff-sequences": "^29.6.3",
"immutable-json-patch": "6.0.1",
"jmespath": "^0.16.0",
"json-source-map": "^0.6.1",
"jsonrepair": "^3.6.0",
"lodash-es": "^4.17.21",
"memoize-one": "^6.0.0",
"natural-compare-lite": "^1.4.0",
"sass": "^1.71.1",
"svelte": "^4.2.12",
"vanilla-picker": "^2.12.2"
}
},
"node_modules/vanilla-jsoneditor/node_modules/ajv": {
@@ -11087,6 +11257,14 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/vanilla-picker": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/vanilla-picker/-/vanilla-picker-2.12.2.tgz",
"integrity": "sha512-dk0gNeNL9fQFGd1VEhNDQfFlbCqAiksRh1H2tVPlavkH88n/a/y30rXi9PPKrYPTK5kEfPO4xcldt4ts/1wIAg==",
"dependencies": {
"@sphinxxxx/color-conversion": "^2.2.2"
}
},
"node_modules/victory-vendor": {
"version": "36.6.11",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.6.11.tgz",
@@ -11108,6 +11286,11 @@
"d3-timer": "^3.0.1"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
},
"node_modules/w3c-xmlserializer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
+2 -2
View File
@@ -49,7 +49,6 @@
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@testing-library/jest-dom": "^6.1.3",
@@ -83,11 +82,12 @@
"react-hook-form": "^7.47.0",
"react-select": "^5.7.4",
"recharts": "^2.8.0",
"sonner": "^1.4.3",
"tailwind-merge": "^1.14.0",
"tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.2.2",
"vanilla-jsoneditor": "^0.18.3",
"vanilla-jsoneditor": "^0.22.0",
"zod": "^3.22.2"
}
}
@@ -1,4 +1,5 @@
import { isChainInfoFilled } from "@/context/ChainsContext/helpers";
import { toastError, toastSuccess } from "@/lib/utils";
import { MultisigThresholdPubkey } from "@cosmjs/amino";
import { fromBase64 } from "@cosmjs/encoding";
import { Account, StargateClient, makeMultisignedTxBytes } from "@cosmjs/stargate";
@@ -6,6 +7,7 @@ import { assert } from "@cosmjs/utils";
import { GetServerSideProps } from "next";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import CompletedTransaction from "../../../../components/dataViews/CompletedTransaction";
import ThresholdInfo from "../../../../components/dataViews/ThresholdInfo";
import TransactionInfo from "../../../../components/dataViews/TransactionInfo";
@@ -69,12 +71,10 @@ const TransactionPage = ({
}) => {
const { chain } = useChains();
const [currentSignatures, setCurrentSignatures] = useState(signatures);
const [broadcastError, setBroadcastError] = useState("");
const [isBroadcasting, setIsBroadcasting] = useState(false);
const [transactionHash, setTransactionHash] = useState(txHash);
const [accountOnChain, setAccountOnChain] = useState<Account | null>(null);
const [pubkey, setPubkey] = useState<MultisigThresholdPubkey>();
const [hasAccountError, setHasAccountError] = useState(false);
const txInfo = dbTxFromJson(transactionJSON);
const router = useRouter();
const multisigAddress = router.query.address?.toString();
@@ -95,20 +95,21 @@ const TransactionPage = ({
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",
);
} catch (e) {
console.error("Failed to find multisig address:", e);
toastError({
description: "Failed to find multisig address",
fullError: e instanceof Error ? e : undefined,
});
}
})();
}, [chain, multisigAddress]);
const broadcastTx = async () => {
const loadingToastId = toast.loading("Broadcasting transaction");
try {
setIsBroadcasting(true);
setBroadcastError("");
assert(accountOnChain, "Account on chain value missing.");
assert(
@@ -132,11 +133,17 @@ const TransactionPage = ({
await requestJson(`/api/transaction/${transactionID}`, {
body: { txHash: result.transactionHash },
});
toastSuccess("Transaction broadcasted with hash", result.transactionHash);
setTransactionHash(result.transactionHash);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
} catch (e) {
console.error("Failed to broadcast tx:", e);
toastError({
description: "Failed to broadcast tx",
fullError: e instanceof Error ? e : undefined,
});
} finally {
setIsBroadcasting(false);
setBroadcastError(e.toString());
toast.dismiss(loadingToastId);
}
};
@@ -159,13 +166,6 @@ const TransactionPage = ({
<StackableContainer>
<h1>{transactionHash ? "Completed Transaction" : "In Progress Transaction"}</h1>
</StackableContainer>
{hasAccountError ? (
<StackableContainer>
<div className="multisig-error">
<p>Multisig address could not be found.</p>
</div>
</StackableContainer>
) : null}
{transactionHash ? <CompletedTransaction transactionHash={transactionHash} /> : null}
{!transactionHash ? (
<StackableContainer lessPadding lessMargin>
@@ -178,7 +178,6 @@ const TransactionPage = ({
primary
disabled={isBroadcasting}
/>
{broadcastError ? <div className="broadcast-error">{broadcastError}</div> : null}
</>
) : null}
{pubkey && txInfo ? (
@@ -194,23 +193,6 @@ const TransactionPage = ({
) : null}
{txInfo ? <TransactionInfo tx={txInfo} /> : null}
</StackableContainer>
<style jsx>{`
.broadcast-error {
background: firebrick;
margin: 20px auto;
padding: 15px;
border-radius: 10px;
text-align: center;
font-family: monospace;
max-width: 475px;
}
.multisig-error p {
max-width: 550px;
color: red;
font-size: 16px;
line-height: 1.4;
}
`}</style>
</Page>
);
};
+5 -12
View File
@@ -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 (
<Page>
<StackableContainer base>
<StackableContainer lessPadding>
<h1 className="title">
<span>{chain.chainDisplayName}</span> Multisig Manager
</h1>
</StackableContainer>
<FindMultisigForm />
</StackableContainer>
</Page>
<div className="m-4 mt-0 flex max-w-xl flex-1 flex-col justify-center gap-4">
<Head title={`${chain.chainDisplayName || "Cosmos Hub"} Multisig Manager`} />
<FindMultisigForm />
</div>
);
};
+24 -5
View File
@@ -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 (
<ChainsProvider>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
<TooltipProvider>
<Header />
<Component {...pageProps} />
<Toaster />
<div className="flex min-h-screen flex-col items-center gap-4">
<Header />
<Component {...pageProps} />
</div>
<Toaster
richColors
closeButton
duration={999999}
/* This need to be overriden or else it doesn't apply the custom styles. A bug from shadcn probably https://github.com/shadcn-ui/ui/issues/2254 */
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
error:
"group-[.toaster]:!bg-destructive group-[.toaster]:!text-destructive-foreground",
success: "group-[.toaster]:!bg-green-500 group-[.toaster]:!text-white",
description: "group-[.toast]:text-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
/>
</TooltipProvider>
</ThemeProvider>
</ChainsProvider>
+19 -58
View File
@@ -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 {