Add AccountView component and Page
This commit is contained in:
@@ -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>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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user