Merge pull request #182 from cosmos/feat/my-account
Add "My account" view
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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?: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user