Merge pull request #181 from cosmos/feat/new-chainselect
Add new ChainConnect
This commit is contained in:
+1
-12
@@ -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
@@ -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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Shadcn ui components
|
||||
/components/ui/
|
||||
@@ -0,0 +1,2 @@
|
||||
# Shadcn ui components
|
||||
/components/ui/
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -39,6 +39,7 @@ const Input = (props: InputProps) => (
|
||||
}
|
||||
|
||||
label {
|
||||
color: white;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
margin-bottom: ${props.type === "checkbox" ? 0 : "1em"};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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<
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
/* eslint-disable no-shadow */
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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(() => {});
|
||||
}
|
||||
@@ -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 } };
|
||||
|
||||
/**
|
||||
|
||||
Generated
+613
-83
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user