Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new ChainConnect #181

Merged
merged 35 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
22733d6
Update component ui deps
abefernan Oct 3, 2023
baf72bd
Ignore ui components for eslint and prettier
abefernan Oct 3, 2023
1e0a14c
Tweak components that would be changed
abefernan Oct 3, 2023
f8f51a1
Update components, fix Portals
abefernan Oct 3, 2023
5f079b1
Simplify sample envfiles
abefernan Nov 20, 2023
33ed6b4
Add and tweak Github registry types
abefernan Nov 20, 2023
3777305
Tweak destructive variant color
abefernan Nov 20, 2023
36e8521
Add requestGhJson helper
abefernan Nov 20, 2023
f174510
Rework ChainsContext
abefernan Nov 20, 2023
cfc9d4d
Add fancy ButtonWithConfirm
abefernan Nov 20, 2023
d83e773
Add chain-registry parse and query helpers
abefernan Nov 20, 2023
80d264b
Tweak card color
abefernan Nov 20, 2023
a75b379
Add new ChainConnect
abefernan Nov 20, 2023
9fce95d
Remove old ChainSelect
abefernan Nov 20, 2023
854d877
Replace old ChainSelect with new Header
abefernan Nov 20, 2023
16ea529
Add explorerAccountLink
abefernan Dec 1, 2023
f1d368a
Extract signing types
abefernan Dec 1, 2023
ca85ce1
Use new explorer link
abefernan Dec 1, 2023
1461f60
Redirect from / to /chainName
abefernan Dec 1, 2023
7d62983
Redirect from multipage index to chainName. Add Skeleton
abefernan Dec 1, 2023
a45f060
Move /create to /chainName/create
abefernan Dec 1, 2023
48b3759
Move multisig index to /chainName, tweak links
abefernan Dec 1, 2023
814bdce
Add AccountView component and Page
abefernan Dec 1, 2023
3760698
Fix navigation flash by using Next Link
abefernan Dec 1, 2023
cd9e8cd
Tweak button skeleton
abefernan Dec 1, 2023
be9a3bb
Add tooltip provider
abefernan Dec 1, 2023
191a8e6
Style toasts
abefernan Dec 1, 2023
c343b67
Add BadgeWithCopy component
abefernan Dec 1, 2023
258111c
Add account view link to header
abefernan Dec 1, 2023
56b096e
Add wallet icons
abefernan Dec 1, 2023
8c61aa1
Remove unused test page
abefernan Dec 1, 2023
9f97cec
Tweak getChainsFromRegistry
abefernan Dec 12, 2023
549076e
Merge branch 'feat/new-chainselect' into feat/my-account
abefernan Dec 12, 2023
527b969
Public key wording
abefernan Dec 12, 2023
dbda28d
Merge pull request #182 from cosmos/feat/my-account
webmaster128 Dec 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -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
13 changes: 1 addition & 12 deletions .env.test.sample
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
FAUNADB_SECRET=
FAUNADB_URL=https://graphql.eu.fauna.com/graphql
NEXT_PUBLIC_NODE_ADDRESS=https://rpc.uni.junonetwork.io:443
NEXT_PUBLIC_DENOM=ujunox
NEXT_PUBLIC_DISPLAY_DENOM=JUNOX
NEXT_PUBLIC_DISPLAY_DENOM_EXPONENT=6
NEXT_PUBLIC_ASSETS=[{"description":"The native token of JUNO Chain","denom_units":[{"denom":"ujunox","exponent":0},{"denom":"junox","exponent":6}],"base":"ujunox","name":"Juno Testnet","display":"junox","symbol":"JUNOX","logo_URIs":{"png":"https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/junotestnet/images/juno.png","svg":"https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/junotestnet/images/juno.svg"},"coingecko_id":"juno-network"}]
NEXT_PUBLIC_GAS_PRICE=0.04ujunox
NEXT_PUBLIC_CHAIN_ID=uni-6
NEXT_PUBLIC_ADDRESS_PREFIX=juno
NEXT_PUBLIC_REGISTRY_NAME=junotestnet
NEXT_PUBLIC_EXPLORER_LINK_TX="https://testnet.mintscan.io/juno-testnet/txs/\${txHash}"
NEXT_PUBLIC_EXPLORER_LINK_ACCOUNT="https://testnet.mintscan.io/juno-testnet/account/\${address}"
NEXT_PUBLIC_CHAIN_DISPLAY_NAME="Juno Testnet"
NEXT_PUBLIC_MULTICHAIN=true
NEXT_PUBLIC_REGISTRY_NAME=junotestnet
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Shadcn ui components
/components/ui/
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Shadcn ui components
/components/ui/
104 changes: 104 additions & 0 deletions components/ChainConnect/ChainDigest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useChains } from "@/context/ChainsContext";
import { deleteLocalChainFromStorage } from "@/context/ChainsContext/storage";
import { ChainInfo } from "@/context/ChainsContext/types";
import { CheckCircle, ChevronsUpDown, ExternalLink } from "lucide-react";
import Link from "next/link";
import ButtonWithConfirm from "../inputs/ButtonWithConfirm";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible";

interface ChainItemProps {
readonly chain: ChainInfo;
readonly simplify?: boolean;
}

export default function ChainDigest({ chain, simplify }: ChainItemProps) {
const { chain: connectedChain, chains } = useChains();

return (
<div className="space-y-1">
<div className="column flex flex-wrap items-center gap-2">
<Avatar className="overflow-visible">
{!simplify && connectedChain.registryName === chain.registryName ? (
<CheckCircle
style={{
position: "absolute",
top: "-6px",
left: "-6px",
border: "2px solid rgb(134 239 172 / var(--tw-bg-opacity))",
borderRadius: "50%",
width: "20px",
height: "20px",
background: "rgb(134 239 172 / var(--tw-bg-opacity))",
color: "rgb(20 83 45 / var(--tw-text-opacity))",
}}
/>
) : null}
<AvatarImage src={chain.logo} alt={`${chain.chainDisplayName} logo`} className="h-auto" />
<AvatarFallback>{chain.registryName.slice(0, 1).toUpperCase()}</AvatarFallback>
</Avatar>
<h4 className="text-sm font-semibold">{chain.chainDisplayName}</h4>
<Badge>{chain.chainId}</Badge>
</div>
<h4 className="text-sm font-semibold">Fee token: {chain.displayDenom}</h4>
<Collapsible className="w-[350px] space-y-2">
<div className="flex items-center space-x-4">
{!simplify && chain.nodeAddresses.length > 1 ? (
<CollapsibleTrigger asChild>
<Button size="sm" className="p-1">
<ChevronsUpDown className="black h-4 w-4" />
<span className="sr-only">Toggle</span>
<h4 className="text-sm font-semibold">RPC endpoints:</h4>
</Button>
</CollapsibleTrigger>
) : (
<h4 className="text-sm font-semibold">RPC endpoint:</h4>
)}
</div>
<div className="rounded-md border px-2 py-1 font-mono text-sm">
{chain.nodeAddress || chain.nodeAddresses[0]}
</div>
<CollapsibleContent className="space-y-2">
{chain.nodeAddresses
.filter(
(address, _, nodeAddresses) =>
(chain.nodeAddress && address !== chain.nodeAddress) ||
(!chain.nodeAddress && address !== nodeAddresses[0]),
)
.map((address) => (
<div key={address} className="rounded-md border px-2 py-1 font-mono text-sm">
{address}
</div>
))}
</CollapsibleContent>
</Collapsible>
{!simplify && chains.localnets.has(chain.registryName) ? (
<div className="flex justify-center pt-2">
<ButtonWithConfirm
onClick={() => {
deleteLocalChainFromStorage(chain.registryName, chains);
}}
text="Delete custom chain"
confirmText="Confirm deletion?"
disabled={connectedChain.registryName === chain.registryName}
/>
</div>
) : !simplify ? (
<div className="flex items-center pt-2 hover:cursor-pointer">
<ExternalLink className="mr-2 h-4 w-4" />
<Link
href={`https://github.com/cosmos/chain-registry/tree/master/${
chains.testnets.has(chain.registryName) ? `testnets/` : ""
}${chain.registryName}`}
target="_blank"
className="text-xs underline"
>
Check it out on the registry
</Link>
</div>
) : null}
</div>
);
}
72 changes: 72 additions & 0 deletions components/ChainConnect/ChainItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useChains } from "@/context/ChainsContext";
import { setNewConnection } from "@/context/ChainsContext/helpers";
import { ChainInfo } from "@/context/ChainsContext/types";
import { cn } from "@/lib/utils";
import { CheckCircle } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { CommandItem } from "../ui/command";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "../ui/hover-card";
import ChainDigest from "./ChainDigest";

interface ChainItemProps {
readonly chain: ChainInfo;
readonly hoverCardElementBoundary: HTMLDivElement | null;
}

export default function ChainItem({ chain, hoverCardElementBoundary }: ChainItemProps) {
const { chain: connectedChain, chainsDispatch } = useChains();

return (
<HoverCard key={chain.registryName} openDelay={300}>
<HoverCardTrigger asChild>
<CommandItem
value={chain.registryName}
onSelect={
connectedChain.registryName === chain.registryName
? () => {}
: () => {
setNewConnection(chainsDispatch, { action: "confirm", chain });
}
}
className={cn(
"group relative inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border-2 border-white p-2.5 text-sm font-medium text-white focus:outline-none focus:ring-4 focus:ring-red-100 aria-selected:text-gray-900",
connectedChain.registryName === chain.registryName
? "cursor-not-allowed border-green-600 bg-green-300 text-green-900 aria-selected:bg-green-300"
: "transparent cursor-pointer border-white",
)}
>
{connectedChain.registryName === chain.registryName ? (
<CheckCircle
style={{
position: "absolute",
top: "-6px",
left: "-6px",
border: "2px solid rgb(134 239 172 / var(--tw-bg-opacity))",
borderRadius: "50%",
width: "25px",
height: "25px",
background: "rgb(134 239 172 / var(--tw-bg-opacity))",
color: "rgb(20 83 45 / var(--tw-text-opacity))",
}}
/>
) : null}
<Avatar>
<AvatarImage
src={chain.logo}
alt={`${chain.chainDisplayName} logo`}
className="h-auto"
/>
<AvatarFallback>{chain.registryName.slice(0, 1).toUpperCase()}</AvatarFallback>
</Avatar>
{chain.registryName}
</CommandItem>
</HoverCardTrigger>
<HoverCardContent
className="w-auto bg-fuchsia-900"
collisionBoundary={hoverCardElementBoundary}
>
<ChainDigest chain={chain} />
</HoverCardContent>
</HoverCard>
);
}
35 changes: 35 additions & 0 deletions components/ChainConnect/ChainsGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ChainInfo } from "@/context/ChainsContext/types";
import { CommandGroup } from "../ui/command";
import ChainItem from "./ChainItem";
import { useRef } from "react";

interface ChainsGroupProps {
readonly chains: readonly ChainInfo[];
readonly heading: string;
readonly emptyMsg: string;
}

export default function ChainsGroup({ chains, heading, emptyMsg }: ChainsGroupProps) {
const containerRef = useRef<HTMLDivElement>(null);

return (
<CommandGroup
heading={heading}
className="[&_[cmdk-group-heading]]:my-3 [&_[cmdk-group-heading]]:text-base [&_[cmdk-group-heading]]:leading-[0] [&_[cmdk-group-heading]]:text-white [&_[cmdk-group-heading]]:underline"
>
{chains.length ? (
<div ref={containerRef} className="flex flex-wrap gap-2">
{chains.map((chain) => (
<ChainItem
key={chain.registryName}
chain={chain}
hoverCardElementBoundary={containerRef.current}
/>
))}
</div>
) : (
<p className="ml-3 max-w-none">{emptyMsg}</p>
)}
</CommandGroup>
);
}
58 changes: 58 additions & 0 deletions components/ChainConnect/ChooseChain.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
61 changes: 61 additions & 0 deletions components/ChainConnect/ConfirmConnection.tsx
Original file line number Diff line number Diff line change
@@ -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>
</>
);
}
Loading