Merge pull request #182 from cosmos/feat/my-account

Add "My account" view
This commit is contained in:
Simon Warta
2023-12-18 16:52:39 +01:00
committed by GitHub
32 changed files with 598 additions and 381 deletions
+26
View File
@@ -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>
);
}
+23 -5
View File
@@ -37,7 +37,8 @@ export default function CustomChainForm() {
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" }),
explorerLink: z.string({ required_error: "Explorer url is 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" }),
})
@@ -56,7 +57,8 @@ export default function CustomChainForm() {
bech32Prefix: defaultChain.addressPrefix,
gasPrice: defaultChain.gasPrice,
rpcNodes: defaultChain.nodeAddresses.join(", "),
explorerLink: defaultChain.explorerLink,
explorerTxLink: defaultChain.explorerLink.tx,
explorerAccountLink: defaultChain.explorerLink.account,
logo: defaultChain.logo,
assets: JSON.stringify(defaultChain.assets),
},
@@ -80,7 +82,10 @@ export default function CustomChainForm() {
assets: JSON.parse(chainFromForm.assets) as RegistryAsset[],
gasPrice: chainFromForm.gasPrice,
addressPrefix: chainFromForm.bech32Prefix,
explorerLink: chainFromForm.explorerLink,
explorerLink: {
tx: chainFromForm.explorerTxLink,
account: chainFromForm.explorerAccountLink,
},
},
});
}
@@ -190,10 +195,10 @@ export default function CustomChainForm() {
)}
/>
<FormField
name="explorerLink"
name="explorerTxLink"
render={({ field }) => (
<FormItem>
<FormLabel>Explorer Link</FormLabel>
<FormLabel>Explorer Tx Link</FormLabel>
<FormControl>
<Input placeholder="url" className="border-white" {...field} />
</FormControl>
@@ -202,6 +207,19 @@ export default function CustomChainForm() {
</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"
+1 -2
View File
@@ -17,9 +17,8 @@ export function ChainHeader() {
</>
) : (
<>
<Skeleton className="h-12 w-12 rounded-full" />
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</>
+21 -1
View File
@@ -1,9 +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="bg-fuchsia-900">
<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>
);
}
@@ -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>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>
+4 -1
View File
@@ -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 -8
View File
@@ -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;
+3 -2
View File
@@ -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 ? (
<>
+7 -2
View File
@@ -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 && (
+2 -3
View File
@@ -13,7 +13,7 @@ export const emptyChain: ChainInfo = {
assets: [],
gasPrice: "",
addressPrefix: "",
explorerLink: "",
explorerLink: { tx: "", account: "" },
};
export const isChainInfoFilled = (chain: Partial<ChainInfo>): chain is ChainInfo =>
@@ -30,8 +30,7 @@ export const isChainInfoFilled = (chain: Partial<ChainInfo>): chain is ChainInfo
chain.displayDenomExponent >= 0 &&
chain.assets?.length &&
chain.gasPrice &&
chain.addressPrefix &&
chain.explorerLink,
chain.addressPrefix,
);
export const setChains = (dispatch: Dispatch, chains: ChainItems) => {
+2 -2
View File
@@ -95,8 +95,8 @@ 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) ? "" : rootRoute;
// Avoid app from thinking the /api route is a registryName
const chainNameFromUrl = rootRoute === "api" ? "" : rootRoute;
const recentChain = getRecentChainFromStorage(chains);
if (!chainNameFromUrl && isChainInfoFilled(recentChain)) {
+25 -24
View File
@@ -1,6 +1,6 @@
import { RegistryAsset } from "@/types/chainRegistry";
import { emptyChain } from "./helpers";
import { ChainInfo, ChainItems } from "./types";
import { ChainInfo, ChainItems, ExplorerLink } from "./types";
const registryShaStorageKey = "context-registry-sha";
export const getShaFromStorage = () => localStorage.getItem(registryShaStorageKey);
@@ -99,7 +99,7 @@ export const getRecentChainFromStorage = (chains: ChainItems): Partial<ChainInfo
export const getChainFromUrl = (chainName: string) => {
if (!chainName) {
return emptyChain;
return { registryName: chainName };
}
const params = new URLSearchParams(location.search);
@@ -118,6 +118,7 @@ export const getChainFromUrl = (chainName: string) => {
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,
@@ -132,7 +133,9 @@ export const getChainFromUrl = (chainName: string) => {
...(assetsValue.length && { assets: assetsValue }),
...(gasPrice && { gasPrice }),
...(addressPrefix && { addressPrefix }),
...(explorerLink && { explorerLink }),
...(explorerLink && {
explorerLink: { tx: explorerLinkValue.tx || "", account: explorerLinkValue.account || "" },
}),
};
return urlChain;
@@ -141,7 +144,7 @@ export const getChainFromUrl = (chainName: string) => {
export const getChainFromEnvfile = (chainName: string) => {
const registryName = process.env.NEXT_PUBLIC_REGISTRY_NAME || "";
if (chainName && registryName !== chainName) {
return emptyChain;
return { registryName: chainName };
}
const logo = process.env.NEXT_PUBLIC_LOGO;
@@ -158,6 +161,7 @@ export const getChainFromEnvfile = (chainName: string) => {
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,
@@ -172,7 +176,9 @@ export const getChainFromEnvfile = (chainName: string) => {
...(assetsValue.length && { assets: assetsValue }),
...(gasPrice && { gasPrice }),
...(addressPrefix && { addressPrefix }),
...(explorerLink && { explorerLink }),
...(explorerLinkValue && {
explorerLink: { tx: explorerLinkValue.tx || "", account: explorerLinkValue.account || "" },
}),
};
return envfileChain;
@@ -192,31 +198,26 @@ export const getChainFromStorage = (
};
export const setChainInUrl = (chain: ChainInfo, chains: ChainItems) => {
const params = new URLSearchParams();
const storedChain = getChainFromStorage(chain.registryName, chains);
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
if (!storedChain || chains.localnets.has(chain.registryName)) {
for (const [key, value] of Object.entries(chain)) {
if (typeof value === "object") {
params.set(key, JSON.stringify(value));
} else {
params.set(key, value);
}
}
} else {
for (const [key, value] of Object.entries(chain)) {
const storedValue = storedChain[key as keyof ChainInfo];
const params = new URLSearchParams();
if (typeof value === "object" && JSON.stringify(value) !== JSON.stringify(storedValue)) {
params.set(key, JSON.stringify(value));
} else if (value !== storedValue) {
params.set(key, value);
}
for (const [key, value] of Object.entries(chain)) {
if (typeof value === "object") {
params.set(key, JSON.stringify(value));
} else {
params.set(key, value);
}
}
const newPathname = location.pathname.includes(chain.registryName) ? location.pathname : "/";
const newUrl = params.size ? `${newPathname}?${params}` : newPathname;
window.history.replaceState({}, "", newUrl);
+6 -1
View File
@@ -33,9 +33,14 @@ export interface ChainInfo {
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";
+20 -2
View File
@@ -1,5 +1,5 @@
import { isChainInfoFilled } from "@/context/ChainsContext/helpers";
import { ChainInfo, ChainItems } from "@/context/ChainsContext/types";
import { ChainInfo, ChainItems, ExplorerLink } from "@/context/ChainsContext/types";
import { GithubChainRegistryItem, RegistryAsset, RegistryChain } from "@/types/chainRegistry";
import { preventUnhandledRejections } from "./promises";
import { requestGhJson } from "./request";
@@ -136,7 +136,25 @@ const getChainInfoFromJsons = (
const firstAsset = cdnRegistryAssets[0];
const logo = getLogoUri(registryChain, firstAsset);
const nodeAddresses = registryChain.apis?.rpc.map(({ address }) => address) ?? [];
const explorerLink = registryChain.explorers?.[0]?.tx_page ?? "";
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;
+6 -1
View File
@@ -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,
+2 -2
View File
@@ -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;
};
+3 -6
View File
@@ -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
+13
View File
@@ -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>
);
}
+19
View File
@@ -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>
);
}
+23
View File
@@ -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;
+6 -3
View File
@@ -1,5 +1,6 @@
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";
@@ -9,9 +10,11 @@ export default function MultisigApp({ Component, pageProps }: AppProps) {
return (
<ChainsProvider>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
<Header />
<Component {...pageProps} />
<Toaster />
<TooltipProvider>
<Header />
<Component {...pageProps} />
<Toaster />
</TooltipProvider>
</ThemeProvider>
</ChainsProvider>
);
-16
View File
@@ -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;
-283
View File
@@ -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 bg-fuchsia-900">
<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
View File
@@ -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;
}
+30
View File
@@ -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

+3
View File
@@ -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

+3 -2
View File
@@ -40,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 {
@@ -60,7 +61,7 @@ export interface RegistryChain {
readonly bech32_prefix: string;
readonly chain_id: string;
readonly chain_name: string;
readonly explorers: readonly RegistryChainExplorer[];
readonly explorers?: readonly RegistryChainExplorer[];
readonly fees?: RegistryChainFees;
readonly pretty_name: string;
readonly logo_URIs?: {
+15
View File
@@ -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;
}