Add AccountView component and Page

This commit is contained in:
abefernan
2023-12-01 16:06:42 +01:00
parent 48b3759873
commit 814bdcedab
5 changed files with 311 additions and 0 deletions
@@ -0,0 +1,29 @@
import { printableCoin } from "@/lib/displayHelpers";
import { Coin } from "@cosmjs/amino";
import { useChains } from "../../../context/ChainsContext";
import { Avatar, AvatarFallback, AvatarImage } from "../../ui/avatar";
import { Badge } from "../../ui/badge";
interface BalancePillProps {
readonly coin: Coin;
}
export default function BalancePill({ coin }: BalancePillProps) {
const { chain } = useChains();
const foundAsset = chain.assets.find((asset) => asset.base === coin.denom);
const logo = foundAsset?.logo_URIs?.svg || foundAsset?.logo_URIs?.png || "";
const macroCoin = printableCoin(coin, chain);
return (
<Badge key={coin.denom} className="px-1">
<Avatar className="mr-2">
<AvatarImage src={logo} alt={`${coin.denom} logo`} className="h-auto" />
<AvatarFallback className="text-white">
{coin.denom.slice(1, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
{macroCoin}
</Badge>
);
}
@@ -0,0 +1,46 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Coin } from "@cosmjs/amino";
import { StargateClient } from "@cosmjs/stargate";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { useChains } from "../../../context/ChainsContext";
import BalancePill from "./BalancePill";
interface BalancesListProps {
readonly walletAddress: string;
readonly setError: Dispatch<SetStateAction<string>>;
}
export default function BalancesList({ walletAddress, setError }: BalancesListProps) {
const { chain } = useChains();
const [balances, setBalances] = useState<readonly Coin[]>([]);
useEffect(() => {
(async function () {
if (!walletAddress) {
return;
}
try {
const client = await StargateClient.connect(chain.nodeAddress);
const newBalances = await client.getAllBalances(walletAddress);
setBalances(newBalances);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : "Failed to get balances");
console.error("Get balances error:", e);
}
})();
}, [chain.nodeAddress, setError, walletAddress]);
return balances.length ? (
<Card className="bg-fuchsia-850 w-full max-w-md border-transparent">
<CardHeader className="p-0">
<CardTitle>Balances</CardTitle>
</CardHeader>
<CardContent className="mt-4 flex flex-wrap gap-2 p-0">
{balances.map((coin) => (
<BalancePill key={coin.denom} coin={coin} />
))}
</CardContent>
</Card>
) : null;
}
@@ -0,0 +1,122 @@
import { LoadingStates, WalletInfo, WalletType } from "@/types/signing";
import { makeCosmoshubPath } from "@cosmjs/amino";
import { toBase64 } from "@cosmjs/encoding";
import { LedgerSigner } from "@cosmjs/ledger-amino";
import TransportWebUSB from "@ledgerhq/hw-transport-webusb";
import { Loader2 } from "lucide-react";
import Image from "next/image";
import { Dispatch, SetStateAction, useCallback, useLayoutEffect, useState } from "react";
import { useChains } from "../../../context/ChainsContext";
import { getConnectError } from "../../../lib/errorHelpers";
import { Button } from "../../ui/button";
interface ButtonConnectWalletProps {
readonly walletType: WalletType;
readonly walletInfoState: [
WalletInfo | null | undefined,
Dispatch<SetStateAction<WalletInfo | null | undefined>>,
];
readonly setError: Dispatch<SetStateAction<string>>;
}
export default function ButtonConnectWallet({
walletType,
walletInfoState: [walletInfo, setWalletInfo],
setError,
}: ButtonConnectWalletProps) {
const { chain } = useChains();
const [loading, setLoading] = useState<LoadingStates>({});
const connectKeplr = useCallback(async () => {
try {
setError("");
setLoading((oldLoading) => ({ ...oldLoading, keplr: true }));
await window.keplr.enable(chain.chainId);
window.keplr.defaultOptions = {
sign: { preferNoSetFee: true, preferNoSetMemo: true, disableBalanceCheck: true },
};
const { bech32Address: address, pubKey: pubKeyArray } = await window.keplr.getKey(
chain.chainId,
);
const pubKey = toBase64(pubKeyArray);
setWalletInfo({ type: "Keplr", address, pubKey });
} catch (e) {
console.error(e);
setError(getConnectError(e));
} finally {
setLoading((newLoading) => ({ ...newLoading, keplr: false }));
}
}, [chain.chainId, setError, setWalletInfo]);
useLayoutEffect(() => {
if (!walletInfo?.address) {
return;
}
const accountChangeKey = "keplr_keystorechange";
if (walletInfo.type === "Keplr") {
window.addEventListener(accountChangeKey, connectKeplr);
} else {
window.removeEventListener(accountChangeKey, connectKeplr);
}
}, [connectKeplr, walletInfo]);
const connectLedger = async () => {
try {
setError("");
setLoading((newLoading) => ({ ...newLoading, ledger: true }));
const ledgerTransport = await TransportWebUSB.create(120000, 120000);
const offlineSigner = new LedgerSigner(ledgerTransport, {
hdPaths: [makeCosmoshubPath(0)],
prefix: chain.addressPrefix,
});
const [{ address, pubkey: pubKeyArray }] = await offlineSigner.getAccounts();
const pubKey = toBase64(pubKeyArray);
setWalletInfo({ type: "Ledger", address, pubKey });
} catch (e) {
console.error(e);
setError(getConnectError(e));
} finally {
setLoading((newLoading) => ({ ...newLoading, ledger: false }));
}
};
const onClick = (() => {
if (walletType === "Keplr") {
return connectKeplr;
}
if (walletType === "Ledger") {
return connectLedger;
}
return () => {};
})();
const isLoading =
(walletType === "Keplr" && loading.keplr) || (walletType === "Ledger" && loading.ledger);
return (
<Button onClick={onClick} disabled={loading.keplr || loading.ledger}>
{isLoading ? (
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : (
<Image
alt=""
src={`/assets/icons/${walletType.toLowerCase()}.svg`}
width={20}
height={20}
className="mr-2"
/>
)}
Connect {walletType}
</Button>
);
}
+101
View File
@@ -0,0 +1,101 @@
import BadgeWithCopy from "@/components/BadgeWithCopy";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { explorerLinkAccount } from "@/lib/displayHelpers";
import { WalletInfo } from "@/types/signing";
import { AlertCircle, Unplug } from "lucide-react";
import Image from "next/image";
import { useState } from "react";
import { useChains } from "../../../context/ChainsContext";
import { Button } from "../../ui/button";
import BalancesList from "./BalancesList";
import ButtonConnectWallet from "./ButtonConnectWallet";
export default function AccountView() {
const { chain } = useChains();
const walletInfoState = useState<WalletInfo | null>();
const [walletInfo, setWalletInfo] = walletInfoState;
const [error, setError] = useState("");
const explorerLink =
explorerLinkAccount(chain.explorerLink.account, walletInfo?.address || "") || "";
return (
<div className="mt-6 flex flex-col gap-4">
<Card className="bg-fuchsia-850 min-w-[400px] border-transparent">
<CardHeader className="p-0">
<CardTitle className="flex items-baseline gap-2">
{walletInfo?.type ? (
<Image
alt=""
src={`/assets/icons/${walletInfo.type.toLowerCase()}.svg`}
width={20}
height={20}
/>
) : null}
{walletInfo?.type ? `${walletInfo.type} wallet connected` : "Connect wallet"}
</CardTitle>
</CardHeader>
{walletInfo ? (
<CardContent className="mt-4 max-w-md p-0">
<h2>Address</h2>
<div className="flex flex-col gap-4">
<BadgeWithCopy name="address" toCopy={walletInfo.address} />
{explorerLink ? (
<Button asChild className="self-center">
<a href={explorerLink} target="_blank">
View in explorer
</a>
</Button>
) : null}
</div>
<div className="mt-4">
<h2>PubKey</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>
);
}