Merge pull request #207 from cosmos/feat/new-account-view

New account view
This commit is contained in:
Abel Fernández
2024-04-15 16:33:38 +02:00
committed by GitHub
12 changed files with 311 additions and 147 deletions
+1
View File
@@ -17,6 +17,7 @@
# misc
.DS_Store
.vscode
*.pem
# debug
@@ -1,29 +0,0 @@
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>
);
}
@@ -1,49 +0,0 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toastError } from "@/lib/utils";
import { Coin } from "@cosmjs/amino";
import { StargateClient } from "@cosmjs/stargate";
import { useEffect, useState } from "react";
import { useChains } from "../../../context/ChainsContext";
import BalancePill from "./BalancePill";
interface BalancesListProps {
readonly walletAddress: string;
}
export default function BalancesList({ walletAddress }: 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) {
console.error("Failed to get balances:", e);
toastError({
description: "Failed to get balances",
fullError: e instanceof Error ? e : undefined,
});
}
})();
}, [chain.nodeAddress, 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,66 @@
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
import { printableCoin, thinSpace } from "@/lib/displayHelpers";
import { toastError } from "@/lib/utils";
import { Coin } from "@cosmjs/amino";
import { StargateClient } from "@cosmjs/stargate";
import { useEffect, useState } from "react";
import { useChains } from "../../../context/ChainsContext";
import { Avatar, AvatarFallback, AvatarImage } from "../../ui/avatar";
interface BalancesTableProps {
readonly walletAddress: string;
}
export default function BalancesTable({ walletAddress }: BalancesTableProps) {
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) {
console.error("Failed to get balances:", e);
toastError({
description: "Failed to get balances",
fullError: e instanceof Error ? e : undefined,
});
}
})();
}, [chain.nodeAddress, walletAddress]);
return balances.length ? (
<Table>
<TableBody>
{balances.map((coin) => {
const foundAsset = chain.assets.find((asset) => asset.base === coin.denom);
const logo = foundAsset?.logo_URIs?.svg || foundAsset?.logo_URIs?.png || "";
const [macroAmount, macroDenom] = printableCoin(coin, chain).split(thinSpace);
return (
<TableRow key={coin.denom}>
<TableCell className="w-0 pr-0">
<Avatar>
<AvatarImage src={logo} alt={`${coin.denom} logo`} className="h-auto" />
<AvatarFallback className="text-white">
{coin.denom.slice(1, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
</TableCell>
<TableCell>{macroDenom}</TableCell>
<TableCell className="text-right">{macroAmount}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
) : (
"No tokens found"
);
}
@@ -1,10 +1,10 @@
import { toastError } from "@/lib/utils";
import { cn, toastError } from "@/lib/utils";
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 { Loader2, Unplug } from "lucide-react";
import Image from "next/image";
import { Dispatch, SetStateAction, useCallback, useLayoutEffect, useState } from "react";
import { useChains } from "../../../context/ChainsContext";
@@ -108,17 +108,27 @@ export default function ButtonConnectWallet({
const isLoading =
(walletType === "Keplr" && loading.keplr) || (walletType === "Ledger" && loading.ledger);
return (
<Button onClick={onClick} disabled={loading.keplr || loading.ledger}>
return walletInfo?.type === walletType ? (
<Button
variant="outline"
onClick={() => {
setWalletInfo(null);
}}
>
<Unplug className="mr-2 h-auto w-5 text-destructive" />
Disconnect {walletInfo.type}
</Button>
) : (
<Button onClick={onClick} disabled={loading.keplr || loading.ledger} variant="outline">
{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"
width={walletType === "Ledger" ? 23 : 20}
height={walletType === "Ledger" ? 23 : 20}
className={cn("mr-2", walletType === "Ledger" && "bg-white p-0.5")}
/>
)}
Connect {walletType}
+74 -49
View File
@@ -1,80 +1,105 @@
import BadgeWithCopy from "@/components/BadgeWithCopy";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { explorerLinkAccount } from "@/lib/displayHelpers";
import { cn } from "@/lib/utils";
import { WalletInfo } from "@/types/signing";
import { Unplug } from "lucide-react";
import copy from "copy-to-clipboard";
import { ArrowUpRightSquare, Copy } from "lucide-react";
import Image from "next/image";
import { useState } from "react";
import { toast } from "sonner";
import { useChains } from "../../../context/ChainsContext";
import { Button } from "../../ui/button";
import BalancesList from "./BalancesList";
import BalancesTable from "./BalancesTable";
import ButtonConnectWallet from "./ButtonConnectWallet";
export default function AccountView() {
const { chain } = useChains();
const walletInfoState = useState<WalletInfo | null>();
const [walletInfo, setWalletInfo] = walletInfoState;
const [walletInfo] = walletInfoState;
const explorerLink =
explorerLinkAccount(chain.explorerLinks.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">
<Card>
<CardHeader className="space-y-1">
<CardTitle className="flex items-center text-2xl">
{walletInfo?.type ? (
<Image
alt=""
src={`/assets/icons/${walletInfo.type.toLowerCase()}.svg`}
width={20}
height={20}
width={walletInfo.type === "Ledger" ? 30 : 27}
height={walletInfo.type === "Ledger" ? 30 : 27}
className={cn("mr-2", walletInfo.type === "Ledger" && "bg-white p-0.5")}
/>
) : null}
{walletInfo?.type ? `${walletInfo.type} wallet connected` : "Connect wallet"}
{walletInfo?.type ? `Connected to ${walletInfo.type}` : "Connect to a wallet"}
</CardTitle>
<CardDescription>Choose between Keplr or Ledger to show its account info</CardDescription>
</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 variant="secondary" 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">
{walletInfo?.type ? (
<Button
variant="secondary"
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} />
<ButtonConnectWallet walletType="Ledger" walletInfoState={walletInfoState} />
</div>
)}
</CardFooter>
<CardContent className="grid gap-4">
<div className="grid grid-cols-2 gap-6">
<ButtonConnectWallet walletType="Keplr" walletInfoState={walletInfoState} />
<ButtonConnectWallet walletType="Ledger" walletInfoState={walletInfoState} />
</div>
</CardContent>
</Card>
{walletInfo?.address ? (
<BalancesList key={walletInfo.address} walletAddress={walletInfo.address} />
<Card>
<CardHeader>
<CardTitle>Account info</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-5">
<div
onClick={async () => {
copy(walletInfo.address);
toast(`Copied address to clipboard`, { description: walletInfo.address });
}}
className=" flex items-center space-x-4 rounded-md border p-4 transition-colors hover:cursor-pointer hover:bg-muted/50"
>
<Copy className="w-5" />
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">Address</p>
<p className="text-sm text-muted-foreground">{walletInfo.address}</p>
</div>
</div>
<div
onClick={async () => {
copy(walletInfo.pubKey);
toast(`Copied public key to clipboard`, { description: walletInfo.pubKey });
}}
className=" flex items-center space-x-4 rounded-md border p-4 transition-colors hover:cursor-pointer hover:bg-muted/50"
>
<Copy className="w-5" />
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">Public key</p>
<p className="text-sm text-muted-foreground">{walletInfo.pubKey}</p>
</div>
</div>
{explorerLink ? (
<Button asChild variant="secondary">
<a href={explorerLink} target="_blank">
View in explorer <ArrowUpRightSquare className="ml-1" />
</a>
</Button>
) : null}
</CardContent>
</Card>
) : null}
{walletInfo?.address ? (
<Card>
<CardHeader>
<CardTitle>Balances</CardTitle>
<CardDescription>
Your list of tokens on {chain.chainDisplayName || "Cosmos Hub"}
</CardDescription>
</CardHeader>
<CardContent>
<BalancesTable walletAddress={walletInfo.address} />
</CardContent>
</Card>
) : null}
</div>
);
+1 -1
View File
@@ -59,7 +59,7 @@ const FindMultisigForm = ({ router }: FindMultisigFormProps) => {
return (
<Card>
<CardHeader>
<CardTitle>Already have a {chain.chainDisplayName || "Cosmos Hub"} multisig?</CardTitle>
<CardTitle>Already have a multisig on {chain.chainDisplayName || "Cosmos Hub"}?</CardTitle>
<CardDescription>
Enter its address below to view its transactions and create new ones.
</CardDescription>
+115
View File
@@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
+4 -4
View File
@@ -18,7 +18,7 @@
"@cosmjs/tendermint-rpc": "^0.32.2",
"@cosmjs/utils": "^0.32.2",
"@hookform/resolvers": "^3.3.1",
"@keplr-wallet/types": "^0.12.23",
"@keplr-wallet/types": "^0.12.76",
"@ledgerhq/hw-transport-webusb": "^6.27.19",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
@@ -1705,9 +1705,9 @@
}
},
"node_modules/@keplr-wallet/types": {
"version": "0.12.23",
"resolved": "https://registry.npmjs.org/@keplr-wallet/types/-/types-0.12.23.tgz",
"integrity": "sha512-PRnsoGswlWV5bwyqGw1o5dKCEY33s8Mgxd7HeqTX9L+Pj6TAapnBlyCxhXBp74W0QySIMe8it37VjhJqa9QWxA==",
"version": "0.12.76",
"resolved": "https://registry.npmjs.org/@keplr-wallet/types/-/types-0.12.76.tgz",
"integrity": "sha512-N2KRh1xCJ3gqZUWEi803RwpeCVrFCxFTZyUgFgIUt/fg0qPyd/F6okjcpvu6dj9BNDppuJntqYUpOUdWA7mY9Q==",
"dependencies": {
"long": "^4.0.0"
}
+1 -1
View File
@@ -23,7 +23,7 @@
"@cosmjs/tendermint-rpc": "^0.32.2",
"@cosmjs/utils": "^0.32.2",
"@hookform/resolvers": "^3.3.1",
"@keplr-wallet/types": "^0.12.23",
"@keplr-wallet/types": "^0.12.76",
"@ledgerhq/hw-transport-webusb": "^6.27.19",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
+26 -7
View File
@@ -1,17 +1,36 @@
import AccountView from "@/components/dataViews/AccountView";
import Page from "@/components/layout/Page";
import Head from "@/components/head";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { useChains } from "@/context/ChainsContext";
import Link from "next/link";
export default function AccountPage() {
const { chain } = useChains();
return (
<Page
goBack={
chain.registryName ? { pathname: `/${chain.registryName}`, title: "home" } : undefined
}
>
<div className="m-4 mt-8 flex max-w-xl flex-1 flex-col justify-center gap-4">
<Head title={`${chain.chainDisplayName || "Cosmos Hub"} Multisig Manager`} />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink>
{chain.registryName ? <Link href={`/${chain.registryName}`}>Home</Link> : null}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Account</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<AccountView />
</Page>
</div>
);
}
+6
View File
@@ -0,0 +1,6 @@
import { Window as KeplrWindow } from "@keplr-wallet/types";
declare global {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Window extends KeplrWindow {}
}