Merge pull request #181 from cosmos/feat/new-chainselect

Add new ChainConnect
This commit is contained in:
Simon Warta
2023-12-18 16:53:02 +01:00
committed by GitHub
61 changed files with 2537 additions and 1014 deletions
+1 -12
View File
@@ -1,15 +1,4 @@
FAUNADB_SECRET=
FAUNADB_URL=https://graphql.eu.fauna.com/graphql
NEXT_PUBLIC_NODE_ADDRESS=https://cosmoshub.validator.network:443
NEXT_PUBLIC_DENOM=uatom
NEXT_PUBLIC_DISPLAY_DENOM=ATOM
NEXT_PUBLIC_DISPLAY_DENOM_EXPONENT=6
NEXT_PUBLIC_ASSETS=[{"description":"The native staking and governance token of the Cosmos Hub.","denom_units":[{"denom":"uatom","exponent":0},{"denom":"atom","exponent":6}],"base":"uatom","name":"Cosmos Hub Atom","display":"atom","symbol":"ATOM","logo_URIs":{"png":"https://raw.githubusercontent.com/cosmos/chain-registry/master/cosmoshub/images/atom.png","svg":"https://raw.githubusercontent.com/cosmos/chain-registry/master/cosmoshub/images/atom.svg"},"coingecko_id":"cosmos"}]
NEXT_PUBLIC_GAS_PRICE=0.03uatom
NEXT_PUBLIC_CHAIN_ID=cosmoshub-4
NEXT_PUBLIC_ADDRESS_PREFIX=cosmos
NEXT_PUBLIC_REGISTRY_NAME=cosmoshub
NEXT_PUBLIC_EXPLORER_LINK_TX="https://www.mintscan.io/cosmos/txs/\${txHash}"
NEXT_PUBLIC_EXPLORER_LINK_ACCOUNT="https://www.mintscan.io/cosmos/account/\${address}"
NEXT_PUBLIC_CHAIN_DISPLAY_NAME="Cosmos Hub"
NEXT_PUBLIC_MULTICHAIN=true
NEXT_PUBLIC_REGISTRY_NAME=cosmoshub
+1 -12
View File
@@ -1,15 +1,4 @@
FAUNADB_SECRET=
FAUNADB_URL=https://graphql.eu.fauna.com/graphql
NEXT_PUBLIC_NODE_ADDRESS=https://rpc.uni.junonetwork.io:443
NEXT_PUBLIC_DENOM=ujunox
NEXT_PUBLIC_DISPLAY_DENOM=JUNOX
NEXT_PUBLIC_DISPLAY_DENOM_EXPONENT=6
NEXT_PUBLIC_ASSETS=[{"description":"The native token of JUNO Chain","denom_units":[{"denom":"ujunox","exponent":0},{"denom":"junox","exponent":6}],"base":"ujunox","name":"Juno Testnet","display":"junox","symbol":"JUNOX","logo_URIs":{"png":"https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/junotestnet/images/juno.png","svg":"https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/junotestnet/images/juno.svg"},"coingecko_id":"juno-network"}]
NEXT_PUBLIC_GAS_PRICE=0.04ujunox
NEXT_PUBLIC_CHAIN_ID=uni-6
NEXT_PUBLIC_ADDRESS_PREFIX=juno
NEXT_PUBLIC_REGISTRY_NAME=junotestnet
NEXT_PUBLIC_EXPLORER_LINK_TX="https://testnet.mintscan.io/juno-testnet/txs/\${txHash}"
NEXT_PUBLIC_EXPLORER_LINK_ACCOUNT="https://testnet.mintscan.io/juno-testnet/account/\${address}"
NEXT_PUBLIC_CHAIN_DISPLAY_NAME="Juno Testnet"
NEXT_PUBLIC_MULTICHAIN=true
NEXT_PUBLIC_REGISTRY_NAME=junotestnet
+2
View File
@@ -0,0 +1,2 @@
# Shadcn ui components
/components/ui/
+2
View File
@@ -0,0 +1,2 @@
# Shadcn ui components
/components/ui/
+26
View File
@@ -0,0 +1,26 @@
import copy from "copy-to-clipboard";
import { Copy } from "lucide-react";
import { Badge } from "./ui/badge";
import { useToast } from "./ui/use-toast";
interface BadgeWithCopyProps {
readonly name: string;
readonly toCopy: string;
}
export default function BadgeWithCopy({ name, toCopy }: BadgeWithCopyProps) {
const { toast } = useToast();
return (
<Badge
onClick={() => {
copy(toCopy);
toast({ description: `Copied ${name} to clipboard` });
}}
className="max-w-md self-start truncate hover:cursor-pointer"
>
<Copy className="mr-2 h-auto w-3" />
<span className="truncate">{toCopy}</span>
</Badge>
);
}
+104
View File
@@ -0,0 +1,104 @@
import { useChains } from "@/context/ChainsContext";
import { deleteLocalChainFromStorage } from "@/context/ChainsContext/storage";
import { ChainInfo } from "@/context/ChainsContext/types";
import { CheckCircle, ChevronsUpDown, ExternalLink } from "lucide-react";
import Link from "next/link";
import ButtonWithConfirm from "../inputs/ButtonWithConfirm";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible";
interface ChainItemProps {
readonly chain: ChainInfo;
readonly simplify?: boolean;
}
export default function ChainDigest({ chain, simplify }: ChainItemProps) {
const { chain: connectedChain, chains } = useChains();
return (
<div className="space-y-1">
<div className="column flex flex-wrap items-center gap-2">
<Avatar className="overflow-visible">
{!simplify && connectedChain.registryName === chain.registryName ? (
<CheckCircle
style={{
position: "absolute",
top: "-6px",
left: "-6px",
border: "2px solid rgb(134 239 172 / var(--tw-bg-opacity))",
borderRadius: "50%",
width: "20px",
height: "20px",
background: "rgb(134 239 172 / var(--tw-bg-opacity))",
color: "rgb(20 83 45 / var(--tw-text-opacity))",
}}
/>
) : null}
<AvatarImage src={chain.logo} alt={`${chain.chainDisplayName} logo`} className="h-auto" />
<AvatarFallback>{chain.registryName.slice(0, 1).toUpperCase()}</AvatarFallback>
</Avatar>
<h4 className="text-sm font-semibold">{chain.chainDisplayName}</h4>
<Badge>{chain.chainId}</Badge>
</div>
<h4 className="text-sm font-semibold">Fee token: {chain.displayDenom}</h4>
<Collapsible className="w-[350px] space-y-2">
<div className="flex items-center space-x-4">
{!simplify && chain.nodeAddresses.length > 1 ? (
<CollapsibleTrigger asChild>
<Button size="sm" className="p-1">
<ChevronsUpDown className="black h-4 w-4" />
<span className="sr-only">Toggle</span>
<h4 className="text-sm font-semibold">RPC endpoints:</h4>
</Button>
</CollapsibleTrigger>
) : (
<h4 className="text-sm font-semibold">RPC endpoint:</h4>
)}
</div>
<div className="rounded-md border px-2 py-1 font-mono text-sm">
{chain.nodeAddress || chain.nodeAddresses[0]}
</div>
<CollapsibleContent className="space-y-2">
{chain.nodeAddresses
.filter(
(address, _, nodeAddresses) =>
(chain.nodeAddress && address !== chain.nodeAddress) ||
(!chain.nodeAddress && address !== nodeAddresses[0]),
)
.map((address) => (
<div key={address} className="rounded-md border px-2 py-1 font-mono text-sm">
{address}
</div>
))}
</CollapsibleContent>
</Collapsible>
{!simplify && chains.localnets.has(chain.registryName) ? (
<div className="flex justify-center pt-2">
<ButtonWithConfirm
onClick={() => {
deleteLocalChainFromStorage(chain.registryName, chains);
}}
text="Delete custom chain"
confirmText="Confirm deletion?"
disabled={connectedChain.registryName === chain.registryName}
/>
</div>
) : !simplify ? (
<div className="flex items-center pt-2 hover:cursor-pointer">
<ExternalLink className="mr-2 h-4 w-4" />
<Link
href={`https://github.com/cosmos/chain-registry/tree/master/${
chains.testnets.has(chain.registryName) ? `testnets/` : ""
}${chain.registryName}`}
target="_blank"
className="text-xs underline"
>
Check it out on the registry
</Link>
</div>
) : null}
</div>
);
}
+72
View File
@@ -0,0 +1,72 @@
import { useChains } from "@/context/ChainsContext";
import { setNewConnection } from "@/context/ChainsContext/helpers";
import { ChainInfo } from "@/context/ChainsContext/types";
import { cn } from "@/lib/utils";
import { CheckCircle } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { CommandItem } from "../ui/command";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "../ui/hover-card";
import ChainDigest from "./ChainDigest";
interface ChainItemProps {
readonly chain: ChainInfo;
readonly hoverCardElementBoundary: HTMLDivElement | null;
}
export default function ChainItem({ chain, hoverCardElementBoundary }: ChainItemProps) {
const { chain: connectedChain, chainsDispatch } = useChains();
return (
<HoverCard key={chain.registryName} openDelay={300}>
<HoverCardTrigger asChild>
<CommandItem
value={chain.registryName}
onSelect={
connectedChain.registryName === chain.registryName
? () => {}
: () => {
setNewConnection(chainsDispatch, { action: "confirm", chain });
}
}
className={cn(
"group relative inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border-2 border-white p-2.5 text-sm font-medium text-white focus:outline-none focus:ring-4 focus:ring-red-100 aria-selected:text-gray-900",
connectedChain.registryName === chain.registryName
? "cursor-not-allowed border-green-600 bg-green-300 text-green-900 aria-selected:bg-green-300"
: "transparent cursor-pointer border-white",
)}
>
{connectedChain.registryName === chain.registryName ? (
<CheckCircle
style={{
position: "absolute",
top: "-6px",
left: "-6px",
border: "2px solid rgb(134 239 172 / var(--tw-bg-opacity))",
borderRadius: "50%",
width: "25px",
height: "25px",
background: "rgb(134 239 172 / var(--tw-bg-opacity))",
color: "rgb(20 83 45 / var(--tw-text-opacity))",
}}
/>
) : null}
<Avatar>
<AvatarImage
src={chain.logo}
alt={`${chain.chainDisplayName} logo`}
className="h-auto"
/>
<AvatarFallback>{chain.registryName.slice(0, 1).toUpperCase()}</AvatarFallback>
</Avatar>
{chain.registryName}
</CommandItem>
</HoverCardTrigger>
<HoverCardContent
className="w-auto bg-fuchsia-900"
collisionBoundary={hoverCardElementBoundary}
>
<ChainDigest chain={chain} />
</HoverCardContent>
</HoverCard>
);
}
+35
View File
@@ -0,0 +1,35 @@
import { ChainInfo } from "@/context/ChainsContext/types";
import { CommandGroup } from "../ui/command";
import ChainItem from "./ChainItem";
import { useRef } from "react";
interface ChainsGroupProps {
readonly chains: readonly ChainInfo[];
readonly heading: string;
readonly emptyMsg: string;
}
export default function ChainsGroup({ chains, heading, emptyMsg }: ChainsGroupProps) {
const containerRef = useRef<HTMLDivElement>(null);
return (
<CommandGroup
heading={heading}
className="[&_[cmdk-group-heading]]:my-3 [&_[cmdk-group-heading]]:text-base [&_[cmdk-group-heading]]:leading-[0] [&_[cmdk-group-heading]]:text-white [&_[cmdk-group-heading]]:underline"
>
{chains.length ? (
<div ref={containerRef} className="flex flex-wrap gap-2">
{chains.map((chain) => (
<ChainItem
key={chain.registryName}
chain={chain}
hoverCardElementBoundary={containerRef.current}
/>
))}
</div>
) : (
<p className="ml-3 max-w-none">{emptyMsg}</p>
)}
</CommandGroup>
);
}
+58
View File
@@ -0,0 +1,58 @@
import { useChains } from "@/context/ChainsContext";
import { getRecentChainsFromStorage } from "@/context/ChainsContext/storage";
import { ChainInfo } from "@/context/ChainsContext/types";
import { useLayoutEffect, useState } from "react";
import { Command, CommandEmpty, CommandInput, CommandList, CommandSeparator } from "../ui/command";
import ChainsGroup from "./ChainsGroup";
export default function ChooseChain() {
const { chains } = useChains();
const [recentChains, setRecentChains] = useState<readonly ChainInfo[]>([]);
useLayoutEffect(() => {
const newRecentChains = getRecentChainsFromStorage(chains);
setRecentChains(newRecentChains);
}, [chains]);
return (
<Command
className="bg-fuchsia-900 text-white"
style={
{
"--accent": "0, 100%, 100%",
"--border": "0, 100%, 100%",
} as React.CSSProperties
}
>
<CommandInput placeholder="Type a chain name or id…" />
<div className="overflow-x-auto overflow-y-auto">
<CommandList className="max-h-full overflow-x-hidden overflow-y-visible">
<CommandEmpty>No results found.</CommandEmpty>
<ChainsGroup
chains={recentChains}
heading="Recently used chains:"
emptyMsg="No recent chains found."
/>
<CommandSeparator className="m-2" />
<ChainsGroup
chains={Array.from(chains.localnets.values())}
heading="Custom chains:"
emptyMsg={`No custom chains found. You can add one on the "Custom chain" tab.`}
/>
<CommandSeparator className="m-2" />
<ChainsGroup
chains={Array.from(chains.mainnets.values())}
heading="Mainnets:"
emptyMsg="No mainnets chains found."
/>
<CommandSeparator className="m-2" />
<ChainsGroup
chains={Array.from(chains.testnets.values())}
heading="Testnets:"
emptyMsg="No testnets chains found."
/>
</CommandList>
</div>
</Command>
);
}
@@ -0,0 +1,61 @@
import { useChains } from "@/context/ChainsContext";
import { setChain, setNewConnection } from "@/context/ChainsContext/helpers";
import { useRouter } from "next/router";
import { Button } from "../ui/button";
import ChainDigest from "./ChainDigest";
interface ConfirmConnectionProps {
readonly closeDialog: () => void;
}
export default function ConfirmConnection({ closeDialog }: ConfirmConnectionProps) {
const router = useRouter();
const { chain, newConnection, chainsDispatch } = useChains();
if (newConnection.action !== "confirm") {
return null;
}
return (
<>
<h3>
Disconnect from "{chain.registryName}" and connect to "{newConnection.chain.registryName}"?
</h3>
<p className="max-w-none">
You will be redirected to the homepage and any filled form will be lost
</p>
<div
className="flex flex-wrap items-center justify-around gap-4"
style={{ "--border": "0, 100%, 100%" } as React.CSSProperties}
>
<div className="rounded-md border border-white p-4">
<ChainDigest chain={chain} simplify />
</div>
<div className="rounded-md border border-white p-4">
<ChainDigest chain={newConnection.chain} simplify />
</div>
</div>
<div className="flex gap-4">
<Button
variant="secondary"
className="mt-4"
onClick={() => {
setNewConnection(chainsDispatch, { ...newConnection, action: "edit" });
}}
>
Edit chain
</Button>
<Button
className="mt-4"
onClick={() => {
setChain(chainsDispatch, newConnection.chain);
closeDialog();
router.push("/");
}}
>
Connect
</Button>
</div>
</>
);
}
+274
View File
@@ -0,0 +1,274 @@
import { useChains } from "@/context/ChainsContext";
import { setNewConnection } from "@/context/ChainsContext/helpers";
import { RegistryAsset } from "@/types/chainRegistry";
import { zodResolver } from "@hookform/resolvers/zod";
import dynamic from "next/dynamic";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Button } from "../ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "../ui/form";
import { Input } from "../ui/input";
const JsonEditor = dynamic(() => import("../inputs/JsonEditor"), { ssr: false });
export default function CustomChainForm() {
const { chain, chains, newConnection, chainsDispatch } = useChains();
const formSchema = z
.object({
localRegistryName: z
.string({ required_error: "Local registry name is required" })
.refine((val) => !chains.mainnets.has(val) && !chains.testnets.has(val), {
message: "Name already exists in remote registry",
}),
chainName: z.string({ required_error: "Chain name is required" }),
chainId: z.string({ required_error: "Chain ID is required" }),
baseDenom: z.string({ required_error: "Base denom is required" }),
displayDenom: z.string({ required_error: "Display denom is required" }),
denomExponent: z.string({ required_error: "Denom exponent is required" }),
bech32Prefix: z.string({ required_error: "Address prefix is required" }),
gasPrice: z.string({ required_error: "Gas price is required" }),
rpcNodes: z.string({ required_error: "Comma separated rpc nodes are required" }),
explorerTxLink: z.string({ required_error: "Explorer tx url is required" }),
explorerAccountLink: z.string({ required_error: "Explorer account url is required" }),
logo: z.string({ required_error: "Logo url is required" }),
assets: z.string({ required_error: "Assets json is required" }),
})
.required();
const defaultChain = newConnection.chain ?? chain;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
localRegistryName: defaultChain.registryName,
chainName: defaultChain.chainDisplayName,
chainId: defaultChain.chainId,
baseDenom: defaultChain.denom,
displayDenom: defaultChain.displayDenom,
denomExponent: String(defaultChain.displayDenomExponent),
bech32Prefix: defaultChain.addressPrefix,
gasPrice: defaultChain.gasPrice,
rpcNodes: defaultChain.nodeAddresses.join(", "),
explorerTxLink: defaultChain.explorerLink.tx,
explorerAccountLink: defaultChain.explorerLink.account,
logo: defaultChain.logo,
assets: JSON.stringify(defaultChain.assets),
},
});
function onSubmit(chainFromForm: z.infer<typeof formSchema>) {
const rpcNodes = chainFromForm.rpcNodes.split(", ");
setNewConnection(chainsDispatch, {
action: "confirm",
chain: {
registryName: chainFromForm.localRegistryName,
logo: chainFromForm.logo,
chainId: chainFromForm.chainId,
chainDisplayName: chainFromForm.chainName,
nodeAddress: "",
nodeAddresses: rpcNodes,
denom: chainFromForm.baseDenom,
displayDenom: chainFromForm.displayDenom,
displayDenomExponent: Number(chainFromForm.denomExponent),
assets: JSON.parse(chainFromForm.assets) as RegistryAsset[],
gasPrice: chainFromForm.gasPrice,
addressPrefix: chainFromForm.bech32Prefix,
explorerLink: {
tx: chainFromForm.explorerTxLink,
account: chainFromForm.explorerAccountLink,
},
},
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="grid grid-cols-3 gap-4">
<FormField
name="localRegistryName"
render={({ field }) => (
<FormItem>
<FormLabel>Local Registry Name</FormLabel>
<FormControl>
<Input placeholder="mynetwork" className="border-white" {...field} />
</FormControl>
<FormDescription>
A unique key to store this chain on your local registry
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="chainName"
render={({ field }) => (
<FormItem>
<FormLabel>Chain Name</FormLabel>
<FormControl>
<Input placeholder="My Network" className="border-white" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="chainId"
render={({ field }) => (
<FormItem>
<FormLabel>Chain ID</FormLabel>
<FormControl>
<Input placeholder="my-net-4" className="border-white" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="baseDenom"
render={({ field }) => (
<FormItem>
<FormLabel>Base Denom</FormLabel>
<FormControl>
<Input placeholder="umycoin" className="border-white" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="displayDenom"
render={({ field }) => (
<FormItem>
<FormLabel>Display Denom</FormLabel>
<FormControl>
<Input placeholder="MYCOIN" className="border-white" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="denomExponent"
render={({ field }) => (
<FormItem>
<FormLabel>Denom Exponent</FormLabel>
<FormControl>
<Input placeholder="6" className="border-white" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="bech32Prefix"
render={({ field }) => (
<FormItem>
<FormLabel>Address Prefix</FormLabel>
<FormControl>
<Input placeholder="mynet" className="border-white" {...field} />
</FormControl>
<FormDescription>Needs to be bech32</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="gasPrice"
render={({ field }) => (
<FormItem>
<FormLabel>Gas Price</FormLabel>
<FormControl>
<Input placeholder="0.04umycoin" className="border-white" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="explorerTxLink"
render={({ field }) => (
<FormItem>
<FormLabel>Explorer Tx Link</FormLabel>
<FormControl>
<Input placeholder="url" className="border-white" {...field} />
</FormControl>
<FormDescription>with {"'${txHash}'"} included</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="explorerAccountLink"
render={({ field }) => (
<FormItem>
<FormLabel>Explorer Account Link</FormLabel>
<FormControl>
<Input placeholder="url" className="border-white" {...field} />
</FormControl>
<FormDescription>with {"'${accountAddress}'"} included</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
name="rpcNodes"
render={({ field }) => (
<FormItem className="mt-4">
<FormLabel>RPC nodes</FormLabel>
<FormControl>
<Input placeholder="url1, url2, …, urln" className="border-white" {...field} />
</FormControl>
<FormDescription>Can be one or more, separated by commas</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="logo"
render={({ field }) => (
<FormItem>
<FormLabel>Logo URI</FormLabel>
<FormControl>
<Input placeholder="logo" className="border-white" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="assets"
render={({ field }) => (
<FormItem className="mt-4">
<FormLabel>Assets</FormLabel>
<FormControl>
<div>
<JsonEditor
content={{ text: field.value }}
onChange={(newMsgContent) => {
field.onChange("text" in newMsgContent ? newMsgContent.text ?? "{}" : "{}");
}}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="mt-4">
Submit
</Button>
</form>
</Form>
);
}
+42
View File
@@ -0,0 +1,42 @@
import { useChains } from "@/context/ChainsContext";
import { isChainInfoFilled } from "@/context/ChainsContext/helpers";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { DialogTrigger } from "../ui/dialog";
import { Skeleton } from "../ui/skeleton";
export function ChainHeader() {
const { chain } = useChains();
return isChainInfoFilled(chain) ? (
<>
<Avatar>
<AvatarImage src={chain.logo} alt={`${chain.chainDisplayName} logo`} />
<AvatarFallback>{chain.registryName.slice(0, 1).toUpperCase()}</AvatarFallback>
</Avatar>
<h1>{chain.chainDisplayName} Multisig</h1>
</>
) : (
<>
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-[200px]" />
</div>
</>
);
}
export default function DialogButton() {
const showChainSelect = process.env.NEXT_PUBLIC_MULTICHAIN?.toLowerCase() === "true";
return showChainSelect ? (
<DialogTrigger>
<div className="group relative m-1 inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border-2 border-white p-2.5 text-sm font-medium text-white hover:bg-white hover:text-gray-900 focus:outline-none focus:ring-4 focus:ring-red-100">
<ChainHeader />
</div>
</DialogTrigger>
) : (
<div className="group relative m-1 inline-flex items-center justify-center gap-2 p-2.5 text-sm font-medium text-white">
<ChainHeader />
</div>
);
}
+23
View File
@@ -0,0 +1,23 @@
import { cn } from "@/lib/utils";
import { ComponentProps } from "react";
import { TabsTrigger } from "../ui/tabs";
export default function TabButton({
value,
children,
className,
...restProps
}: ComponentProps<typeof TabsTrigger>) {
return (
<TabsTrigger
value={value}
className={cn(
"border-2 border-solid border-white bg-transparent text-white data-[state=active]:bg-white data-[state=active]:text-gray-900",
className,
)}
{...restProps}
>
{children}
</TabsTrigger>
);
}
+52
View File
@@ -0,0 +1,52 @@
import { useChains } from "@/context/ChainsContext";
import { setNewConnection } from "@/context/ChainsContext/helpers";
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader } from "../ui/dialog";
import { Tabs, TabsContent, TabsList } from "../ui/tabs";
import ChooseChain from "./ChooseChain";
import ConfirmConnection from "./ConfirmConnection";
import CustomChainForm from "./CustomChainForm";
import DialogButton from "./DialogButton";
import TabButton from "./TabButton";
const tabs = { choose: "choose", custom: "custom" };
export default function ChainConnect() {
const { newConnection, chainsDispatch } = useChains();
const [dialogOpen, setDialogOpen] = useState(false);
return (
<Dialog
open={dialogOpen}
onOpenChange={(open) => {
setDialogOpen(open);
setNewConnection(chainsDispatch, { action: "edit" });
}}
>
<DialogButton />
<DialogContent
className={"max-h-[75%] max-w-[75%] overflow-y-auto bg-fuchsia-900"}
style={newConnection.action === "confirm" ? { width: "auto" } : {}}
>
{newConnection.action === "confirm" ? (
<ConfirmConnection closeDialog={() => setDialogOpen(false)} />
) : (
<Tabs defaultValue={newConnection.chain ? tabs.custom : tabs.choose}>
<DialogHeader>
<TabsList className="justify-start gap-2 bg-transparent">
<TabButton value={tabs.choose}>Choose chain</TabButton>
<TabButton value={tabs.custom}>Custom chain</TabButton>
</TabsList>
</DialogHeader>
<TabsContent value={tabs.choose}>
<ChooseChain />
</TabsContent>
<TabsContent value={tabs.custom}>
<CustomChainForm />
</TabsContent>
</Tabs>
)}
</DialogContent>
</Dialog>
);
}
+29
View File
@@ -0,0 +1,29 @@
import { useChains } from "@/context/ChainsContext";
import { UserCircle2 } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import ChainConnect from "./ChainConnect";
export default function Header() {
const { pathname } = useRouter();
const { chain } = useChains();
return (
<header className="flex flex-row items-center justify-between gap-4 bg-fuchsia-900 px-3">
<ChainConnect />
<Link
href={
pathname.includes(chain.registryName)
? {
pathname: "account",
query: { chainName: chain.registryName },
}
: `/${chain.registryName}/account`
}
className="h-10 w-10 rounded-full hover:outline-dashed hover:outline-white focus:outline-dashed focus:outline-white"
>
<UserCircle2 className="h-full w-auto" />
</Link>
</header>
);
}
-290
View File
@@ -1,290 +0,0 @@
import { StargateClient } from "@cosmjs/stargate";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useChains } from "../../context/ChainsContext";
import {
setChain,
setChainFromRegistry,
setChainsError,
} from "../../context/ChainsContext/helpers";
import { RegistryAsset } from "../../types/chainRegistry";
import GearIcon from "../icons/Gear";
import Button from "../inputs/Button";
import Input from "../inputs/Input";
import Select from "../inputs/Select";
import StackableContainer from "../layout/StackableContainer";
interface ChainOption {
readonly label: string;
readonly value: string;
}
const ChainSelect = () => {
const router = useRouter();
const { chain, chains, chainsError, chainsDispatch } = useChains();
const [optionToConfirm, setOptionToConfirm] = useState<ChainOption | null>(null);
const [showAuxView, setShowAuxView] = useState<null | "settings" | "confirmRedirect">(null);
const [chainInForm, setChainInForm] = useState(chain);
const [stringAssets, setStringAssets] = useState(JSON.stringify(chain.assets));
const chainArray = [...chains.mainnets, ...chains.testnets];
const chainOptions: readonly ChainOption[] = chainArray.map(({ name }) => ({
label: name,
value: name,
}));
const selectValue = chainOptions.find((option) => option.value === chain.registryName) ?? {
label: "unknown chain",
value: chain.registryName,
};
useEffect(() => {
setChainInForm(chain);
setStringAssets(JSON.stringify(chain.assets));
}, [chain]);
useEffect(() => {
try {
const assets: readonly RegistryAsset[] = JSON.parse(stringAssets);
setChainInForm((oldChain) => ({ ...oldChain, assets }));
} catch {
setChainsError(chainsDispatch, "Assets needs to be valid JSON");
}
}, [chainsDispatch, stringAssets]);
const selectChainOption = (chainOption: ChainOption) => {
if (router.pathname !== "/" && chainOption.value !== selectValue.value) {
setOptionToConfirm(chainOption);
setShowAuxView("confirmRedirect");
return;
}
setChainsError(chainsDispatch, null);
setChainFromRegistry(chainsDispatch, chainOption.value);
setOptionToConfirm(null);
};
const redirectAndChangeChain = () => {
setShowAuxView(null);
if (optionToConfirm) {
setChainFromRegistry(chainsDispatch, optionToConfirm.value);
setOptionToConfirm(null);
}
router.push("/");
};
const setChainFromForm = async () => {
setChainsError(chainsDispatch, null);
try {
// test client connection
const client = await StargateClient.connect(chainInForm.nodeAddress);
await client.getHeight();
setShowAuxView(null);
setChain(chainsDispatch, chainInForm);
} catch (error) {
if (error instanceof Error) {
setChainsError(chainsDispatch, error.message);
} else {
setChainsError(chainsDispatch, "Error when setting new chain");
}
setShowAuxView("settings");
}
};
return (
<div className="chain-select-container">
<StackableContainer lessPadding base>
<p>Chain select</p>
<div className="flex" style={{ margin: 0 }}>
<div className="select-parent">
<Select
name="chain-select"
options={chainOptions}
value={selectValue}
onChange={selectChainOption}
/>
</div>
{showAuxView ? (
<button
className="remove"
onClick={() => {
setShowAuxView(null);
setOptionToConfirm(null);
}}
>
</button>
) : (
<button onClick={() => setShowAuxView("settings")} style={{ width: "auto" }}>
<GearIcon color="white" />
</button>
)}
</div>
{showAuxView === "settings" ? (
<>
{chainsError ? <p className="error">{chainsError}</p> : null}
<StackableContainer lessPadding lessMargin lessRadius>
<p>Settings</p>
<div className="settings-group">
<Input
width="48%"
value={chainInForm.chainDisplayName}
onChange={({ target }) =>
setChainInForm((oldChain) => ({ ...oldChain, chainDisplayName: target.value }))
}
label="Chain Name"
/>
<Input
width="48%"
value={chainInForm.chainId}
onChange={({ target }) =>
setChainInForm((oldChain) => ({ ...oldChain, chainId: target.value }))
}
label="Chain ID"
/>
</div>
<div className="settings-group">
<Input
width="48%"
value={chainInForm.addressPrefix}
onChange={({ target }) =>
setChainInForm((oldChain) => ({ ...oldChain, addressPrefix: target.value }))
}
label="Bech32 Prefix (address prefix)"
/>
<Input
width="48%"
value={chainInForm.nodeAddress}
onChange={({ target }) =>
setChainInForm((oldChain) => ({ ...oldChain, nodeAddress: target.value }))
}
label="RPC Node URL (must be https)"
/>
</div>
<div className="settings-group">
<Input
width="48%"
value={chainInForm.displayDenom}
onChange={({ target }) =>
setChainInForm((oldChain) => ({ ...oldChain, displayDenom: target.value }))
}
label="Display Denom"
/>
<Input
width="48%"
value={chainInForm.denom}
onChange={({ target }) =>
setChainInForm((oldChain) => ({ ...oldChain, denom: target.value }))
}
label="Base Denom"
/>
</div>
<div className="settings-group">
<Input
width="48%"
value={chainInForm.displayDenomExponent}
onChange={({ target }) =>
setChainInForm((oldChain) => ({
...oldChain,
displayDenomExponent: Number(target.value),
}))
}
label="Denom Exponent"
/>
<Input
width="48%"
value={stringAssets}
onChange={({ target }) => setStringAssets(target.value)}
label="Assets"
/>
</div>
<div className="settings-group">
<Input
width="48%"
value={chainInForm.gasPrice}
onChange={({ target }) =>
setChainInForm((oldChain) => ({ ...oldChain, gasPrice: target.value }))
}
label="Gas Price"
/>
<Input
width="48%"
value={chainInForm.explorerLink}
onChange={({ target }) =>
setChainInForm((oldChain) => ({ ...oldChain, explorerLink: target.value }))
}
label="Explorer Link (with '${txHash}' included)"
/>
</div>
<Button label="Set Chain" onClick={setChainFromForm} />
</StackableContainer>
</>
) : null}
{showAuxView === "confirmRedirect" && optionToConfirm ? (
<StackableContainer lessPadding lessMargin lessRadius>
<p>
If you change to {optionToConfirm.label} your unsaved changes will be lost and you
will be redirected to the main screen
</p>
<Button label={`Change to ${optionToConfirm.label}`} onClick={redirectAndChangeChain} />
</StackableContainer>
) : null}
</StackableContainer>
<style jsx>{`
.chain-select-container {
position: absolute;
z-index: 10;
top: 1em;
right: 1em;
width: ${showAuxView === "settings" ? "600px" : "300px"};
}
.flex {
margin-top: 0.5em;
display: flex;
justify-content: space-between;
align-items: center;
}
.select-parent {
width: calc(98% - 3em);
}
.settings-group {
display: flex;
justify-content: space-between;
margin-top: 1.5em;
}
button {
background: none;
border: none;
display: block;
width: 3em;
height: 3em;
opacity: 0.7;
user-select: none;
}
button:hover {
opacity: 1;
}
button.remove {
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
border: none;
color: white;
}
.error {
color: coral;
font-size: 0.8em;
text-align: left;
margin: 1em 0 0 0;
}
`}</style>
</div>
);
};
export default ChainSelect;
@@ -0,0 +1,29 @@
import { printableCoin } from "@/lib/displayHelpers";
import { Coin } from "@cosmjs/amino";
import { useChains } from "../../../context/ChainsContext";
import { Avatar, AvatarFallback, AvatarImage } from "../../ui/avatar";
import { Badge } from "../../ui/badge";
interface BalancePillProps {
readonly coin: Coin;
}
export default function BalancePill({ coin }: BalancePillProps) {
const { chain } = useChains();
const foundAsset = chain.assets.find((asset) => asset.base === coin.denom);
const logo = foundAsset?.logo_URIs?.svg || foundAsset?.logo_URIs?.png || "";
const macroCoin = printableCoin(coin, chain);
return (
<Badge key={coin.denom} className="px-1">
<Avatar className="mr-2">
<AvatarImage src={logo} alt={`${coin.denom} logo`} className="h-auto" />
<AvatarFallback className="text-white">
{coin.denom.slice(1, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
{macroCoin}
</Badge>
);
}
@@ -0,0 +1,46 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Coin } from "@cosmjs/amino";
import { StargateClient } from "@cosmjs/stargate";
import { Dispatch, SetStateAction, 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) {
const { chain } = useChains();
const [balances, setBalances] = useState<readonly Coin[]>([]);
useEffect(() => {
(async function () {
if (!walletAddress) {
return;
}
try {
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);
}
})();
}, [chain.nodeAddress, setError, walletAddress]);
return balances.length ? (
<Card className="bg-fuchsia-850 w-full max-w-md border-transparent">
<CardHeader className="p-0">
<CardTitle>Balances</CardTitle>
</CardHeader>
<CardContent className="mt-4 flex flex-wrap gap-2 p-0">
{balances.map((coin) => (
<BalancePill key={coin.denom} coin={coin} />
))}
</CardContent>
</Card>
) : null;
}
@@ -0,0 +1,122 @@
import { LoadingStates, WalletInfo, WalletType } from "@/types/signing";
import { makeCosmoshubPath } from "@cosmjs/amino";
import { toBase64 } from "@cosmjs/encoding";
import { LedgerSigner } from "@cosmjs/ledger-amino";
import TransportWebUSB from "@ledgerhq/hw-transport-webusb";
import { Loader2 } from "lucide-react";
import Image from "next/image";
import { Dispatch, SetStateAction, useCallback, useLayoutEffect, useState } from "react";
import { useChains } from "../../../context/ChainsContext";
import { getConnectError } from "../../../lib/errorHelpers";
import { Button } from "../../ui/button";
interface ButtonConnectWalletProps {
readonly walletType: WalletType;
readonly walletInfoState: [
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);
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({ type: "Keplr", address, pubKey });
} catch (e) {
console.error(e);
setError(getConnectError(e));
} finally {
setLoading((newLoading) => ({ ...newLoading, keplr: false }));
}
}, [chain.chainId, setError, setWalletInfo]);
useLayoutEffect(() => {
if (!walletInfo?.address) {
return;
}
const accountChangeKey = "keplr_keystorechange";
if (walletInfo.type === "Keplr") {
window.addEventListener(accountChangeKey, connectKeplr);
} else {
window.removeEventListener(accountChangeKey, connectKeplr);
}
}, [connectKeplr, walletInfo]);
const connectLedger = async () => {
try {
setError("");
setLoading((newLoading) => ({ ...newLoading, ledger: true }));
const ledgerTransport = await TransportWebUSB.create(120000, 120000);
const offlineSigner = new LedgerSigner(ledgerTransport, {
hdPaths: [makeCosmoshubPath(0)],
prefix: chain.addressPrefix,
});
const [{ address, pubkey: pubKeyArray }] = await offlineSigner.getAccounts();
const pubKey = toBase64(pubKeyArray);
setWalletInfo({ type: "Ledger", address, pubKey });
} catch (e) {
console.error(e);
setError(getConnectError(e));
} finally {
setLoading((newLoading) => ({ ...newLoading, ledger: false }));
}
};
const onClick = (() => {
if (walletType === "Keplr") {
return connectKeplr;
}
if (walletType === "Ledger") {
return connectLedger;
}
return () => {};
})();
const isLoading =
(walletType === "Keplr" && loading.keplr) || (walletType === "Ledger" && loading.ledger);
return (
<Button onClick={onClick} disabled={loading.keplr || loading.ledger}>
{isLoading ? (
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : (
<Image
alt=""
src={`/assets/icons/${walletType.toLowerCase()}.svg`}
width={20}
height={20}
className="mr-2"
/>
)}
Connect {walletType}
</Button>
);
}
+101
View File
@@ -0,0 +1,101 @@
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 Image from "next/image";
import { useState } from "react";
import { useChains } from "../../../context/ChainsContext";
import { Button } from "../../ui/button";
import BalancesList from "./BalancesList";
import ButtonConnectWallet from "./ButtonConnectWallet";
export default function AccountView() {
const { chain } = useChains();
const walletInfoState = useState<WalletInfo | null>();
const [walletInfo, setWalletInfo] = walletInfoState;
const [error, setError] = useState("");
const explorerLink =
explorerLinkAccount(chain.explorerLink.account, walletInfo?.address || "") || "";
return (
<div className="mt-6 flex flex-col gap-4">
<Card className="bg-fuchsia-850 min-w-[400px] border-transparent">
<CardHeader className="p-0">
<CardTitle className="flex items-baseline gap-2">
{walletInfo?.type ? (
<Image
alt=""
src={`/assets/icons/${walletInfo.type.toLowerCase()}.svg`}
width={20}
height={20}
/>
) : null}
{walletInfo?.type ? `${walletInfo.type} wallet connected` : "Connect wallet"}
</CardTitle>
</CardHeader>
{walletInfo ? (
<CardContent className="mt-4 max-w-md p-0">
<h2>Address</h2>
<div className="flex flex-col gap-4">
<BadgeWithCopy name="address" toCopy={walletInfo.address} />
{explorerLink ? (
<Button asChild className="self-center">
<a href={explorerLink} target="_blank">
View in explorer
</a>
</Button>
) : null}
</div>
<div className="mt-4">
<h2>Public key</h2>
<BadgeWithCopy name="pubKey" toCopy={walletInfo.pubKey} />
</div>
</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
onClick={() => {
setWalletInfo(null);
}}
>
<Unplug className="mr-2 h-auto w-5 text-red-500" />
Disconnect {walletInfo.type}
</Button>
) : (
<div className="flex w-full flex-col gap-4">
<ButtonConnectWallet
walletType="Keplr"
walletInfoState={walletInfoState}
setError={setError}
/>
<ButtonConnectWallet
walletType="Ledger"
walletInfoState={walletInfoState}
setError={setError}
/>
</div>
)}
</CardFooter>
</Card>
{walletInfo?.address ? (
<BalancesList
key={walletInfo.address}
walletAddress={walletInfo.address}
setError={setError}
/>
) : null}
</div>
);
}
@@ -10,8 +10,8 @@ interface CompletedTransactionProps {
const CompletedTransaction = ({ transactionHash }: CompletedTransactionProps) => {
const { chain } = useChains();
const baseURL = chain.explorerLink ? chain.explorerLink : "";
const explorerLink = explorerLinkTx(baseURL, transactionHash);
const explorerLink = explorerLinkTx(chain.explorerLink.tx, transactionHash);
return (
<StackableContainer lessPadding lessMargin>
<StackableContainer lessPadding lessMargin lessRadius>
+4 -1
View File
@@ -69,7 +69,10 @@ const FindMultisigForm = (props: Props) => {
</StackableContainer>
<StackableContainer lessPadding>
<p className="create-help">Don't have a multisig?</p>
<Button label="Create New Multisig" onClick={() => props.router.push("create")} />
<Button
label="Create New Multisig"
onClick={() => props.router.push(`${chain.registryName}/create`)}
/>
</StackableContainer>
<style jsx>{`
.multisig-form {
+1 -8
View File
@@ -1,3 +1,4 @@
import { LoadingStates, SigningStatus } from "@/types/signing";
import { MultisigThresholdPubkey, makeCosmoshubPath } from "@cosmjs/amino";
import { createWasmAminoConverters, wasmTypes } from "@cosmjs/cosmwasm-stargate";
import { toBase64 } from "@cosmjs/encoding";
@@ -20,14 +21,6 @@ import HashView from "../dataViews/HashView";
import Button from "../inputs/Button";
import StackableContainer from "../layout/StackableContainer";
type SigningStatus = "not_signed" | "not_a_member" | "signed";
interface LoadingStates {
readonly signing?: boolean;
readonly keplr?: boolean;
readonly ledger?: boolean;
}
interface TransactionSigningProps {
readonly signatures: DbSignature[];
readonly tx: DbTransaction;
+50
View File
@@ -0,0 +1,50 @@
import { cn } from "@/lib/utils";
import { ComponentProps, MouseEventHandler, useEffect, useState } from "react";
import { Button } from "../ui/button";
interface ButtonWithConfirmProps extends Omit<ComponentProps<typeof Button>, "children"> {
readonly text: string;
readonly confirmText: string;
readonly onClick: MouseEventHandler<HTMLButtonElement>;
}
export default function ButtonWithConfirm({
text,
confirmText,
onClick,
...restProps
}: ButtonWithConfirmProps) {
const [toConfirm, setToConfirm] = useState(false);
useEffect(() => {
if (!toConfirm) {
return;
}
const timeout = setTimeout(() => {
setToConfirm(false);
}, 3000);
return () => {
clearTimeout(timeout);
};
}, [toConfirm]);
return (
<Button
size="sm"
variant={toConfirm ? "destructive" : "default"}
className={cn(toConfirm ? "" : "bg-yellow-300 hover:bg-yellow-300")}
onClick={
toConfirm
? onClick
: () => {
setToConfirm(true);
}
}
{...restProps}
>
{toConfirm ? confirmText : text}
</Button>
);
}
+1
View File
@@ -39,6 +39,7 @@ const Input = (props: InputProps) => (
}
label {
color: white;
font-style: italic;
font-size: 12px;
margin-bottom: ${props.type === "checkbox" ? 0 : "1em"};
+1
View File
@@ -51,6 +51,7 @@ export default function JsonEditor({ label, ...editorProps }: JsonEditorProps) {
{label ? <label>{label}</label> : null}
<style jsx>{`
.container {
padding: 0;
display: flex;
flex-direction: column;
gap: 0.8em;
+3 -2
View File
@@ -1,3 +1,4 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import Head from "../head";
@@ -19,7 +20,7 @@ const Page = ({ title, goBack, children }: PageProps) => {
const linkProps = (() => {
if (!goBack) {
return {};
return { href: "" };
}
if (goBack.needsConfirm && !showConfirm) {
@@ -52,7 +53,7 @@ const Page = ({ title, goBack, children }: PageProps) => {
}}
>
<p>
<a {...linkProps}> Back to {goBack.title}</a>
<Link {...linkProps}> Back to {goBack.title}</Link>
</p>
{showConfirm ? (
<>
+1 -7
View File
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
@@ -9,12 +8,7 @@ const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = ({
className,
...props
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
<AlertDialogPrimitive.Portal className={cn(className)} {...props} />
)
const AlertDialogPortal = AlertDialogPrimitive.Portal
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
const AlertDialogOverlay = React.forwardRef<
-2
View File
@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-shadow */
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
+1 -6
View File
@@ -8,12 +8,7 @@ const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = ({
className,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props} />
)
const DialogPortal = DialogPrimitive.Portal
DialogPortal.displayName = DialogPrimitive.Portal.displayName
const DialogOverlay = React.forwardRef<
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
+6 -1
View File
@@ -38,7 +38,12 @@ const ScrollBar = React.forwardRef<
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
<ScrollAreaPrimitive.ScrollAreaThumb
className={cn(
"relative rounded-full bg-border",
orientation === "vertical" && "flex-1"
)}
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+1 -6
View File
@@ -11,12 +11,7 @@ const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = ({
className,
...props
}: SheetPrimitive.DialogPortalProps) => (
<SheetPrimitive.Portal className={cn(className)} {...props} />
)
const SheetPortal = SheetPrimitive.Portal
SheetPortal.displayName = SheetPrimitive.Portal.displayName
const SheetOverlay = React.forwardRef<
+1 -1
View File
@@ -6,7 +6,7 @@ const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="w-full overflow-auto">
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
+7 -2
View File
@@ -12,10 +12,15 @@ export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
<ToastProvider >
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<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 && (
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-shadow */
// Inspired by react-hot-toast library
import * as React from "react"
+25 -11
View File
@@ -1,23 +1,37 @@
import { ChainInfo, ChainItems, Dispatch } from "./types";
import { ChainInfo, ChainItems, Dispatch, NewConnection } from "./types";
export const emptyChain: ChainInfo = {
registryName: "",
logo: "",
chainId: "",
chainDisplayName: "",
nodeAddress: "",
nodeAddresses: [],
denom: "",
displayDenom: "",
displayDenomExponent: 0,
assets: [],
gasPrice: "",
chainId: "",
chainDisplayName: "",
registryName: "",
addressPrefix: "",
explorerLink: "",
explorerLink: { tx: "", account: "" },
};
export const isChainInfoFilled = ({ displayDenomExponent, assets, ...restFields }: ChainInfo) =>
displayDenomExponent >= 0 &&
assets.length > 0 &&
Object.values(restFields).every((value) => value !== "");
export const isChainInfoFilled = (chain: Partial<ChainInfo>): chain is ChainInfo =>
Boolean(
chain.registryName &&
typeof chain.logo === "string" &&
chain.chainId &&
chain.chainDisplayName &&
typeof chain.nodeAddress === "string" &&
chain.nodeAddresses?.length &&
chain.denom &&
chain.displayDenom &&
chain.displayDenomExponent &&
chain.displayDenomExponent >= 0 &&
chain.assets?.length &&
chain.gasPrice &&
chain.addressPrefix,
);
export const setChains = (dispatch: Dispatch, chains: ChainItems) => {
dispatch({ type: "setChains", payload: chains });
@@ -27,8 +41,8 @@ export const setChain = (dispatch: Dispatch, chain: ChainInfo) => {
dispatch({ type: "setChain", payload: chain });
};
export const setChainFromRegistry = (dispatch: Dispatch, chainName: string) => {
dispatch({ type: "setChain", payload: { ...emptyChain, registryName: chainName } });
export const setNewConnection = (dispatch: Dispatch, newConnection: NewConnection) => {
dispatch({ type: "setNewConnection", payload: newConnection });
};
export const setChainsError = (dispatch: Dispatch, chainsError: string | null) => {
+36 -16
View File
@@ -1,7 +1,7 @@
import { ReactNode, createContext, useContext, useEffect, useReducer } from "react";
import { setChain, setChains, setChainsError } from "./helpers";
import { getChain, useChainFromRegistry, useChainsFromRegistry } from "./service";
import { setChainInStorage, setChainInUrl } from "./storage";
import { emptyChain, isChainInfoFilled, setChain, setChains, setChainsError } from "./helpers";
import { getChain, getNodeFromArray, useChainsFromRegistry } from "./service";
import { addLocalChainInStorage, addRecentChainNameInStorage, setChainInUrl } from "./storage";
import { Action, ChainsContextType, State } from "./types";
const ChainsContext = createContext<ChainsContextType | undefined>(undefined);
@@ -12,10 +12,28 @@ const chainsReducer = (state: State, action: Action) => {
return { ...state, chains: action.payload };
}
case "setChain": {
setChainInStorage(action.payload);
setChainInUrl(action.payload);
if (!isChainInfoFilled(action.payload)) {
return state;
}
if (
!state.chains.mainnets.has(action.payload.registryName) &&
!state.chains.testnets.has(action.payload.registryName)
) {
addLocalChainInStorage(action.payload, state.chains);
}
addRecentChainNameInStorage(action.payload.registryName);
setChainInUrl(action.payload, state.chains);
return { ...state, chain: action.payload };
}
case "addNodeAddress": {
return { ...state, chain: { ...state.chain, nodeAddress: action.payload } };
}
case "setNewConnection": {
return { ...state, newConnection: action.payload };
}
case "setChainsError": {
return { ...state, chainsError: action.payload };
}
@@ -31,27 +49,29 @@ interface ChainsProviderProps {
export const ChainsProvider = ({ children }: ChainsProviderProps) => {
const [state, dispatch] = useReducer(chainsReducer, {
chain: getChain(),
chains: { mainnets: [], testnets: [] },
chain: emptyChain,
chains: { mainnets: new Map(), testnets: new Map(), localnets: new Map() },
newConnection: { action: "edit" },
});
const { chainItems, chainItemsError } = useChainsFromRegistry();
const { chainFromRegistry, chainFromRegistryError } = useChainFromRegistry(
state.chain,
chainItems,
);
useEffect(() => {
setChains(dispatch, chainItems);
setChainsError(dispatch, chainItemsError);
const loadedChain = getChain(chainItems);
setChain(dispatch, loadedChain);
}, [chainItems, chainItemsError]);
useEffect(() => {
if (chainFromRegistry !== state.chain) {
setChain(dispatch, chainFromRegistry);
setChainsError(dispatch, chainFromRegistryError);
}
}, [chainFromRegistry, chainFromRegistryError, state.chain]);
(async function addNodeAddress() {
if (isChainInfoFilled(state.chain) && !state.chain.nodeAddress) {
const nodeAddress = await getNodeFromArray(state.chain.nodeAddresses);
dispatch({ type: "addNodeAddress", payload: nodeAddress });
}
})();
}, [state.chain]);
return <ChainsContext.Provider value={{ state, dispatch }}>{children}</ChainsContext.Provider>;
};
+61 -144
View File
@@ -1,79 +1,79 @@
import { getChainsFromRegistry, getShaFromRegistry } from "@/lib/chainRegistry";
import { StargateClient } from "@cosmjs/stargate";
import { useEffect, useState } from "react";
import { requestJson } from "../../lib/request";
import {
GithubChainRegistryItem,
RegistryAsset,
RegistryChain,
RegistryChainApisRpc,
RegistryChainExplorer,
} from "../../types/chainRegistry";
import { emptyChain, isChainInfoFilled } from "./helpers";
import {
getChainFromEnvfile,
getChainFromStorage,
getChainFromUrl,
setChainInStorage,
setChainInUrl,
getChainsFromStorage,
getRecentChainFromStorage,
getShaFromStorage,
setChainsInStorage,
setShaInStorage,
} from "./storage";
import { ChainInfo, ChainItems } from "./types";
const chainsUrl = "https://api.github.com/repos/cosmos/chain-registry/contents";
const testnetsUrl = "https://api.github.com/repos/cosmos/chain-registry/contents/testnets";
const registryGhUrl = "https://cdn.jsdelivr.net/gh/cosmos/chain-registry/";
const nonChainsFilter = (item: GithubChainRegistryItem) =>
item.type === "dir" && !item.name.startsWith(".") && !item.name.startsWith("_");
import { ChainItems } from "./types";
export const useChainsFromRegistry = () => {
const [chainItems, setChainItems] = useState<ChainItems>({ mainnets: [], testnets: [] });
const [chainItems, setChainItems] = useState<ChainItems>({
mainnets: new Map(),
testnets: new Map(),
localnets: new Map(),
});
const [chainItemsError, setChainItemsError] = useState<string | null>(null);
useEffect(() => {
(async function () {
if (chainItems.mainnets.size && chainItems.testnets.size) {
return;
}
const storedChains = getChainsFromStorage();
try {
const [mainnets, testnets] = await Promise.all([
requestJson(chainsUrl),
requestJson(testnetsUrl),
]);
setChainItems({
mainnets: mainnets.filter(nonChainsFilter),
testnets: testnets.filter(nonChainsFilter),
});
const storedSha = getShaFromStorage();
const registrySha = await getShaFromRegistry();
if (storedSha === registrySha && storedChains.mainnets.size && storedChains.testnets.size) {
setChainItems(storedChains);
return;
}
const registryChains = await getChainsFromRegistry();
const chains: ChainItems = { ...storedChains, ...registryChains };
setChainItems(chains);
if (chains.mainnets.size && chains.testnets.size) {
setChainsInStorage(chains);
setShaInStorage(registrySha);
} else {
setShaInStorage("");
}
} catch (e) {
if (storedChains.mainnets.size && storedChains.testnets.size) {
setChainItems(storedChains);
return;
}
if (e instanceof Error) {
console.error(e.message);
setChainItemsError(e.message);
} else {
setChainItemsError("Failed to get chains from registry");
}
}
})();
}, []);
}, [chainItems.mainnets.size, chainItems.testnets.size]);
return { chainItems, chainItemsError };
};
export const getChainItemFromRegistry = async (chainName: string, isTestnet?: boolean) => {
const chainGhPath = isTestnet ? "testnets/" + chainName : chainName;
const chainGhUrl = registryGhUrl + chainGhPath + "/chain.json";
const chain: RegistryChain = await requestJson(chainGhUrl);
return chain;
};
export const getAssetItemsFromRegistry = async (chainName: string, isTestnet?: boolean) => {
const assetsGhPath = isTestnet ? "testnets/" + chainName : chainName;
const assetsGhUrl = registryGhUrl + assetsGhPath + "/assetlist.json";
const assets: readonly RegistryAsset[] = (await requestJson(assetsGhUrl)).assets;
return assets;
};
const getNodeFromArray = async (nodeArray: readonly RegistryChainApisRpc[]) => {
export const getNodeFromArray = async (nodeArray: readonly string[]) => {
// only return https connections
const secureNodes = nodeArray
.filter(({ address }) => address.startsWith("https://"))
.map(({ address }) => address);
.filter((address) => address.startsWith("https://"))
.map((address) => address);
if (!secureNodes.length) {
throw new Error("No SSL enabled RPC nodes available for this chain");
@@ -91,109 +91,26 @@ const getNodeFromArray = async (nodeArray: readonly RegistryChainApisRpc[]) => {
throw new Error("No RPC nodes available for this chain");
};
const getExplorerFromArray = (explorers: readonly RegistryChainExplorer[]) => {
return explorers[0]?.tx_page ?? "";
};
export const useChainFromRegistry = (chain: ChainInfo, chains: ChainItems) => {
const [chainFromRegistry, setChainFromRegistry] = useState<ChainInfo>(chain);
const [chainFromRegistryError, setChainFromRegistryError] = useState<string | null>(null);
useEffect(() => {
(async function () {
try {
if (isChainInfoFilled(chain) || !chains.mainnets.length || !chains.testnets.length) {
return;
}
const isTestnet = !!chains.testnets.find(({ name }) => name === chain.registryName);
const chainItem = await getChainItemFromRegistry(chain.registryName, isTestnet);
const registryAssets = await getAssetItemsFromRegistry(chain.registryName, isTestnet);
const firstAsset = registryAssets[0];
const nodeAddress = await getNodeFromArray(chainItem.apis.rpc);
const explorerLink = getExplorerFromArray(chainItem.explorers);
const firstAssetDenom = firstAsset.base;
const displayDenom = firstAsset.symbol;
const displayUnit = firstAsset.denom_units.find((u) => u.denom == firstAsset.display);
if (!displayUnit) {
return setChainFromRegistryError(`Unit not found for ${firstAsset.display}`);
}
const feeToken = chainItem.fees.fee_tokens.find(
(token) => token.denom == firstAssetDenom,
) ?? {
denom: firstAssetDenom,
};
const gasPrice =
feeToken.average_gas_price ??
feeToken.low_gas_price ??
feeToken.high_gas_price ??
feeToken.fixed_min_gas_price ??
0.03;
const formattedGasPrice = firstAsset ? `${gasPrice}${firstAssetDenom}` : "";
const newChain: ChainInfo = {
registryName: chain.registryName,
addressPrefix: chainItem.bech32_prefix,
chainId: chainItem.chain_id,
chainDisplayName: chainItem.pretty_name,
nodeAddress,
explorerLink,
denom: firstAssetDenom,
displayDenom,
displayDenomExponent: displayUnit.exponent,
gasPrice: formattedGasPrice,
assets: registryAssets,
};
if (!isChainInfoFilled(newChain)) {
setChainFromRegistryError(
`Chain ${newChain.registryName} loaded from the registry with missing data`,
);
return;
}
setChainFromRegistry(newChain);
} catch (e) {
if (e instanceof Error) {
setChainFromRegistryError(e.message);
} else {
setChainFromRegistryError(`Failed to get chain ${chain.registryName} from registry`);
}
}
})();
}, [chain, chains.mainnets.length, chains.testnets]);
return { chainFromRegistry, chainFromRegistryError };
};
export const getChain = () => {
export const getChain = (chains: ChainItems) => {
if (typeof window === "undefined") return emptyChain;
const rootRoute = location.pathname.split("/")[1];
// Avoid app from thinking the /create and /api routes are registryNames
const chainNameFromUrl = ["create", "api"].includes(rootRoute) ? null : rootRoute;
// Avoid app from thinking the /api route is a registryName
const chainNameFromUrl = rootRoute === "api" ? "" : rootRoute;
const chainFromUrl = getChainFromUrl(chainNameFromUrl);
if (chainFromUrl) {
setChainInStorage(chainFromUrl);
return chainFromUrl;
const recentChain = getRecentChainFromStorage(chains);
if (!chainNameFromUrl && isChainInfoFilled(recentChain)) {
return recentChain;
}
const chainFromStorage = getChainFromStorage(chainNameFromUrl);
if (chainFromStorage) {
setChainInUrl(chainFromStorage);
return chainFromStorage;
}
const urlChain = getChainFromUrl(chainNameFromUrl);
const envfileChain = getChainFromEnvfile(chainNameFromUrl);
const storedChain = getChainFromStorage(
chainNameFromUrl || envfileChain.registryName || "cosmoshub",
chains,
);
const chainFromEnvfile = getChainFromEnvfile(chainNameFromUrl);
if (chainFromEnvfile) {
setChainInStorage(chainFromEnvfile);
setChainInUrl(chainFromEnvfile);
return chainFromEnvfile;
}
const chain = { ...storedChain, ...envfileChain, ...urlChain };
return { ...emptyChain, registryName: chainNameFromUrl || "cosmoshub" };
return isChainInfoFilled(chain) ? chain : emptyChain;
};
+207 -55
View File
@@ -1,72 +1,224 @@
import { isChainInfoFilled } from "./helpers";
import { ChainInfo } from "./types";
import { RegistryAsset } from "@/types/chainRegistry";
import { emptyChain } from "./helpers";
import { ChainInfo, ChainItems, ExplorerLink } from "./types";
const localStorageKey = "context-chain-info";
const registryShaStorageKey = "context-registry-sha";
export const getShaFromStorage = () => localStorage.getItem(registryShaStorageKey);
export const setShaInStorage = (sha: string) => localStorage.setItem(registryShaStorageKey, sha);
export const getChainFromUrl = (chainName: string | null) => {
const params = new URLSearchParams(location.search);
const chainsStorageKey = "context-chain-items";
const chain: ChainInfo = {
registryName: decodeURIComponent(params.get("registryName") || ""),
chainId: decodeURIComponent(params.get("chainId") || ""),
nodeAddress: decodeURIComponent(params.get("nodeAddress") || ""),
denom: decodeURIComponent(params.get("denom") || ""),
displayDenom: decodeURIComponent(params.get("displayDenom") || ""),
displayDenomExponent: Number(decodeURIComponent(params.get("displayDenomExponent") || "")),
assets: JSON.parse(decodeURIComponent(params.get("assets") || "[]")),
gasPrice: decodeURIComponent(params.get("gasPrice") || ""),
chainDisplayName: decodeURIComponent(params.get("chainDisplayName") || ""),
addressPrefix: decodeURIComponent(params.get("addressPrefix") || ""),
explorerLink: decodeURIComponent(params.get("explorerLink") || ""),
export const getChainsFromStorage = () => {
const storedChains = localStorage.getItem(chainsStorageKey);
if (!storedChains) {
const chains: ChainItems = { mainnets: new Map(), testnets: new Map(), localnets: new Map() };
return chains;
}
const { mainnets, testnets, localnets } = JSON.parse(storedChains);
const chains: ChainItems = {
mainnets: new Map(mainnets),
testnets: new Map(testnets),
localnets: new Map(localnets),
};
const isChainNameValid = chain.registryName === chainName || !chainName;
return isChainNameValid && isChainInfoFilled(chain) ? chain : null;
return chains;
};
export const setChainInUrl = (chain: ChainInfo) => {
export const setChainsInStorage = (chains: ChainItems) => {
const arrayMainnets = Array.from(chains.mainnets.entries());
const arrayTestnets = Array.from(chains.testnets.entries());
const arrayLocalnets = Array.from(chains.localnets.entries());
const stringChains = JSON.stringify({
mainnets: arrayMainnets,
testnets: arrayTestnets,
localnets: arrayLocalnets,
});
localStorage.setItem(chainsStorageKey, stringChains);
};
export const addLocalChainInStorage = (chain: ChainInfo, chains: ChainItems) => {
chains.localnets.set(chain.registryName, chain);
setChainsInStorage(chains);
};
export const deleteLocalChainFromStorage = (chainName: string, chains: ChainItems) => {
chains.localnets.delete(chainName);
setChainsInStorage(chains);
};
const recentChainsStorageKey = "context-recent-chains";
export const getRecentChainNamesFromStorage = () => {
const storedNames = localStorage.getItem(recentChainsStorageKey);
if (!storedNames) return [];
const chainNames: readonly string[] = JSON.parse(storedNames);
return chainNames;
};
export const setRecentChainNamesInStorage = (chainNames: readonly string[]) => {
const stringChainNames = JSON.stringify(chainNames);
localStorage.setItem(recentChainsStorageKey, stringChainNames);
};
export const addRecentChainNameInStorage = (chainName: string) => {
const storedNames = getRecentChainNamesFromStorage();
const newChains = storedNames.filter((storedName) => storedName !== chainName);
setRecentChainNamesInStorage([chainName, ...newChains.slice(0, 3)]);
};
export const getRecentChainsFromStorage = (chains: ChainItems) => {
const recentChainNames = getRecentChainNamesFromStorage();
const recentChains = recentChainNames.map((chainName) => {
const chain =
chains.localnets.get(chainName) ??
chains.testnets.get(chainName) ??
chains.mainnets.get(chainName);
return chain ?? null;
});
const nonNullRecentChains: readonly ChainInfo[] = recentChains.filter(
(chain): chain is ChainInfo => chain !== null,
);
return nonNullRecentChains;
};
export const getRecentChainFromStorage = (chains: ChainItems): Partial<ChainInfo> => {
const recentChains = getRecentChainsFromStorage(chains);
const recentChain = recentChains?.[0] ?? {};
return recentChain;
};
export const getChainFromUrl = (chainName: string) => {
if (!chainName) {
return { registryName: chainName };
}
const params = new URLSearchParams(location.search);
const logo = params.get("logo");
const chainId = params.get("chainId");
const chainDisplayName = params.get("chainDisplayName");
const nodeAddresses = params.get("nodeAddresses");
const denom = params.get("denom");
const displayDenom = params.get("displayDenom");
const displayDenomExponent = params.get("displayDenomExponent");
const assets = params.get("assets");
const gasPrice = params.get("gasPrice");
const addressPrefix = params.get("addressPrefix");
const explorerLink = params.get("explorerLink");
const nodeAddressesValue: readonly string[] = JSON.parse(nodeAddresses || "[]");
const assetsValue: readonly RegistryAsset[] = JSON.parse(assets || "[]");
const explorerLinkValue: Partial<ExplorerLink> = JSON.parse(explorerLink || "{}");
const urlChain: Partial<ChainInfo> = {
registryName: chainName,
...(logo && { logo }),
...(chainId && { chainId }),
...(chainDisplayName && { chainDisplayName }),
...(nodeAddressesValue.length && { nodeAddress: "" }),
...(nodeAddressesValue.length && { nodeAddresses: nodeAddressesValue }),
...(denom && { denom }),
...(displayDenom && { displayDenom }),
...(displayDenomExponent && { displayDenomExponent: Number(displayDenomExponent) }),
...(assetsValue.length && { assets: assetsValue }),
...(gasPrice && { gasPrice }),
...(addressPrefix && { addressPrefix }),
...(explorerLink && {
explorerLink: { tx: explorerLinkValue.tx || "", account: explorerLinkValue.account || "" },
}),
};
return urlChain;
};
export const getChainFromEnvfile = (chainName: string) => {
const registryName = process.env.NEXT_PUBLIC_REGISTRY_NAME || "";
if (chainName && registryName !== chainName) {
return { registryName: chainName };
}
const logo = process.env.NEXT_PUBLIC_LOGO;
const chainId = process.env.NEXT_PUBLIC_CHAIN_ID;
const chainDisplayName = process.env.NEXT_PUBLIC_CHAIN_DISPLAY_NAME;
const nodeAddresses = process.env.NEXT_PUBLIC_NODE_ADDRESSES;
const denom = process.env.NEXT_PUBLIC_DENOM;
const displayDenom = process.env.NEXT_PUBLIC_DISPLAY_DENOM;
const displayDenomExponent = process.env.NEXT_PUBLIC_DISPLAY_DENOM_EXPONENT;
const assets = process.env.NEXT_PUBLIC_ASSETS;
const gasPrice = process.env.NEXT_PUBLIC_GAS_PRICE;
const addressPrefix = process.env.NEXT_PUBLIC_ADDRESS_PREFIX;
const explorerLink = process.env.NEXT_PUBLIC_EXPLORER_LINK_TX;
const nodeAddressesValue: readonly string[] = JSON.parse(nodeAddresses || "[]");
const assetsValue: readonly RegistryAsset[] = JSON.parse(assets || "[]");
const explorerLinkValue: Partial<ExplorerLink> = JSON.parse(explorerLink || "{}");
const envfileChain: Partial<ChainInfo> = {
registryName: chainName,
...(logo && { logo }),
...(chainId && { chainId }),
...(chainDisplayName && { chainDisplayName }),
...(nodeAddressesValue.length && { nodeAddress: "" }),
...(nodeAddressesValue.length && { nodeAddresses: nodeAddressesValue }),
...(denom && { denom }),
...(displayDenom && { displayDenom }),
...(displayDenomExponent && { displayDenomExponent: Number(displayDenomExponent) }),
...(assetsValue.length && { assets: assetsValue }),
...(gasPrice && { gasPrice }),
...(addressPrefix && { addressPrefix }),
...(explorerLinkValue && {
explorerLink: { tx: explorerLinkValue.tx || "", account: explorerLinkValue.account || "" },
}),
};
return envfileChain;
};
export const getChainFromStorage = (
chainName: string | null,
{ localnets, testnets, mainnets }: ChainItems,
) => {
if (!chainName) {
return emptyChain;
}
return (
localnets.get(chainName) ?? testnets.get(chainName) ?? mainnets.get(chainName) ?? emptyChain
);
};
export const setChainInUrl = (chain: ChainInfo, chains: ChainItems) => {
const newPathname = location.pathname.includes(chain.registryName)
? location.pathname
: `/${chain.registryName}`;
if (chains.mainnets.has(chain.registryName) || chains.testnets.has(chain.registryName)) {
window.history.replaceState({}, "", newPathname);
return;
}
// Set full url if chain is not on chain-registry repo
const params = new URLSearchParams();
for (const [key, value] of Object.entries(chain)) {
if (typeof value === "object") {
params.set(key, encodeURIComponent(JSON.stringify(value)));
params.set(key, JSON.stringify(value));
} else {
params.set(key, encodeURIComponent(value ?? ""));
params.set(key, value);
}
}
window.history.replaceState({}, "", `${location.pathname}?${params}`);
};
const newUrl = params.size ? `${newPathname}?${params}` : newPathname;
export const getChainFromStorage = (chainName: string | null) => {
const storedChain = localStorage.getItem(localStorageKey);
if (!storedChain) return null;
const chain: ChainInfo = JSON.parse(storedChain);
const isChainNameValid = chain.registryName === chainName || !chainName;
return isChainNameValid && isChainInfoFilled(chain) ? chain : null;
};
export const setChainInStorage = (chain: ChainInfo) => {
const stringChain = JSON.stringify(chain);
localStorage.setItem(localStorageKey, stringChain);
};
export const getChainFromEnvfile = (chainName: string | null) => {
const chain: ChainInfo = {
nodeAddress: process.env.NEXT_PUBLIC_NODE_ADDRESS || "",
denom: process.env.NEXT_PUBLIC_DENOM || "",
displayDenom: process.env.NEXT_PUBLIC_DISPLAY_DENOM || "",
displayDenomExponent: Number(process.env.NEXT_PUBLIC_DISPLAY_DENOM_EXPONENT || 0),
assets: JSON.parse(process.env.NEXT_PUBLIC_ASSETS || "[]"),
gasPrice: process.env.NEXT_PUBLIC_GAS_PRICE || "",
chainId: process.env.NEXT_PUBLIC_CHAIN_ID || "",
chainDisplayName: process.env.NEXT_PUBLIC_CHAIN_DISPLAY_NAME || "",
registryName: process.env.NEXT_PUBLIC_REGISTRY_NAME || "",
addressPrefix: process.env.NEXT_PUBLIC_ADDRESS_PREFIX || "",
explorerLink: process.env.NEXT_PUBLIC_EXPLORER_LINK_TX || "",
};
const isChainNameValid = chain.registryName === chainName || !chainName;
return isChainNameValid && isChainInfoFilled(chain) ? chain : null;
window.history.replaceState({}, "", newUrl);
};
+31 -4
View File
@@ -1,4 +1,4 @@
import { GithubChainRegistryItem, RegistryAsset } from "../../types/chainRegistry";
import { RegistryAsset } from "../../types/chainRegistry";
export interface ChainsContextType {
readonly state: State;
@@ -8,30 +8,49 @@ export interface ChainsContextType {
export interface State {
readonly chains: ChainItems;
readonly chain: ChainInfo;
readonly newConnection: NewConnection;
readonly chainsError?: string | null;
}
export type Dispatch = (action: Action) => void;
export interface ChainItems {
readonly mainnets: readonly GithubChainRegistryItem[];
readonly testnets: readonly GithubChainRegistryItem[];
readonly mainnets: Map<string, ChainInfo>;
readonly testnets: Map<string, ChainInfo>;
readonly localnets: Map<string, ChainInfo>;
}
export interface ChainInfo {
readonly registryName: string;
readonly logo: string;
readonly chainId: string;
readonly chainDisplayName: string;
readonly nodeAddress: string;
readonly nodeAddresses: readonly string[];
readonly denom: string;
readonly displayDenom: string;
readonly displayDenomExponent: number;
readonly assets: readonly RegistryAsset[];
readonly gasPrice: string;
readonly addressPrefix: string;
readonly explorerLink: string;
readonly explorerLink: ExplorerLink;
}
export type ExplorerLink = {
readonly tx: string;
readonly account: string;
};
export type NewConnection =
| {
readonly action: "edit";
readonly chain?: ChainInfo;
}
| {
readonly action: "confirm";
readonly chain: ChainInfo;
};
export type Action =
| {
readonly type: "setChains";
@@ -41,6 +60,14 @@ export type Action =
readonly type: "setChain";
readonly payload: ChainInfo;
}
| {
readonly type: "addNodeAddress";
readonly payload: string;
}
| {
readonly type: "setNewConnection";
readonly payload: NewConnection;
}
| {
readonly type: "setChainsError";
readonly payload: string | null;
+197
View File
@@ -0,0 +1,197 @@
import { isChainInfoFilled } from "@/context/ChainsContext/helpers";
import { ChainInfo, ChainItems, ExplorerLink } from "@/context/ChainsContext/types";
import { GithubChainRegistryItem, RegistryAsset, RegistryChain } from "@/types/chainRegistry";
import { preventUnhandledRejections } from "./promises";
import { requestGhJson } from "./request";
const chainRegistryRepo = "cosmos/chain-registry";
const repoBranch = "master";
const shaUrl = `https://api.github.com/repos/${chainRegistryRepo}/commits/${repoBranch}`;
const mainnetsUrl = `https://api.github.com/repos/${chainRegistryRepo}/contents`;
const testnetsUrl = `https://api.github.com/repos/${chainRegistryRepo}/contents/testnets`;
const registryCdnUrl = `https://cdn.jsdelivr.net/gh/${chainRegistryRepo}@${repoBranch}`;
const getShaFromRegistry = async () => {
const { sha }: { sha: string } = await requestGhJson(shaUrl);
return sha;
};
interface RegistryPromises {
readonly chainInfo: Promise<RegistryChain>;
readonly assetList: Promise<{ readonly assets: readonly RegistryAsset[] }>;
}
const getChainsFromRegistry = async () => {
const chains: ChainItems = { mainnets: new Map(), testnets: new Map(), localnets: new Map() };
const [mainnetGhItems, testnetGhItems]: [
readonly GithubChainRegistryItem[],
readonly GithubChainRegistryItem[],
] = await Promise.all([requestGhJson(mainnetsUrl), requestGhJson(testnetsUrl)]);
const mainnetPromisesMap = new Map<string, RegistryPromises>();
for (const { type, path } of mainnetGhItems) {
if (type !== "dir" || path.startsWith(".") || path.startsWith("_") || path === "testnets") {
continue;
}
mainnetPromisesMap.set(path, {
chainInfo: requestGhJson(`${registryCdnUrl}/${path}/chain.json`),
assetList: requestGhJson(`${registryCdnUrl}/${path}/assetlist.json`),
});
}
const mainnetPromisesArray = [
...Array.from(mainnetPromisesMap.values()).map(({ chainInfo }) => chainInfo),
...Array.from(mainnetPromisesMap.values()).map(({ assetList }) => assetList),
];
preventUnhandledRejections(...mainnetPromisesArray);
await Promise.allSettled(mainnetPromisesArray);
for (const { chainInfo, assetList } of mainnetPromisesMap.values()) {
try {
const registryChain = await chainInfo;
const { assets }: { assets: readonly RegistryAsset[] } = await assetList;
const chain = getChainInfoFromJsons(registryChain, assets);
if (isChainInfoFilled(chain)) {
chains.mainnets.set(chain.registryName, chain);
}
} catch {}
}
const testnetPromisesMap = new Map<string, RegistryPromises>();
for (const { type, path } of testnetGhItems) {
if (type !== "dir" || path.startsWith("testnets/.") || path.startsWith("testnets/_")) {
continue;
}
testnetPromisesMap.set(path, {
chainInfo: requestGhJson(`${registryCdnUrl}/${path}/chain.json`),
assetList: requestGhJson(`${registryCdnUrl}/${path}/assetlist.json`),
});
}
const testnetPromisesArray = [
...Array.from(testnetPromisesMap.values()).map(({ chainInfo }) => chainInfo),
...Array.from(testnetPromisesMap.values()).map(({ assetList }) => assetList),
];
preventUnhandledRejections(...testnetPromisesArray);
await Promise.allSettled(testnetPromisesArray);
for (const { chainInfo, assetList } of testnetPromisesMap.values()) {
try {
const registryChain = await chainInfo;
const { assets }: { assets: readonly RegistryAsset[] } = await assetList;
const chain = getChainInfoFromJsons(registryChain, assets);
if (isChainInfoFilled(chain)) {
chains.testnets.set(chain.registryName, chain);
}
} catch {}
}
return chains;
};
const getParsedCdnLogoUri = (registryUri: string | undefined) => {
if (!registryUri?.includes("github") || !registryUri.includes(chainRegistryRepo)) {
return registryUri;
}
const [, path] = registryUri.split(`${chainRegistryRepo}/${repoBranch}`);
return `${registryCdnUrl}${path}`;
};
const getLogoUri = (
{ logo_URIs: chainUris }: RegistryChain,
{ logo_URIs: firstAssetUris }: RegistryAsset,
) =>
getParsedCdnLogoUri(chainUris?.svg) ||
getParsedCdnLogoUri(chainUris?.png) ||
firstAssetUris?.svg ||
firstAssetUris?.png ||
"";
const getChainInfoFromJsons = (
registryChain: RegistryChain,
registryAssets: readonly RegistryAsset[],
): ChainInfo => {
const cdnRegistryAssets: readonly RegistryAsset[] = registryAssets.map(
({ logo_URIs, ...restProps }) => ({
logo_URIs: logo_URIs
? {
png: getParsedCdnLogoUri(logo_URIs.png) || "",
svg: getParsedCdnLogoUri(logo_URIs.svg) || "",
}
: undefined,
...restProps,
}),
);
const firstAsset = cdnRegistryAssets[0];
const logo = getLogoUri(registryChain, firstAsset);
const nodeAddresses = registryChain.apis?.rpc.map(({ address }) => address) ?? [];
let explorerLink: ExplorerLink = { tx: "", account: "" };
// Prefer same explorer for both tx and account links
for (const explorer of registryChain.explorers ?? []) {
if (explorer.tx_page && explorer.account_page) {
explorerLink = { tx: explorer.tx_page, account: explorer.account_page };
break;
}
if (!explorerLink.tx && explorer.tx_page) {
explorerLink = { ...explorerLink, tx: explorer.tx_page };
}
if (!explorerLink.account && explorer.account_page) {
explorerLink = { ...explorerLink, account: explorer.account_page };
}
}
const firstAssetDenom = firstAsset.base;
const displayUnit = firstAsset.denom_units.find((u) => u.denom == firstAsset.display);
const displayDenom = displayUnit ? firstAsset.symbol : firstAsset.base;
const displayDenomExponent = displayUnit
? displayUnit.exponent
: firstAsset.denom_units[0].exponent;
const feeToken = registryChain.fees?.fee_tokens.find(
(token) => token.denom == firstAssetDenom,
) ?? { denom: firstAssetDenom };
const gasPrice =
feeToken.average_gas_price ??
feeToken.low_gas_price ??
feeToken.high_gas_price ??
feeToken.fixed_min_gas_price ??
0.03;
const formattedGasPrice = firstAsset ? `${gasPrice}${firstAssetDenom}` : "";
const chain: ChainInfo = {
registryName: registryChain.chain_name,
logo,
addressPrefix: registryChain.bech32_prefix,
chainId: registryChain.chain_id,
chainDisplayName: registryChain.pretty_name,
nodeAddresses,
nodeAddress: "",
explorerLink,
denom: firstAssetDenom,
displayDenom,
displayDenomExponent,
gasPrice: formattedGasPrice,
assets: cdnRegistryAssets,
};
return chain;
};
export { getChainsFromRegistry, getShaFromRegistry };
+6 -1
View File
@@ -7,8 +7,13 @@ const testChainInfo: ChainInfo = {
addressPrefix: "juno",
chainId: "uni-6",
chainDisplayName: "Juno Testnet",
logo: "https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/junotestnet/images/juno.svg",
nodeAddress: "https://rpc.uni.junonetwork.io",
explorerLink: "https://testnet.ezstaking.tools/juno-testnet/txs/${txHash}",
nodeAddresses: ["https://rpc.uni.junonetwork.io"],
explorerLink: {
tx: "https://testnet.ezstaking.tools/juno-testnet/txs/${txHash}",
account: "https://testnet.app.ezstaking.io/juno-testnet/account/${accountAddress}",
},
denom: "ujunox",
displayDenom: "JUNOX",
displayDenomExponent: 6,
+2 -2
View File
@@ -162,8 +162,8 @@ const explorerLinkTx = (link: string, hash: string) => {
* for accounts. Returns null otherwise.
*/
const explorerLinkAccount = (link: string, address: string) => {
if (link && link.includes("${address}")) {
return link.replace("${address}", address);
if (link && link.includes("${accountAddress}")) {
return link.replace("${accountAddress}", address);
}
return null;
};
+7
View File
@@ -0,0 +1,7 @@
/*
Prevents unhandled promise rejections from being thrown.
Follows https://jakearchibald.com/2023/unhandled-rejections
*/
export function preventUnhandledRejections(...promises: Promise<unknown>[]) {
for (const promise of promises) promise.catch(() => {});
}
+7
View File
@@ -15,6 +15,13 @@ export const requestJson = async (
return response.ok ? response.json() : Promise.reject(new Error(await response.text()));
};
export const requestGhJson = (endpoint: string, { headers, ...restConfig }: RequestConfig = {}) => {
return requestJson(endpoint, {
...restConfig,
headers: { ...headers, Accept: "application/vnd.github+json" },
});
};
type RequestGraphQlJsonConfig = Omit<RequestInit, "body"> & { body: { query: string } };
/**
+613 -83
View File
@@ -20,32 +20,32 @@
"@keplr-wallet/types": "^0.12.23",
"@ledgerhq/hw-transport-webusb": "^6.27.19",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-aspect-ratio": "^1.0.3",
"@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-context-menu": "^2.1.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-hover-card": "^1.0.6",
"@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-navigation-menu": "^1.1.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-menubar": "^1.0.4",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.4",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^1.2.2",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "^1.1.2",
"@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.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.6",
"@radix-ui/react-tooltip": "^1.0.7",
"@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0",
"@types/node": "20.5.9",
@@ -73,9 +73,9 @@
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.4",
"react": "18.2.0",
"react-day-picker": "^8.8.1",
"react-day-picker": "^8.8.2",
"react-dom": "18.2.0",
"react-hook-form": "^7.46.1",
"react-hook-form": "^7.47.0",
"react-select": "^5.7.4",
"recharts": "^2.8.0",
"tailwind-merge": "^1.14.0",
@@ -1954,15 +1954,15 @@
}
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.4.tgz",
"integrity": "sha512-jbfBCRlKYlhbitueOAv7z74PXYeIQmWpKwm3jllsdkw7fGWNkxqP3v0nY9WmOzcPqpQuoorNtvViBgL46n5gVg==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.5.tgz",
"integrity": "sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dialog": "1.0.4",
"@radix-ui/react-dialog": "1.0.5",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2"
},
@@ -2028,9 +2028,9 @@
}
},
"node_modules/@radix-ui/react-avatar": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.3.tgz",
"integrity": "sha512-9ToF7YNex3Ste45LrAeTlKtONI9yVRt/zOS158iilIkW5K/Apeyb/TUQlcEFTEFvWr8Kzdi2ZYrm1/suiXPajQ==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz",
"integrity": "sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-context": "1.0.1",
@@ -2174,14 +2174,14 @@
}
},
"node_modules/@radix-ui/react-context-menu": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.1.4.tgz",
"integrity": "sha512-HVHLUtZOBiR2Fh5l07qQ9y0IgX4dGZF0S9Gwdk4CVA+DL9afSphvFNa4nRiw6RNgb6quwLV4dLPF/gFDvNaOcQ==",
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.1.5.tgz",
"integrity": "sha512-R5XaDj06Xul1KGb+WP8qiOh7tKJNz2durpLBXAGZjSVtctcRFCuEvy2gtMwRJGePwQQE5nV77gs4FwRi8T+r2g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-menu": "2.0.5",
"@radix-ui/react-menu": "2.0.6",
"@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"
@@ -2202,19 +2202,19 @@
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.4.tgz",
"integrity": "sha512-hJtRy/jPULGQZceSAP2Re6/4NpKo8im6V8P2hUqZsdFiSL8l35kYsw3qbRI6Ay5mQd2+wlLqje770eq+RJ3yZg==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz",
"integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.4",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.3",
"@radix-ui/react-focus-scope": "1.0.4",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-portal": "1.0.3",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
@@ -2237,6 +2237,81 @@
}
}
},
"node_modules/@radix-ui/react-dialog/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-dialog/node_modules/@radix-ui/react-focus-scope": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
"integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"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-dialog/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-direction": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz",
@@ -2282,16 +2357,16 @@
}
},
"node_modules/@radix-ui/react-dropdown-menu": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.5.tgz",
"integrity": "sha512-xdOrZzOTocqqkCkYo8yRPCib5OkTkqN7lqNCdxwPOdE466DOaNl4N8PkUIlsXthQvW5Wwkd+aEmWpfWlBoDPEw==",
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz",
"integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-menu": "2.0.5",
"@radix-ui/react-menu": "2.0.6",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-controllable-state": "1.0.1"
},
@@ -2353,17 +2428,17 @@
}
},
"node_modules/@radix-ui/react-hover-card": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.6.tgz",
"integrity": "sha512-2K3ToJuMk9wjwBOa+jdg2oPma+AmLdcEyTNsG/iC4BDVG3E0/mGCjbY8PEDSLxJcUi+nJi2QII+ec/4kWd88DA==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.7.tgz",
"integrity": "sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.4",
"@radix-ui/react-popper": "1.1.2",
"@radix-ui/react-portal": "1.0.3",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-popper": "1.1.3",
"@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-controllable-state": "1.0.1"
@@ -2383,6 +2458,88 @@
}
}
},
"node_modules/@radix-ui/react-hover-card/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-hover-card/node_modules/@radix-ui/react-popper": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz",
"integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1",
"@radix-ui/react-use-rect": "1.0.1",
"@radix-ui/react-use-size": "1.0.1",
"@radix-ui/rect": "1.0.1"
},
"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-hover-card/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-icons": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz",
@@ -2433,9 +2590,9 @@
}
},
"node_modules/@radix-ui/react-menu": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.5.tgz",
"integrity": "sha512-Gw4f9pwdH+w5w+49k0gLjN0PfRDHvxmAgG16AbyJZ7zhwZ6PBHKtWohvnSwfusfnK3L68dpBREHpVkj8wEM7ZA==",
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz",
"integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
@@ -2443,12 +2600,12 @@
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.4",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.3",
"@radix-ui/react-focus-scope": "1.0.4",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-popper": "1.1.2",
"@radix-ui/react-portal": "1.0.3",
"@radix-ui/react-popper": "1.1.3",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-roving-focus": "1.0.4",
@@ -2472,10 +2629,117 @@
}
}
},
"node_modules/@radix-ui/react-menu/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-menu/node_modules/@radix-ui/react-focus-scope": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
"integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"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-menu/node_modules/@radix-ui/react-popper": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz",
"integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1",
"@radix-ui/react-use-rect": "1.0.1",
"@radix-ui/react-use-size": "1.0.1",
"@radix-ui/rect": "1.0.1"
},
"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-menu/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-menubar": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.0.3.tgz",
"integrity": "sha512-GqjdxzYCjjKhcgEODDP8SrYfbWNh/Hm3lyuFkP5Q5IbX0QfXklLF1o1AqA3oTV2kulUgN/kOZVS92hIIShEgpA==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.0.4.tgz",
"integrity": "sha512-bHgUo9gayKZfaQcWSSLr++LyS0rgh+MvD89DE4fJ6TkGHvjHgPaBZf44hdka7ogOxIOdj9163J+5xL2Dn4qzzg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
@@ -2484,7 +2748,7 @@
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-menu": "2.0.5",
"@radix-ui/react-menu": "2.0.6",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-roving-focus": "1.0.4",
"@radix-ui/react-use-controllable-state": "1.0.1"
@@ -2505,9 +2769,9 @@
}
},
"node_modules/@radix-ui/react-navigation-menu": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.3.tgz",
"integrity": "sha512-x4Uv0N47ABx3/frJazYXxvMpZeKJe0qmRIgQ2o3lhTqnTVg+CaZfVVO4nQLn3QJcDkTz8icElKffhFng47XIBA==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.4.tgz",
"integrity": "sha512-Cc+seCS3PmWmjI51ufGG7zp1cAAIRqHVw7C9LOA2TZ+R4hG6rDvHcTqIsEEFLmZO3zNVH72jOOE7kKNy8W+RtA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
@@ -2515,7 +2779,7 @@
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.4",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
@@ -2540,21 +2804,48 @@
}
}
},
"node_modules/@radix-ui/react-navigation-menu/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-popover": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.6.tgz",
"integrity": "sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz",
"integrity": "sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.4",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.3",
"@radix-ui/react-focus-scope": "1.0.4",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-popper": "1.1.2",
"@radix-ui/react-portal": "1.0.3",
"@radix-ui/react-popper": "1.1.3",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
@@ -2577,6 +2868,113 @@
}
}
},
"node_modules/@radix-ui/react-popover/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-popover/node_modules/@radix-ui/react-focus-scope": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
"integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"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-popover/node_modules/@radix-ui/react-popper": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz",
"integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1",
"@radix-ui/react-use-rect": "1.0.1",
"@radix-ui/react-use-size": "1.0.1",
"@radix-ui/rect": "1.0.1"
},
"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-popover/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-popper": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz",
@@ -2767,9 +3165,9 @@
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.4.tgz",
"integrity": "sha512-OIClwBkwPG+FKvC4OMTRaa/3cfD069nkKFFL/TQzRzaO42Ce5ivKU9VMKgT7UU6UIkjcQqKBrDOIzWtPGw6e6w==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.5.tgz",
"integrity": "sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/number": "1.0.1",
@@ -2974,17 +3372,17 @@
}
},
"node_modules/@radix-ui/react-toast": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.4.tgz",
"integrity": "sha512-wf+fc8DOywrpRK3jlPlWVe+ELYGHdKDaaARJZNuUTWyWYq7+ANCFLp4rTjZ/mcGkJJQ/vZ949Zis9xxEpfq9OA==",
"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.4",
"@radix-ui/react-portal": "1.0.3",
"@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",
@@ -3007,6 +3405,56 @@
}
}
},
"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",
@@ -3033,18 +3481,18 @@
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.6.tgz",
"integrity": "sha512-DmNFOiwEc2UDigsYj6clJENma58OelxD24O4IODoZ+3sQc3Zb+L8w1EP+y9laTuKCLAysPw4fD6/v0j4KNV8rg==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz",
"integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.4",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-popper": "1.1.2",
"@radix-ui/react-portal": "1.0.3",
"@radix-ui/react-popper": "1.1.3",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
@@ -3066,6 +3514,88 @@
}
}
},
"node_modules/@radix-ui/react-tooltip/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-tooltip/node_modules/@radix-ui/react-popper": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz",
"integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1",
"@radix-ui/react-use-rect": "1.0.1",
"@radix-ui/react-use-size": "1.0.1",
"@radix-ui/rect": "1.0.1"
},
"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-tooltip/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-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
@@ -9274,9 +9804,9 @@
}
},
"node_modules/react-day-picker": {
"version": "8.8.1",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.8.1.tgz",
"integrity": "sha512-U7RsRoRI5pyMXhKq54hS9yM11WEGkPf8hIdrxIM/sefgmQjuxazqgwcZFMiPZW/K9vtmzLZFf9bLW0wVsGYd5w==",
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.8.2.tgz",
"integrity": "sha512-sK5M5PNZaLiszmACUKUpVu1eX3eFDVV+WLdWQ3BxTPbEC9jhuawmlgpbSXX5dIIQQwJpZ4wwP5+vsMVOwa1IRw==",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/gpbl"
@@ -9299,9 +9829,9 @@
}
},
"node_modules/react-hook-form": {
"version": "7.46.1",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.46.1.tgz",
"integrity": "sha512-0GfI31LRTBd5tqbXMGXT1Rdsv3rnvy0FjEk8Gn9/4tp6+s77T7DPZuGEpBRXOauL+NhyGT5iaXzdIM2R6F/E+w==",
"version": "7.47.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.47.0.tgz",
"integrity": "sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==",
"engines": {
"node": ">=12.22.0"
},
+14 -14
View File
@@ -24,32 +24,32 @@
"@keplr-wallet/types": "^0.12.23",
"@ledgerhq/hw-transport-webusb": "^6.27.19",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-aspect-ratio": "^1.0.3",
"@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-context-menu": "^2.1.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-hover-card": "^1.0.6",
"@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-navigation-menu": "^1.1.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-menubar": "^1.0.4",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.4",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^1.2.2",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "^1.1.2",
"@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.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.6",
"@radix-ui/react-tooltip": "^1.0.7",
"@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0",
"@types/node": "20.5.9",
@@ -77,9 +77,9 @@
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.4",
"react": "18.2.0",
"react-day-picker": "^8.8.1",
"react-day-picker": "^8.8.2",
"react-dom": "18.2.0",
"react-hook-form": "^7.46.1",
"react-hook-form": "^7.47.0",
"react-select": "^5.7.4",
"recharts": "^2.8.0",
"tailwind-merge": "^1.14.0",
+3 -6
View File
@@ -29,10 +29,7 @@ const Multipage = () => {
const [accountError, setAccountError] = useState(null);
const multisigAddress = router.query.address?.toString();
const explorerHref = explorerLinkAccount(
process.env.NEXT_PUBLIC_EXPLORER_LINK_ACCOUNT || "",
multisigAddress || "",
);
const explorerLink = explorerLinkAccount(chain.explorerLink.account, multisigAddress || "");
const fetchMultisig = useCallback(
async (address: string) => {
@@ -65,12 +62,12 @@ const Multipage = () => {
}, [fetchMultisig, multisigAddress]);
return (
<Page goBack={{ pathname: "/", title: "home" }}>
<Page goBack={{ pathname: `/${chain.registryName}`, title: "home" }}>
<StackableContainer base>
<StackableContainer>
<label>Multisig Address</label>
<h1>{multisigAddress ? <HashView hash={multisigAddress} /> : "No Address"}</h1>
{explorerHref ? <Button href={explorerHref} label="View in Explorer"></Button> : null}
{explorerLink ? <Button href={explorerLink} label="View in Explorer"></Button> : null}
</StackableContainer>
{pubkey ? (
<MultisigMembers
+13
View File
@@ -0,0 +1,13 @@
import AccountView from "@/components/dataViews/AccountView";
import Page from "@/components/layout/Page";
import { useChains } from "@/context/ChainsContext";
export default function AccountPage() {
const { chain } = useChains();
return (
<Page goBack={{ pathname: `/${chain.registryName}`, title: "home" }}>
<AccountView />
</Page>
);
}
+19
View File
@@ -0,0 +1,19 @@
import MultisigForm from "@/components/forms/MultisigForm";
import Page from "@/components/layout/Page";
import StackableContainer from "@/components/layout/StackableContainer";
import { useChains } from "@/context/ChainsContext";
export default function CreatePage() {
const { chain } = useChains();
return (
<Page goBack={{ pathname: `/${chain.registryName}`, title: "home", needsConfirm: true }}>
<StackableContainer base>
<StackableContainer lessPadding>
<h1 className="title">Create Legacy Multisig</h1>
</StackableContainer>
<MultisigForm />
</StackableContainer>
</Page>
);
}
+23
View File
@@ -0,0 +1,23 @@
import FindMultisigForm from "@/components/forms/FindMultisigForm";
import Page from "@/components/layout/Page";
import StackableContainer from "@/components/layout/StackableContainer";
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>
);
};
export default MultiPage;
+8 -7
View File
@@ -1,20 +1,21 @@
import Header from "@/components/Header";
import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import ThemeProvider from "@/context/ThemesContext";
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import ChainSelect from "../components/chainSelect/ChainSelect";
import { ChainsProvider } from "../context/ChainsContext";
function MultisigApp({ Component, pageProps }: AppProps) {
const showChainSelect = process.env.NEXT_PUBLIC_MULTICHAIN?.toLowerCase() === "true";
export default function MultisigApp({ Component, pageProps }: AppProps) {
return (
<ChainsProvider>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
{showChainSelect && <ChainSelect />}
<Component {...pageProps} />
<Toaster />
<TooltipProvider>
<Header />
<Component {...pageProps} />
<Toaster />
</TooltipProvider>
</ThemeProvider>
</ChainsProvider>
);
}
export default MultisigApp;
-16
View File
@@ -1,16 +0,0 @@
import MultisigForm from "../components/forms/MultisigForm";
import Page from "../components/layout/Page";
import StackableContainer from "../components/layout/StackableContainer";
const CreatePage = () => (
<Page goBack={{ pathname: "/", title: "home", needsConfirm: true }}>
<StackableContainer base>
<StackableContainer lessPadding>
<h1 className="title">Create Legacy Multisig</h1>
</StackableContainer>
<MultisigForm />
</StackableContainer>
</Page>
);
export default CreatePage;
-283
View File
@@ -1,283 +0,0 @@
import { CalendarDateRangePicker } from "@/components/dashboard/date-range-picker";
import { MainNav } from "@/components/dashboard/main-nav";
import { Overview } from "@/components/dashboard/overview";
import { RecentSales } from "@/components/dashboard/recent-sales";
import { Search } from "@/components/dashboard/search";
import TeamSwitcher from "@/components/dashboard/team-switcher";
import { UserNav } from "@/components/dashboard/user-nav";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ToastAction } from "@/components/ui/toast";
import { useToast } from "@/components/ui/use-toast";
import { Calculator, Calendar, CreditCard, Settings, Smile, User } from "lucide-react";
import { Metadata } from "next";
import Image from "next/image";
export const metadata: Metadata = {
title: "Dashboard",
description: "Example dashboard app built using the components.",
};
export default function DashboardPage() {
const { toast } = useToast();
return (
<>
<div className="md:hidden">
<Image
src="/examples/dashboard-light.png"
width={1280}
height={866}
alt="Dashboard"
className="block dark:hidden"
/>
<Image
src="/examples/dashboard-dark.png"
width={1280}
height={866}
alt="Dashboard"
className="hidden dark:block"
/>
</div>
<div className="hidden flex-col md:flex" style={{ marginTop: "120px" }}>
<div className="border-b">
<div className="flex h-16 items-center px-4">
<TeamSwitcher />
<MainNav className="mx-6" />
<div className="ml-auto flex items-center space-x-4">
<Search />
<UserNav />
</div>
</div>
</div>
<div className="flex-1 space-y-4 p-8 pt-6">
<div className="flex items-center justify-between space-y-2">
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
<div className="flex items-center space-x-2">
<CalendarDateRangePicker />
<Button>Download</Button>
</div>
</div>
<Tabs defaultValue="overview" className="space-y-4">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics" disabled>
Analytics
</TabsTrigger>
<TabsTrigger value="reports" disabled>
Reports
</TabsTrigger>
<TabsTrigger value="notifications" disabled>
Notifications
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$45,231.89</div>
<p className="text-xs text-muted-foreground">+20.1% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Subscriptions</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+2350</div>
<p className="text-xs text-muted-foreground">+180.1% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Sales</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<rect width="20" height="14" x="2" y="5" rx="2" />
<path d="M2 10h20" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+12,234</div>
<p className="text-xs text-muted-foreground">+19% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Now</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+573</div>
<p className="text-xs text-muted-foreground">+201 since last hour</p>
</CardContent>
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Overview</CardTitle>
</CardHeader>
<CardContent className="pl-2">
<Overview />
</CardContent>
</Card>
<Card className="col-span-3">
<CardHeader>
<CardTitle>Recent Sales</CardTitle>
<CardDescription>You made 265 sales this month.</CardDescription>
</CardHeader>
<CardContent>
<RecentSales />
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
</div>
<Command className="rounded-lg border shadow-md">
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>
<Calendar className="mr-2 h-4 w-4" />
<span>Calendar</span>
</CommandItem>
<CommandItem>
<Smile className="mr-2 h-4 w-4" />
<span>Search Emoji</span>
</CommandItem>
<CommandItem>
<Calculator className="mr-2 h-4 w-4" />
<span>Calculator</span>
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem>
<User className="mr-2 h-4 w-4" />
<span>Profile</span>
<CommandShortcut>P</CommandShortcut>
</CommandItem>
<CommandItem>
<CreditCard className="mr-2 h-4 w-4" />
<span>Billing</span>
<CommandShortcut>B</CommandShortcut>
</CommandItem>
<CommandItem>
<Settings className="mr-2 h-4 w-4" />
<span>Settings</span>
<CommandShortcut>S</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
<Button
variant="outline"
className="mt-4"
onClick={() => {
toast({
title: "Scheduled: Catch up ",
description: "Friday, February 10, 2023 at 5:57 PM",
duration: 0,
action: <ToastAction altText="Goto schedule to undo">Undo</ToastAction>,
});
}}
>
Add to calendar
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" className="mt-4">
Show Dialog
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your account and remove
your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}
+34 -13
View File
@@ -1,23 +1,44 @@
import FindMultisigForm from "../components/forms/FindMultisigForm";
import Page from "../components/layout/Page";
import StackableContainer from "../components/layout/StackableContainer";
import Page from "@/components/layout/Page";
import StackableContainer from "@/components/layout/StackableContainer";
import { Skeleton } from "@/components/ui/skeleton";
import { isChainInfoFilled } from "@/context/ChainsContext/helpers";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useChains } from "../context/ChainsContext";
const MultiPage = () => {
export default function MultiPage() {
const router = useRouter();
const { chain } = useChains();
useEffect(() => {
if (isChainInfoFilled(chain)) {
router.replace(`/${chain.registryName}`);
}
}, [chain, router]);
return (
<Page>
<StackableContainer base>
<StackableContainer lessPadding>
<h1 className="title">
<span>{chain.chainDisplayName}</span> Multisig Manager
</h1>
</StackableContainer>
<FindMultisigForm />
<div className="space-y-10">
<StackableContainer>
<Skeleton className="h-4 w-[250px]" />
</StackableContainer>
<div className="space-y-8">
<StackableContainer>
<div className="space-y-2">
<Skeleton className="h-4 w-[350px]" />
<Skeleton className="h-4 w-[300px]" />
</div>
</StackableContainer>
<StackableContainer>
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[280px]" />
</div>
</StackableContainer>
</div>
</div>
</StackableContainer>
</Page>
);
};
export default MultiPage;
}
+30
View File
@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 42 42">
<g clip-path="url(#a)">
<path fill="url(#b)" d="M32.455 0H9.545A9.545 9.545 0 0 0 0 9.545v22.91A9.545 9.545 0 0 0 9.545 42h22.91A9.545 9.545 0 0 0 42 32.455V9.545A9.545 9.545 0 0 0 32.455 0Z"/>
<path fill="url(#c)" d="M32.455 0H9.545A9.545 9.545 0 0 0 0 9.545v22.91A9.545 9.545 0 0 0 9.545 42h22.91A9.545 9.545 0 0 0 42 32.455V9.545A9.545 9.545 0 0 0 32.455 0Z"/>
<path fill="url(#d)" d="M32.455 0H9.545A9.545 9.545 0 0 0 0 9.545v22.91A9.545 9.545 0 0 0 9.545 42h22.91A9.545 9.545 0 0 0 42 32.455V9.545A9.545 9.545 0 0 0 32.455 0Z"/>
<path fill="url(#e)" d="M32.455 0H9.545A9.545 9.545 0 0 0 0 9.545v22.91A9.545 9.545 0 0 0 9.545 42h22.91A9.545 9.545 0 0 0 42 32.455V9.545A9.545 9.545 0 0 0 32.455 0Z"/>
<path fill="#fff" d="M17.253 32.261V22.52l9.465 9.742h5.267v-.253L21.096 20.912l10.05-10.526v-.125h-5.3l-8.593 9.303V10.26h-4.268v22h4.268Z"/>
</g>
<defs>
<radialGradient id="c" cx="0" cy="0" r="1" gradientTransform="matrix(47.49745 -47.75613 48.47062 48.20806 2.006 40.409)" gradientUnits="userSpaceOnUse">
<stop stop-color="#232DE3"/>
<stop offset="1" stop-color="#232DE3" stop-opacity="0"/>
</radialGradient>
<radialGradient id="d" cx="0" cy="0" r="1" gradientTransform="rotate(-138.45 27.79 13.343) scale(42.1137 64.2116)" gradientUnits="userSpaceOnUse">
<stop stop-color="#8B4DFF"/>
<stop offset="1" stop-color="#8B4DFF" stop-opacity="0"/>
</radialGradient>
<radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="matrix(0 33.1135 -80.3423 0 20.65 .311)" gradientUnits="userSpaceOnUse">
<stop stop-color="#24D5FF"/>
<stop offset="1" stop-color="#1BB8FF" stop-opacity="0"/>
</radialGradient>
<linearGradient id="b" x1="21" x2="21" y1="0" y2="42" gradientUnits="userSpaceOnUse">
<stop stop-color="#1FD1FF"/>
<stop offset="1" stop-color="#1BB8FF"/>
</linearGradient>
<clipPath id="a">
<path fill="#fff" d="M0 0h42v42H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 768.91 669.35">
<path d="M0 479.29v190.06h289.22V627.2H42.14V479.29H0zm726.77 0V627.2H479.69v42.14h289.22V479.29h-42.14zM289.64 190.06v289.22h190.05v-38.01H331.78V190.06h-42.14zM0 0v190.06h42.14V42.14h247.08V0H0zm479.69 0v42.14h247.08v147.92h42.14V0H479.69z"/>
</svg>

After

Width:  |  Height:  |  Size: 344 B

+1 -1
View File
@@ -57,7 +57,7 @@
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
+23 -4
View File
@@ -15,6 +15,19 @@ export interface GithubChainRegistryItem {
};
}
export interface GithubTreeItem {
readonly path: string;
readonly mode: string;
readonly type: string;
readonly sha: string;
readonly size: number;
readonly url: string;
}
export interface GithubTreesResponse {
readonly tree: readonly GithubTreeItem[];
}
export interface RegistryChainApisRpc {
readonly address: string;
readonly provider: string;
@@ -27,7 +40,8 @@ export interface RegistryChainApis {
export interface RegistryChainExplorer {
readonly kind: string;
readonly url: string;
readonly tx_page: string;
readonly tx_page?: string;
readonly account_page?: string;
}
export interface RegistryChainFeeTokens {
@@ -43,12 +57,17 @@ export interface RegistryChainFees {
}
export interface RegistryChain {
readonly apis: RegistryChainApis;
readonly apis?: RegistryChainApis;
readonly bech32_prefix: string;
readonly chain_id: string;
readonly explorers: readonly RegistryChainExplorer[];
readonly fees: RegistryChainFees;
readonly chain_name: string;
readonly explorers?: readonly RegistryChainExplorer[];
readonly fees?: RegistryChainFees;
readonly pretty_name: string;
readonly logo_URIs?: {
readonly png?: string;
readonly svg?: string;
};
}
/**
+15
View File
@@ -0,0 +1,15 @@
export type SigningStatus = "not_signed" | "not_a_member" | "signed";
export type WalletType = "Keplr" | "Ledger";
export interface WalletInfo {
readonly type: WalletType;
readonly address: string;
readonly pubKey: string;
}
export interface LoadingStates {
readonly signing?: boolean;
readonly keplr?: boolean;
readonly ledger?: boolean;
}