diff --git a/Dockerfile b/Dockerfile index 96806e0..16df3ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Use the official Node.js v14.x image as the base image -FROM node:14 +FROM node:16 # Set the working directory to /app WORKDIR /app diff --git a/components/chain-wallet-card.tsx b/components/apps/dashboard/chain-wallet-card.tsx similarity index 94% rename from components/chain-wallet-card.tsx rename to components/apps/dashboard/chain-wallet-card.tsx index 492fd5f..6305201 100644 --- a/components/chain-wallet-card.tsx +++ b/components/apps/dashboard/chain-wallet-card.tsx @@ -1,65 +1,65 @@ -import { Box, Button, Heading, HStack } from "@chakra-ui/react"; -import { ChainName, WalletStatus } from "@cosmos-kit/core"; -import { useChain } from "@cosmos-kit/react"; -import { useEffect } from "react"; -import { ConnectedShowAddress } from "./react"; - -export const ChainWalletCard = ({ - chainName, - type = "address-on-page", - setGlobalStatus, -}: { - chainName: ChainName; - type: string; - setGlobalStatus?: (status: WalletStatus) => void; -}) => { - const { chain, status, address, openView } = useChain(chainName); - useEffect(() => { - if (status == "Connecting") { - setGlobalStatus?.(WalletStatus.Connecting); - } else if (status == "Connected") { - setGlobalStatus?.(WalletStatus.Connected); - } else { - setGlobalStatus?.(WalletStatus.Disconnected); - } - }, [status]); - - switch (type) { - case "address-in-modal": - return ( - - - {chain.pretty_name} - - - - ); - case "address-on-page": - return ( - - - {chain.pretty_name} - - - - - - ); - default: - throw new Error("No such chain card type: " + type); - } -}; +import { Box, Button, Heading, HStack } from "@chakra-ui/react"; +import { ChainName, WalletStatus } from "@cosmos-kit/core"; +import { useChain } from "@cosmos-kit/react"; +import { useEffect } from "react"; +import { ConnectedShowAddress } from "./react"; + +export const ChainWalletCard = ({ + chainName, + type = "address-on-page", + setGlobalStatus, +}: { + chainName: ChainName; + type: string; + setGlobalStatus?: (status: WalletStatus) => void; +}) => { + const { chain, status, address, openView } = useChain(chainName); + useEffect(() => { + if (status == "Connecting") { + setGlobalStatus?.(WalletStatus.Connecting); + } else if (status == "Connected") { + setGlobalStatus?.(WalletStatus.Connected); + } else { + setGlobalStatus?.(WalletStatus.Disconnected); + } + }, [status]); + + switch (type) { + case "address-in-modal": + return ( + + + {chain.pretty_name} + + + + ); + case "address-on-page": + return ( + + + {chain.pretty_name} + + + + + + ); + default: + throw new Error("No such chain card type: " + type); + } +}; \ No newline at end of file diff --git a/components/apps/dashboard.tsx b/components/apps/dashboard/dashboard.tsx similarity index 91% rename from components/apps/dashboard.tsx rename to components/apps/dashboard/dashboard.tsx index dc3af9a..5bf0377 100644 --- a/components/apps/dashboard.tsx +++ b/components/apps/dashboard/dashboard.tsx @@ -18,8 +18,8 @@ import { import { useChain } from '@cosmos-kit/react'; import { IoWalletOutline } from 'react-icons/io5'; - import {ChainWalletCard} from '../chain-wallet-card'; -import { FaUserCircle } from "react-icons/fa"; +import {ChainWalletCard} from './chain-wallet-card'; +import { FaUserCircle } from "react-icons/fa"; const chainNames_1 = ['cosmoshub', 'osmosis']; const chainNames_2 = ['stargaze', 'akash']; @@ -61,7 +61,7 @@ export default function DashboardContent(){ + ); + }; + + export const CopyAddressBtn = ({ + walletStatus, + connected, + }: { + walletStatus: WalletStatus; + connected: ReactNode; + }) => { + switch (walletStatus) { + case "Connected": + return <>{connected}; + default: + return ( + + + + ); + } + }; \ No newline at end of file diff --git a/components/apps/dashboard/react/astronaut.tsx b/components/apps/dashboard/react/astronaut.tsx new file mode 100644 index 0000000..0b5eb0c --- /dev/null +++ b/components/apps/dashboard/react/astronaut.tsx @@ -0,0 +1,156 @@ +export const Astronaut = (props: any) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); \ No newline at end of file diff --git a/components/apps/dashboard/react/handleChangeColor.tsx b/components/apps/dashboard/react/handleChangeColor.tsx new file mode 100644 index 0000000..7c7ff46 --- /dev/null +++ b/components/apps/dashboard/react/handleChangeColor.tsx @@ -0,0 +1,9 @@ +// use for let color mode value fit Rules of Hooks +export function handleChangeColorModeValue( + colorMode: string, + light: any, + dark: any + ) { + if (colorMode === "light") return light; + if (colorMode === "dark") return dark; + } \ No newline at end of file diff --git a/components/apps/dashboard/react/index.ts b/components/apps/dashboard/react/index.ts new file mode 100644 index 0000000..7e45090 --- /dev/null +++ b/components/apps/dashboard/react/index.ts @@ -0,0 +1,5 @@ +export * from './address-card'; +export * from './astronaut'; +export * from './user-card'; +export * from './wallet-connect'; +export * from './warn-block'; \ No newline at end of file diff --git a/components/apps/dashboard/react/user-card.tsx b/components/apps/dashboard/react/user-card.tsx new file mode 100644 index 0000000..47df5c3 --- /dev/null +++ b/components/apps/dashboard/react/user-card.tsx @@ -0,0 +1,34 @@ +import { Box,Stack, Text } from "@chakra-ui/react"; +import React from "react"; + +import { ConnectedUserCardType } from "../../../../types"; + +export const ConnectedUserInfo = ({ + username, + icon, +}: ConnectedUserCardType) => { + return ( + + {username && ( + <> + + {icon} + + + {username} + + + )} + + ); +}; \ No newline at end of file diff --git a/components/apps/dashboard/react/wallet-connect.tsx b/components/apps/dashboard/react/wallet-connect.tsx new file mode 100644 index 0000000..0743c76 --- /dev/null +++ b/components/apps/dashboard/react/wallet-connect.tsx @@ -0,0 +1,202 @@ +import { Button, Icon, Stack, Text, useColorModeValue } from "@chakra-ui/react"; +import { WalletStatus } from "@cosmos-kit/core"; +import React, { MouseEventHandler, ReactNode } from "react"; +import { FiAlertTriangle } from "react-icons/fi"; +import { IoWallet } from "react-icons/io5"; + +import { ConnectWalletType } from "../../../../types"; + +export const ConnectWalletButton = ({ + buttonText, + isLoading, + isDisabled, + icon, + onClickConnectBtn, +}: ConnectWalletType) => { + return ( + + ); +}; + +export const Disconnected = ({ + buttonText, + onClick, +}: { + buttonText: string; + onClick: MouseEventHandler; +}) => { + return ( + + ); +}; + +export const Connected = ({ + buttonText, + onClick, +}: { + buttonText: string; + onClick: MouseEventHandler; +}) => { + return ( + + ); +}; + +export const Connecting = () => { + return ; +}; + +export const Rejected = ({ + buttonText, + wordOfWarning, + onClick, +}: { + buttonText: string; + wordOfWarning?: string; + onClick: MouseEventHandler; +}) => { + const bg = useColorModeValue("orange.200", "orange.300"); + + return ( + + + {wordOfWarning && ( + + + + + Warning:  + + {wordOfWarning} + + + )} + + ); +}; + +export const Error = ({ + buttonText, + wordOfWarning, + onClick, +}: { + buttonText: string; + wordOfWarning?: string; + onClick: MouseEventHandler; +}) => { + const bg = useColorModeValue("orange.200", "orange.300"); + + return ( + + + {wordOfWarning && ( + + + + + Warning:  + + {wordOfWarning} + + + )} + + ); +}; + +export const NotExist = ({ + buttonText, + onClick, +}: { + buttonText: string; + onClick: MouseEventHandler; +}) => { + return ( + + ); +}; + +export const WalletConnectComponent = ({ + walletStatus, + disconnect, + connecting, + connected, + rejected, + error, + notExist, +}: { + walletStatus: WalletStatus; + disconnect: ReactNode; + connecting: ReactNode; + connected: ReactNode; + rejected: ReactNode; + error: ReactNode; + notExist: ReactNode; +}) => { + switch (walletStatus) { + case "Disconnected": + return <>{disconnect}; + case "Connecting": + return <>{connecting}; + case "Connected": + return <>{connected}; + case "Rejected": + return <>{rejected}; + case "Error": + return <>{error}; + case "NotExist": + return <>{notExist}; + default: + return <>{disconnect}; + } +}; \ No newline at end of file diff --git a/components/apps/dashboard/react/warn-block.tsx b/components/apps/dashboard/react/warn-block.tsx new file mode 100644 index 0000000..ccf67f3 --- /dev/null +++ b/components/apps/dashboard/react/warn-block.tsx @@ -0,0 +1,90 @@ +import { Box, Stack, Text, useColorModeValue } from "@chakra-ui/react"; +import { WalletStatus } from "@cosmos-kit/core"; +import React, { ReactNode } from "react"; + +export const WarnBlock = ({ + wordOfWarning, + icon, +}: { + wordOfWarning?: string; + icon?: ReactNode; +}) => { + return ( + + + + {icon} + + {wordOfWarning} + + + ); +}; + +export const RejectedWarn = ({ + wordOfWarning, + icon, +}: { + wordOfWarning?: string; + icon?: ReactNode; +}) => { + return ; +}; + +export const ConnectStatusWarn = ({ + walletStatus, + rejected, + error, +}: { + walletStatus: WalletStatus; + rejected: ReactNode; + error: ReactNode; +}) => { + switch (walletStatus) { + case "Rejected": + return <>{rejected}; + case "Error": + return <>{error}; + default: + return <>; + } +}; \ No newline at end of file diff --git a/components/apps/gov/react/address-card.tsx b/components/apps/gov/react/address-card.tsx index 1e96ab1..74d887d 100644 --- a/components/apps/gov/react/address-card.tsx +++ b/components/apps/gov/react/address-card.tsx @@ -2,41 +2,42 @@ import { Box, Button, Icon, + Image, Text, useClipboard, useColorMode, - Image } from "@chakra-ui/react"; import { WalletStatus } from "@cosmos-kit/core"; -import { FaCheckCircle } from 'react-icons/fa'; -import { FiCopy } from 'react-icons/fi'; -import React, { ReactNode, useEffect,useState } from "react"; +import React, { ReactNode, useEffect, useState } from "react"; +import { FaCheckCircle } from "react-icons/fa"; +import { FiCopy } from "react-icons/fi"; -import { CopyAddressType } from "../types"; +import { CopyAddressType } from "../../../types"; +import { handleChangeColorModeValue } from "./handleChangeColor"; const SIZES = { lg: { height: 12, walletImageSize: 7, icon: 5, - fontSize: 'md', + fontSize: "md", }, md: { height: 10, walletImageSize: 6, icon: 4, - fontSize: 'sm', + fontSize: "sm", }, sm: { height: 7, walletImageSize: 5, icon: 3.5, - fontSize: 'sm', + fontSize: "sm", }, }; export function stringTruncateFromCenter(str: string, maxLength: number) { - const midChar = '…'; // character to insert into the center of the result + const midChar = "…"; // character to insert into the center of the result if (str.length <= maxLength) return str; @@ -49,26 +50,16 @@ export function stringTruncateFromCenter(str: string, maxLength: number) { return str.substring(0, left) + midChar + str.substring(right); } -export function handleChangeColorModeValue( - colorMode: string, - light: string, - dark: string -) { - if (colorMode === 'light') return light; - if (colorMode === 'dark') return dark; -} - - export const ConnectedShowAddress = ({ address, walletIcon, isLoading, isRound, - size = 'md', + size = "md", maxDisplayLength, }: CopyAddressType) => { - const { hasCopied, onCopy } = useClipboard(address ? address : ''); - const [displayAddress, setDisplayAddress] = useState(''); + const { hasCopied, onCopy } = useClipboard(address ? address : ""); + const [displayAddress, setDisplayAddress] = useState(""); const { colorMode } = useColorMode(); const defaultMaxLength = { lg: 14, @@ -77,7 +68,7 @@ export const ConnectedShowAddress = ({ }; useEffect(() => { - if (!address) setDisplayAddress('address not identified yet'); + if (!address) setDisplayAddress("address not identified yet"); if (address && maxDisplayLength) setDisplayAddress(stringTruncateFromCenter(address, maxDisplayLength)); if (address && !maxDisplayLength) @@ -87,6 +78,7 @@ export const ConnectedShowAddress = ({ defaultMaxLength[size as keyof typeof defaultMaxLength] ) ); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [address]); return ( @@ -96,12 +88,12 @@ export const ConnectedShowAddress = ({ display="flex" alignItems="center" justifyContent="center" - borderRadius={isRound ? 'full' : 'lg'} + borderRadius={isRound ? "full" : "lg"} border="1px solid" borderColor={handleChangeColorModeValue( colorMode, - 'gray.200', - 'whiteAlpha.300' + "gray.200", + "whiteAlpha.300" )} w="full" h={SIZES[size as keyof typeof SIZES].height} @@ -110,30 +102,30 @@ export const ConnectedShowAddress = ({ pr={2} color={handleChangeColorModeValue( colorMode, - 'gray.700', - 'whiteAlpha.600' + "gray.700", + "whiteAlpha.600" )} transition="all .3s ease-in-out" isDisabled={!address && true} isLoading={isLoading} _hover={{ - bg: 'rgba(142, 142, 142, 0.05)', + bg: "rgba(142, 142, 142, 0.05)", }} _focus={{ - outline: 'none', + outline: "none", }} _disabled={{ opacity: 0.6, - cursor: 'not-allowed', - borderColor: 'rgba(142, 142, 142, 0.1)', + cursor: "not-allowed", + borderColor: "rgba(142, 142, 142, 0.1)", _hover: { - bg: 'transparent', + bg: "transparent", }, _active: { - outline: 'none', + outline: "none", }, _focus: { - outline: 'none', + outline: "none", }, }} onClick={onCopy} @@ -170,11 +162,11 @@ export const ConnectedShowAddress = ({ opacity={0.9} color={ hasCopied - ? 'green.400' + ? "green.400" : handleChangeColorModeValue( colorMode, - 'gray.500', - 'whiteAlpha.400' + "gray.500", + "whiteAlpha.400" ) } /> @@ -191,9 +183,17 @@ export const CopyAddressBtn = ({ connected: ReactNode; }) => { switch (walletStatus) { - case WalletStatus.Connected: + case "Connected": return <>{connected}; default: - return <>; + return ( + + + + ); } -}; +}; \ No newline at end of file diff --git a/components/apps/gov/react/chain-dropdown.tsx b/components/apps/gov/react/chain-dropdown.tsx index d9f4b29..f98ea08 100644 --- a/components/apps/gov/react/chain-dropdown.tsx +++ b/components/apps/gov/react/chain-dropdown.tsx @@ -26,7 +26,7 @@ import { ChainOption, ChangeChainDropdownType, ChangeChainMenuType -} from '../types'; +} from '../../../types'; const SkeletonOptions = () => { return ( diff --git a/components/apps/gov/react/choose-chain.tsx b/components/apps/gov/react/choose-chain.tsx index e812e2c..f54c831 100644 --- a/components/apps/gov/react/choose-chain.tsx +++ b/components/apps/gov/react/choose-chain.tsx @@ -4,7 +4,7 @@ import { ChooseChainInfo, ChainOption, handleSelectChainDropdown -} from '../types'; +} from '../../../types'; export function ChooseChain({ chainName, diff --git a/components/apps/gov/react/proposal-card.tsx b/components/apps/gov/react/proposal-card.tsx index a7886cf..5f917dc 100644 --- a/components/apps/gov/react/proposal-card.tsx +++ b/components/apps/gov/react/proposal-card.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import type { Proposal } from 'interchain/types/codegen/cosmos/gov/v1beta1/gov'; import dayjs from 'dayjs'; import { cosmos } from 'interchain'; -import { VoteOption } from '../../../types'; +import { VoteOption } from '../../../../types'; import { decodeUint8Arr, Votes } from './vote'; import { Badge, diff --git a/components/apps/gov/react/proposal-modal.tsx b/components/apps/gov/react/proposal-modal.tsx index dc651a8..944da43 100644 --- a/components/apps/gov/react/proposal-modal.tsx +++ b/components/apps/gov/react/proposal-modal.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState } from 'react'; -import { TransactionResult, VoteOption } from '../../../types'; +import { TransactionResult, VoteOption } from '../../../../types'; import { AiFillCheckCircle, AiFillCloseCircle, diff --git a/components/apps/gov/react/user-card.tsx b/components/apps/gov/react/user-card.tsx index 13c8bd7..adf255c 100644 --- a/components/apps/gov/react/user-card.tsx +++ b/components/apps/gov/react/user-card.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Text, Stack, Box } from '@chakra-ui/react'; -import { ConnectedUserCardType } from '../types'; +import { ConnectedUserCardType } from '../../../types'; export const ConnectedUserInfo = ({ username, diff --git a/components/apps/gov/react/wallet-connect.tsx b/components/apps/gov/react/wallet-connect.tsx index 683e923..5d5450b 100644 --- a/components/apps/gov/react/wallet-connect.tsx +++ b/components/apps/gov/react/wallet-connect.tsx @@ -1,7 +1,7 @@ import React, { MouseEventHandler, ReactNode } from 'react'; import { Button, Icon, Stack, Text, useColorModeValue } from '@chakra-ui/react'; import { IoWallet } from 'react-icons/io5'; -import { ConnectWalletType } from '../types'; +import { ConnectWalletType } from '../../../types'; import { FiAlertTriangle } from 'react-icons/fi'; import { WalletStatus } from '@cosmos-kit/core'; @@ -35,7 +35,6 @@ export const ConnectWalletButton = ({ }} onClick={onClickConnectBtn} > - {buttonText ? buttonText : 'Connect Wallet'} ); diff --git a/components/apps/gov/store/index.ts b/components/apps/gov/store/index.ts new file mode 100644 index 0000000..4b144c4 --- /dev/null +++ b/components/apps/gov/store/index.ts @@ -0,0 +1,23 @@ +import { makeAutoObservable } from 'mobx'; +import type { ChainName } from '@cosmos-kit/core'; + +class ChainStore { + sourceChain: ChainName = 'terpnetwork'; + sourceAddress: string = ''; + + constructor() { + makeAutoObservable(this); + } + + setSourceChain(chainName: ChainName) { + this.sourceChain = chainName; + } + + setSourceAddress(address: string) { + this.sourceAddress = address; + } +} + +const chainStore = new ChainStore(); + +export default chainStore; \ No newline at end of file diff --git a/components/apps/poap/claim.tsx b/components/apps/poap/claim.tsx deleted file mode 100644 index 998e885..0000000 --- a/components/apps/poap/claim.tsx +++ /dev/null @@ -1,648 +0,0 @@ -import { - useDisclosure, - Box, - Button, - FormControl, - FormErrorMessage, - FormHelperText, - FormLabel, - HStack, - Image, - Input, - Modal, - ModalCloseButton, - ModalContent, - ModalOverlay, - Text, - VStack, - Center, - } from "@chakra-ui/react"; - import { BadgeResponse } from "@steak-enjoyers/badges.js/types/codegen/Hub.types"; - import { bech32 } from "bech32"; - import { useEffect, useState } from "react"; - import { QrReader } from "react-qr-reader"; - import * as secp256k1 from "secp256k1"; - - import ScanIcon from "./scanIcon"; - import TxModal from "./txModal"; - import { getTimestampInSeconds, formatTimestamp, sha256, hexToBytes, bytesToHex } from "../helpers"; - import { useStore } from "../store"; -import React from "react"; - - // https://stackoverflow.com/questions/5214127/css-technique-for-a-horizontal-line-with-words-in-the-middle - const hrStyle = { - bg: "rgb(226, 232, 240)", - content: `""`, - display: "inline-block", - height: "1px", - position: "relative", - verticalAlign: "middle", - width: "calc(50% - 0.5rem - 9px)", - }; - - const fillerImageUrl = "https://via.placeholder.com/500?text=Image+Not+Available"; - const fillerText = "Undefined"; - - enum Page { - Credential = 1, - Preview, - Submit, - } - - export default function Claim() { - const store = useStore(); - - // which page to display - const [page, setPage] = useState(Page.Credential); - - // inputs - badge id - const [idStr, setIdStr] = useState(""); - const [idValid, setIdValid] = useState(null); - const [idInvalidReason, setIdInvalidReason] = useState(""); - - // inputs - key - const [privkeyStr, setPrivkeyStr] = useState(""); - const [privkeyValid, setPrivkeyValid] = useState(null); - const [privkeyInvalidReason, setPrivkeyInvalidReason] = useState(""); - - // inputs - owner - const [owner, setOwner] = useState(""); - const [ownerValid, setOwnerValid] = useState(null); - const [ownerInvalidReason, setOwnerInvalidReason] = useState(""); - - // whether webcam modal is open on the credentials page - const { isOpen: isCameraOpen, onOpen: onCameraOpen, onClose: onCameraClose } = useDisclosure(); - - // whether tx modal is open on the submit page - const { isOpen: isTxModalOpen, onOpen: onTxModalOpen, onClose: onTxModalClose } = useDisclosure(); - - // values on the preview page - const [badge, setBadge] = useState(); - - // whenever input id is changed, validate it - useEffect(() => { - function setIdValidNull() { - setIdValid(null); - setIdInvalidReason(""); - console.log("empty id"); - } - - function setIdValidTrue() { - setIdValid(true); - setIdInvalidReason(""); - console.log(`id "${idStr}" is valid`); - } - - function setIdValidFalse(reason: string) { - setIdValid(false); - setIdInvalidReason(reason); - console.log(`invalid id "${idStr}": ${reason}`); - } - - //-------------------- - // stateless checks - //-------------------- - - if (idStr === "") { - return setIdValidNull(); - } - - const id = Number(idStr); - - if (!Number.isInteger(id)) { - return setIdValidFalse("id must be an integer!"); - } - - if (id < 1) { - return setIdValidFalse("id cannot be zero!"); - } - - if (!!store.badgeCount && id > store.badgeCount) { - return setIdValidFalse( - `id cannot be greater than the current badge count! (count: ${store.badgeCount})` - ); - } - - //-------------------- - // stateful checks - //-------------------- - - // skip if the query client isn't initialized - if (!store.wasmClient) { - return setIdValidNull(); - } - - store.getBadge(id).then((badge) => { - if (badge.rule !== "by_keys" && !("by_key" in badge.rule)) { - return setIdValidFalse("id is valid but this badge is not publicly mintable!"); - } - - if (badge.expiry && getTimestampInSeconds() > badge.expiry) { - return setIdValidFalse( - `id is valid but minting deadline has already elapsed! (deadline: ${formatTimestamp( - badge.expiry - )})` - ); - } - - if (badge.max_supply && badge.current_supply >= badge.max_supply) { - return setIdValidFalse( - `id is valid but max supply has already been reached! (max supply: ${badge.max_supply})` - ); - } - - return setIdValidTrue(); - }); - }, [idStr, store.wasmClient]); - - // whenever input key is changed, we need to validate it - useEffect(() => { - function setPrivkeyValidNull() { - setPrivkeyValid(null); - setPrivkeyInvalidReason(""); - console.log("empty key"); - } - - function setPrivkeyValidTrue() { - setPrivkeyValid(true); - setPrivkeyInvalidReason(""); - console.log(`key "${privkeyStr}" is valid`); - } - - function setPrivkeyValidFalse(reason: string) { - setPrivkeyValid(false); - setPrivkeyInvalidReason(reason); - console.log(`invalid key "${privkeyStr}": ${reason}`); - } - - //-------------------- - // stateless checks - //-------------------- - - if (privkeyStr === "") { - return setPrivkeyValidNull(); - } - - const bytes = Buffer.from(privkeyStr, "hex"); - - // A string is a valid hex-encoded bytearray if it can be decoded to a Buffer, and the string - // has exactly twice as many bytes as the number of the Buffer's bytes. - if (bytes.length * 2 != privkeyStr.length) { - return setPrivkeyValidFalse("not a valid hex string!"); - } - - try { - if (!secp256k1.privateKeyVerify(bytes)) { - return setPrivkeyValidFalse("not a valid secp256k1 private key!"); - } - } catch (err) { - return setPrivkeyValidFalse(`not a valid secp256k1 private key: ${err}`); - } - - //-------------------- - // stateful checks - //-------------------- - - // skip if the query client isn't initialized - if (!store.wasmClient) { - return setPrivkeyValidNull(); - } - - // Now we know the key is a valid secp256k1 privkey, we need to check whether it is eligible for - // claiming the badge. - // Firstly, if we don't already have a valid badge id, it's impossible to determine to badge's - // eligibility. Simply return null in this case. - if (!!!idValid) { - return setPrivkeyValidNull(); - } - - const pubkeyStr = bytesToHex(secp256k1.publicKeyCreate(hexToBytes(privkeyStr))); - - // this block of code is fucking atrocious, but "it just works" - store.getBadge(Number(idStr)).then((badge) => { - if (badge.rule === "by_keys") { - store - .isKeyWhitelisted(Number(idStr), pubkeyStr) - .then((isWhitelisted) => { - if (isWhitelisted) { - return setPrivkeyValidTrue(); - } else { - return setPrivkeyValidFalse(`this key is not eligible to claim badge #${idStr}`); - } - }) - .catch((err) => { - return setPrivkeyValidFalse( - `failed to check this key's eligibility to claim badge #${idStr}: ${err}` - ); - }); - } else if ("by_key" in badge.rule) { - if (pubkeyStr === badge.rule["by_key"]) { - return setPrivkeyValidTrue(); - } else { - return setPrivkeyValidFalse(`this key is not eligible to claim badge #${idStr}`); - } - } else { - return setPrivkeyValidFalse(`this key is not eligible to claim badge #${idStr}`); - } - }); - }, [privkeyStr, idStr, idValid, store.wasmClient]); - - // whenver input owner address is changed, we need to validate it - useEffect(() => { - function setOwnerValidNull() { - setOwnerValid(null); - setOwnerInvalidReason(""); - console.log("empty key"); - } - - function setOwnerValidTrue() { - setOwnerValid(true); - setOwnerInvalidReason(""); - console.log(`key "${privkeyStr}" is valid`); - } - - function setOwnerValidFalse(reason: string) { - setOwnerValid(false); - setOwnerInvalidReason(reason); - console.log(`invalid key "${privkeyStr}": ${reason}`); - } - - //-------------------- - // stateless checks - //-------------------- - - if (owner === "") { - return setOwnerValidNull(); - } - - try { - const { prefix } = bech32.decode(owner); - if (prefix !== store.networkConfig!.prefix) { - return setOwnerValidFalse( - `address has incorrect prefix: expecting ${store.networkConfig!.prefix}, found ${prefix}` - ); - } - } catch (err) { - return setOwnerValidFalse(`not a valid bech32 address: ${err}`); - } - - //-------------------- - // stateful checks - //-------------------- - - // skip if the query client isn't initialized - if (!store.wasmClient) { - return setOwnerValidNull(); - } - - // Now we know the owner is a valid bech32 address, we need to check whether it is eligible for - // claiming the badge. - // Firstly, if we don't already have a valid badge id, it's impossible to determine to badge's - // eligibility. Simply return null in this case. - if (!!!idValid) { - return setOwnerValidNull(); - } - - store - .isOwnerEligible(Number(idStr), owner) - .then((eligible) => { - if (eligible) { - return setOwnerValidTrue(); - } else { - return setOwnerValidFalse(`this address is not eligible to claim badge #${idStr}`); - } - }) - .catch((err) => { - return setOwnerValidFalse( - `failed to check this address' eligibility to claim badge #${idStr}: ${err}` - ); - }); - }, [owner, idStr, idValid, store.wasmClient]); - - // if the id has been updated, we need to update the metadata displayed on the preview page - // only update if the id is valid AND wasm client has been initialized - useEffect(() => { - if (!store.wasmClient) { - console.log(`wasm client is uninitialized, setting badge to undefined`); - return setBadge(undefined); - } - if (!idValid) { - console.log(`invalid badge id "${idStr}", setting badge to undefined`); - return setBadge(undefined); - } - store - .getBadge(Number(idStr)) - .then((badge) => { - console.log(`successfully fetched badge with id "${idStr}"! badge:`, badge); - setBadge(badge); - }) - .catch((err) => { - console.log(`failed to fetch badge with id "${idStr}"! reason:`, err); - setBadge(undefined); - }); - }, [idStr, idValid, store.wasmClient]); - - // when the component is first mounted, we check the URL query params and auto-fill id and key - useEffect(() => { - const url = window.location.href; - const split = url.split("?"); - const params = new URLSearchParams(split[1]); - setIdStr(params.get("id") ?? ""); - setPrivkeyStr(params.get("key") ?? ""); - }, []); - - // if image url starts with `ipfs://...`, we grab the CID and return it with Larry's pinata gateway - // otherwise, we return the url unmodified - function parseImageUrl(url: string) { - const ipfsPrefix = "ipfs://"; - if (url.startsWith(ipfsPrefix)) { - const cid = url.slice(ipfsPrefix.length); - return `https://ipfs-gw.stargaze-apis.com/ipfs/${cid}`; - } else { - return url; - } - } - - async function getMintMsg() { - const privKey = Buffer.from(privkeyStr, "hex"); - const msg = `claim badge ${idStr} for user ${owner}`; - const msgBytes = Buffer.from(msg, "utf8"); - const msgHashBytes = sha256(msgBytes); - const { signature } = secp256k1.ecdsaSign(msgHashBytes, privKey); - - const badge = await store.getBadge(Number(idStr)); - - if (badge.rule === "by_keys") { - return { - mint_by_keys: { - id: Number(idStr), - owner, - pubkey: Buffer.from(secp256k1.publicKeyCreate(privKey)).toString("hex"), - signature: Buffer.from(signature).toString("hex"), - }, - }; - } else if ("by_key" in badge.rule) { - return { - mint_by_key: { - id: Number(idStr), - owner, - signature: Buffer.from(signature).toString("hex"), - }, - }; - } else { - return { - mint_by_minter: { - id: Number(idStr), - owners: [owner], - }, - }; - } - } - - // when user closes the tx modal, we reset the page: revert back to the credentials page, and - // empty the inputs - function onClosingTxModal() { - setPage(Page.Credential); - setIdStr(""); - setIdValid(null); - setPrivkeyStr(""); - setPrivkeyValid(null); - setOwner(""); - setOwnerValid(null); - onTxModalClose(); - } - - const credentialPage = ( - - - 1️⃣ Enter your claim credentials - - - - - { - if (!!result) { - const text = result.getText(); - - const split = text.split("?"); - if (split.length !== 2) { - return alert(`!! invalid QR !!\nnot a valid URL with a query string: ${text}`); - } - - const params = new URLSearchParams(split[1]); - if (!(params.has("id") && params.has("key"))) { - return alert( - `!! invalid QR !!\nquery string does not contain both parameters "id" and "key"` - ); - } - - setIdStr(params.get("id")!); - setPrivkeyStr(params.get("key")!); - onCameraClose(); - } - }} - videoContainerStyle={{ - padding: "0", - borderRadius: "var(--chakra-radii-lg)", - // TODO: The video takes a 1-2 seconds to load. At the mean time the box shadow is - // very ugly. Is there any way to delay the displaying of box shadow after the video - // is loaded? - boxShadow: "var(--chakra-shadows-dark-lg)", - }} - videoStyle={{ - position: "relative", - borderRadius: "var(--chakra-radii-lg)", - }} - /> - - - - - or - - Enter manually: - - id - { - setIdStr(event.target.value); - }} - /> - {idValid !== null ? ( - idValid ? ( - ✅ valid id - ) : ( - {idInvalidReason} - ) - ) : null} - - - key - { - setPrivkeyStr(event.target.value); - }} - /> - {privkeyValid !== null ? ( - privkeyValid ? ( - ✅ valid key - ) : ( - {privkeyInvalidReason} - ) - ) : null} - - - - - ); - - const previewPage = ( - - 2️⃣ Preview of your badge - badge-image - - - - name - - {badge?.metadata.name ?? fillerText} - - - - description - - - {badge?.metadata.description ?? fillerText} - - - {/* - - creator - - {badge?.manager ?? fillerText} - */} - - - current supply - - {badge?.current_supply ?? fillerText} - - - - max supply - - {badge ? badge.max_supply ?? "No max supply" : fillerText} - - - - minting deadline - - - {badge ? (badge.expiry ? formatTimestamp(badge.expiry) : "No deadline") : fillerText} - - - - - - - ); - - const submitPage = ( - - 3️⃣ Claim now! - - Your Stargaze address - { - setOwner(event.target.value); - }} - /> - {ownerValid !== null ? ( - ownerValid ? ( - ✅ valid address - ) : ( - {ownerInvalidReason} - ) - ) : ( - - Unfortunately, autofill by connecting to a wallet app isn't supported yet. Please - copy-paste your address here. - - )} - - - - - - ); - - const pages = { - [Page.Credential]: credentialPage, - [Page.Preview]: previewPage, - [Page.Submit]: submitPage, - }; - - return
{pages[page]}
; - } - \ No newline at end of file diff --git a/components/apps/poap/externalLinkIcon.tsx b/components/apps/poap/externalLinkIcon.tsx deleted file mode 100644 index 70efc01..0000000 --- a/components/apps/poap/externalLinkIcon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react"; -import { Icon, IconProps } from "@chakra-ui/react"; - -export default function ExternalLinkIcon(props: IconProps) { - return ( - - - - - - - - ); -} diff --git a/components/apps/poap/index.ts b/components/apps/poap/index.ts deleted file mode 100644 index be97c37..0000000 --- a/components/apps/poap/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './claim' -export * from './poapDashboard' diff --git a/components/apps/poap/modalWrapper.tsx b/components/apps/poap/modalWrapper.tsx deleted file mode 100644 index 96fb213..0000000 --- a/components/apps/poap/modalWrapper.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React,{ FC, ReactNode } from "react"; -import { - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalBody, - ModalCloseButton, - Flex, -} from "@chakra-ui/react"; - -type Props = { - showHeader?: boolean; - children?: ReactNode; - title?: string; - isOpen: boolean; - onClose: () => void; -}; - -const ModalWrapper: FC = ({ showHeader = true, children, isOpen, onClose, title = "" }) => { - return ( - - - - {showHeader ? ( - - - {title} - - - - ) : null} - {children} - - - ); -}; - -export default ModalWrapper; diff --git a/components/apps/poap/poapDashboard.tsx b/components/apps/poap/poapDashboard.tsx deleted file mode 100644 index 93af07b..0000000 --- a/components/apps/poap/poapDashboard.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import Head from "next/head"; -import { NextPage } from "next"; - -import Claim from "./claim"; -import React from "react"; - -const PoapContent: NextPage = () => { - return ( - <> - - badges | claim - - - - - ); -}; - -export default PoapContent; diff --git a/components/apps/poap/scanIcon.tsx b/components/apps/poap/scanIcon.tsx deleted file mode 100644 index ee396fa..0000000 --- a/components/apps/poap/scanIcon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon, IconProps } from "@chakra-ui/react"; -import React from "react"; - -export default function ScanIcon(props: IconProps) { - return ( - - - - ); -} diff --git a/components/apps/poap/txFailedIcon.tsx b/components/apps/poap/txFailedIcon.tsx deleted file mode 100644 index 2399e5e..0000000 --- a/components/apps/poap/txFailedIcon.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Icon, IconProps } from "@chakra-ui/react"; -import React from "react"; - -export default function TxFailedIcon(props: IconProps) { - return ( - - - - - ); -} diff --git a/components/apps/poap/txModal.tsx b/components/apps/poap/txModal.tsx deleted file mode 100644 index 3b9100a..0000000 --- a/components/apps/poap/txModal.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { Box, Button, Flex, Link, Spinner, Text } from "@chakra-ui/react"; -import React,{ FC, useState, useEffect } from "react"; - -import ModalWrapper from "./modalWrapper"; -import SuccessIcon from "./txSuccessIcon"; -import FailedIcon from "./txFailedIcon"; -import ExternalLinkIcon from "./externalLinkIcon"; -import { truncateString } from "../helpers"; -import { useStore } from "../store"; - -function SpinnerWrapper() { - return ( - - ); -} - -function TxHashText(txhash: string, url: string) { - return ( - - - Tx Hash - - - {truncateString(txhash, 6, 6)} - - - - ); -} - -function TxFailedText(error: any) { - return ( - - {error} - - ); -} - -function CloseButton(showCloseBtn: boolean, onClick: () => void) { - return showCloseBtn ? ( - - ) : null; -} - -type Props = { - getMsg: () => Promise; - isOpen: boolean; - onClose: () => void; -}; - -const TxModal: FC = ({ getMsg, isOpen, onClose }) => { - const store = useStore(); - const [showCloseBtn, setShowCloseBtn] = useState(false); - const [txStatusHeader, setTxStatusHeader] = useState(); - const [txStatusIcon, setTxStatusIcon] = useState(); - const [txStatusDetail, setTxStatusDetail] = useState(); - - useEffect(() => { - setTxStatusHeader("Broadcasting Transaction..."); - setTxStatusIcon(SpinnerWrapper()); - setTxStatusDetail(Should be done in a few seconds 😉); - setShowCloseBtn(false); - }, [isOpen]); - - useEffect(() => { - if (isOpen) { - getMsg() - .then((msg) => { - console.log("created execute msg:", msg); - store - .wasmClient!.execute(store.senderAddr!, store.networkConfig!.hub, msg, "auto", "", []) - .then((result) => { - setTxStatusHeader("Transaction Successful"); - setTxStatusDetail( - TxHashText( - result.transactionHash, - store.networkConfig!.getExplorerUrl(result.transactionHash) - ) - ); - setTxStatusIcon(); - setShowCloseBtn(true); - }) - .catch((error) => { - setTxStatusHeader("Transaction Failed"); - setTxStatusIcon(); - setTxStatusDetail(TxFailedText(error)); - setShowCloseBtn(true); - }); - }) - .catch((error) => { - setTxStatusHeader("Transaction Failed"); - setTxStatusIcon(); - setTxStatusDetail(TxFailedText(error)); - setShowCloseBtn(true); - }); - } - }, [isOpen]); - - return ( - - - - {txStatusHeader} - - - {txStatusIcon} - - - {txStatusDetail} - {CloseButton(showCloseBtn, onClose)} - - - - ); -}; - -export default TxModal; diff --git a/components/apps/poap/txSuccessIcon.tsx b/components/apps/poap/txSuccessIcon.tsx deleted file mode 100644 index 5a3771c..0000000 --- a/components/apps/poap/txSuccessIcon.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Icon, IconProps } from "@chakra-ui/react"; -import React from "react"; - -export default function TxSuccessIcon(props: IconProps) { - return ( - - - - - ); -} diff --git a/components/apps/staking/hooks/index.ts b/components/apps/staking/hooks/index.ts new file mode 100644 index 0000000..a72eead --- /dev/null +++ b/components/apps/staking/hooks/index.ts @@ -0,0 +1,3 @@ +export * from './useInputBox'; +export * from './useFeeEstimation'; +export * from './useTransactionToast'; \ No newline at end of file diff --git a/components/apps/staking/hooks/useFeeEstimation.ts b/components/apps/staking/hooks/useFeeEstimation.ts new file mode 100644 index 0000000..26f29d8 --- /dev/null +++ b/components/apps/staking/hooks/useFeeEstimation.ts @@ -0,0 +1,45 @@ +import { EncodeObject } from '@cosmjs/proto-signing'; +import { ChainName } from '@cosmos-kit/core'; +import { useChain } from '@cosmos-kit/react'; +import { GasPrice, calculateFee } from '@cosmjs/stargate'; +import { getGasCoin, getStakeCoin } from '../../../../config'; + +export const useFeeEstimation = (chainName: ChainName) => { + const { getSigningStargateClient, chain } = useChain(chainName); + + const gasPrice = chain.fees?.fee_tokens[0].average_gas_price || 0.025; + + const gascoin = getGasCoin(chainName); + const stakecoin = getStakeCoin(chainName); + + + const estimateFee = async ( + address: string, + messages: EncodeObject[], + modifier?: number, + memo?: string + ) => { + const stargateClient = await getSigningStargateClient(); + if (!stargateClient) { + throw new Error('getSigningStargateClient error'); + } + + const gasEstimation = await stargateClient.simulate( + address, + messages, + memo + ); + if (!gasEstimation) { + throw new Error('estimate gas error'); + } + + const fee = calculateFee( + Math.round(gasEstimation * (modifier || 1.5)), + GasPrice.fromString(gasPrice + gascoin.base) + ); + + return fee; + }; + + return { estimateFee }; +}; \ No newline at end of file diff --git a/components/apps/staking/hooks/useInputBox.tsx b/components/apps/staking/hooks/useInputBox.tsx new file mode 100644 index 0000000..f655999 --- /dev/null +++ b/components/apps/staking/hooks/useInputBox.tsx @@ -0,0 +1,101 @@ +import { ChangeEvent, useState } from 'react'; +import { + Text, + Input, + InputGroup, + InputRightElement, + Button, +} from '@chakra-ui/react'; +import { StatBox } from '../react/delegate-modal'; +import React from 'react'; + +export const InputBox = ({ + label, + token, + value, + onChange, + onMaxClick, + isMaxBtnLoading = false, +}: { + label: string; + token: string; + value: number | string; + onChange: (event: React.ChangeEvent) => void; + onMaxClick: () => void; + isMaxBtnLoading?: boolean; +}) => ( + + + + + + {token} + + + + } + /> +); + +export const useInputBox = (maxAmount?: number) => { + const [amount, setAmount] = useState(''); + const [max, setMax] = useState(maxAmount || 0); + + const handleInputChange = (e: ChangeEvent) => { + if (Number(e.target.value) > max) { + setAmount(max); + return; + } + + if (e.target.value === '') { + setAmount(''); + return; + } + + setAmount(+Number(e.target.value).toFixed(6)); + }; + + const renderInputBox = ( + label: string, + token: string, + onMaxClick?: () => void, + isLoading?: boolean + ) => { + return ( + handleInputChange(e)} + onMaxClick={() => (onMaxClick ? onMaxClick() : setAmount(max))} + /> + ); + }; + + return { renderInputBox, amount, setAmount, setMax }; +}; \ No newline at end of file diff --git a/components/apps/staking/hooks/useTransactionToast.tsx b/components/apps/staking/hooks/useTransactionToast.tsx new file mode 100644 index 0000000..7d143c4 --- /dev/null +++ b/components/apps/staking/hooks/useTransactionToast.tsx @@ -0,0 +1,31 @@ +import { useToast, Text, Box } from '@chakra-ui/react'; +import { TransactionResult } from '../../../../types'; +import React from 'react'; + +export const useTransactionToast = () => { + const toast = useToast({ + position: 'top-right', + containerStyle: { + maxWidth: '150px', + }, + }); + + const showToast = (code: number, res?: any) => { + toast({ + title: `Transaction ${ + code === TransactionResult.Success ? 'successful' : 'failed' + }`, + status: code === TransactionResult.Success ? 'success' : 'error', + duration: code === TransactionResult.Success ? 5000 : 20000, + isClosable: true, + description: ( + + {res?.message} + {res?.rawLog} + + ), + }); + }; + + return { showToast }; +}; \ No newline at end of file diff --git a/components/apps/staking/index.tsx b/components/apps/staking/index.tsx new file mode 100644 index 0000000..bf27123 --- /dev/null +++ b/components/apps/staking/index.tsx @@ -0,0 +1,137 @@ +import Head from 'next/head'; +import { useState } from 'react'; +import { + Button, + ButtonGroup, + Box, + Card, + CardBody, + CardHeader, + Container, + Flex, + Heading, + Link, + Icon, + SimpleGrid, + Stack, + StackDivider, + Text, + useColorMode, + VStack, +} from '@chakra-ui/react'; +import { StakingSection } from './react'; +import { defaultChainName } from '../../../config'; +import { WalletStatus } from '@cosmos-kit/core'; +import { useChain } from '@cosmos-kit/react'; +import { BsFillMoonStarsFill, BsFillSunFill } from 'react-icons/bs'; +import { FaUserCircle } from 'react-icons/fa'; +import { IoWalletOutline } from 'react-icons/io5'; +import { ChainWalletCard } from '../../../components'; +import React from 'react'; + + +export default function StakingContent() { + const { username, connect, disconnect, wallet } = useChain('terpnetwork'); + const [globalStatus, setGlobalStatus] = useState( + WalletStatus.Disconnected + ); + + const getGlobalButton = () => { + if (globalStatus === 'Connecting') { + return ( + + + + + ); + } + + return ( + + ); + }; + + return ( + + + Delegate Terps + + + + + + {getGlobalButton()} + + + + + + + + + + + Built + With + + Cosmology + + + + + ); +}; \ No newline at end of file diff --git a/components/apps/staking/react/address-card.tsx b/components/apps/staking/react/address-card.tsx new file mode 100644 index 0000000..8e601bd --- /dev/null +++ b/components/apps/staking/react/address-card.tsx @@ -0,0 +1,199 @@ +import { + Box, + Button, + Icon, + Image, + Text, + useClipboard, + useColorMode, + } from "@chakra-ui/react"; + import { WalletStatus } from "@cosmos-kit/core"; + import React, { ReactNode, useEffect, useState } from "react"; + import { FaCheckCircle } from "react-icons/fa"; + import { FiCopy } from "react-icons/fi"; + + import { CopyAddressType } from "../../../types"; + import { handleChangeColorModeValue } from "./handleChangeColor"; + + const SIZES = { + lg: { + height: 12, + walletImageSize: 7, + icon: 5, + fontSize: "md", + }, + md: { + height: 10, + walletImageSize: 6, + icon: 4, + fontSize: "sm", + }, + sm: { + height: 7, + walletImageSize: 5, + icon: 3.5, + fontSize: "sm", + }, + }; + + export function stringTruncateFromCenter(str: string, maxLength: number) { + const midChar = "…"; // character to insert into the center of the result + + if (str.length <= maxLength) return str; + + // length of beginning part + const left = Math.ceil(maxLength / 2); + + // start index of ending part + const right = str.length - Math.floor(maxLength / 2) + 1; + + return str.substring(0, left) + midChar + str.substring(right); + } + + export const ConnectedShowAddress = ({ + address, + walletIcon, + isLoading, + isRound, + size = "md", + maxDisplayLength, + }: CopyAddressType) => { + const { hasCopied, onCopy } = useClipboard(address ? address : ""); + const [displayAddress, setDisplayAddress] = useState(""); + const { colorMode } = useColorMode(); + const defaultMaxLength = { + lg: 14, + md: 16, + sm: 18, + }; + + useEffect(() => { + if (!address) setDisplayAddress("address not identified yet"); + if (address && maxDisplayLength) + setDisplayAddress(stringTruncateFromCenter(address, maxDisplayLength)); + if (address && !maxDisplayLength) + setDisplayAddress( + stringTruncateFromCenter( + address, + defaultMaxLength[size as keyof typeof defaultMaxLength] + ) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [address]); + + return ( + + ); + }; + + export const CopyAddressBtn = ({ + walletStatus, + connected, + }: { + walletStatus: WalletStatus; + connected: ReactNode; + }) => { + switch (walletStatus) { + case "Connected": + return <>{connected}; + default: + return ( + + + + ); + } + }; \ No newline at end of file diff --git a/components/apps/staking/react/all-validators.tsx b/components/apps/staking/react/all-validators.tsx new file mode 100644 index 0000000..00cc982 --- /dev/null +++ b/components/apps/staking/react/all-validators.tsx @@ -0,0 +1,369 @@ +import { Token } from './stats'; +import { IoArrowForward } from 'react-icons/io5'; +import { + Heading, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + Button, + Box, + Icon, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + useDisclosure, + Stack, + Text, + Image, + useColorMode, + Center, +} from '@chakra-ui/react'; +import { + DelegateWarning, + StatBox, + ValidatorDesc, + ValidatorInfo, +} from './delegate-modal'; +import { useState } from 'react'; +import { exponentiate, getExponent } from './staking'; +import { useChain } from '@cosmos-kit/react'; +import { cosmos } from 'interchain'; +import { getStakeCoin,getGasCoin } from '../../../../config'; +import { StdFee } from '@cosmjs/amino'; +import type { + Validator, + DelegationResponse as Delegation, +} from 'interchain/types/codegen/cosmos/staking/v1beta1/staking'; +import { TransactionResult } from '../../../../types'; +import { ChainName } from '@cosmos-kit/core'; +import BigNumber from 'bignumber.js'; +import { + useFeeEstimation, + useInputBox, + useTransactionToast, +} from '../hooks'; + +const { delegate } = cosmos.staking.v1beta1.MessageComposer.fromPartial; + +export const Thumbnail = ({ + identity, + name, + thumbnailUrl, +}: { + identity: string | undefined; + name: string | undefined; + thumbnailUrl: string; +}) => { + return ( + <> + {identity && thumbnailUrl ? ( + {name} + ) : ( +
+ {name && name.trim().slice(0, 1).toUpperCase()} +
+ )} + + ); +}; + +export type MaxAmountAndFee = { + maxAmount: string; + fee: StdFee; +}; + +const AllValidators = ({ + validators, + balance, + delegations, + updateData, + unbondingDays, + chainName, + thumbnails, +}: { + validators: Validator[]; + balance: number; + delegations: Delegation[]; + updateData: () => void; + unbondingDays: number; + chainName: ChainName; + thumbnails: { + [key: string]: string; + }; +}) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + const { getSigningStargateClient, address } = useChain(chainName); + const { renderInputBox, amount, setAmount } = useInputBox(balance); + const [currentValidator, setCurrentValidator] = useState(); + const [isDelegating, setIsDelegating] = useState(false); + const [isSimulating, setIsSimulating] = useState(false); + const [maxAmountAndFee, setMaxAmountAndFee] = useState(); + + const stakecoin = getStakeCoin(chainName); + const gascoin = getGasCoin(chainName); + const exp = getExponent(chainName); + + const { colorMode } = useColorMode(); + const { showToast } = useTransactionToast(); + const { estimateFee } = useFeeEstimation(chainName); + + const getDelegation = (validatorAddr: string, delegations: Delegation[]) => { + const delegation = delegations.filter( + (d) => d?.delegation?.validatorAddress === validatorAddr + ); + + if (delegation.length === 1) { + return exponentiate(delegation[0].balance!.amount, -exp); + } + + return 0; + }; + + const onModalClose = () => { + setAmount(''); + setIsDelegating(false); + onClose(); + setIsSimulating(false); + }; + + const onDelegateClick = async () => { + setIsDelegating(true); + + const stargateClient = await getSigningStargateClient(); + + if (!stargateClient || !address || !currentValidator?.operatorAddress) { + console.error('stargateClient undefined or address undefined.'); + return; + } + + const delegationAmount = new BigNumber(amount).shiftedBy(exp).toString(); + + const msg = delegate({ + delegatorAddress: address, + validatorAddress: currentValidator.operatorAddress, + amount: { + amount: delegationAmount, + denom: stakecoin.base, + }, + }); + + const isMaxAmountAndFeeExists = + maxAmountAndFee && + new BigNumber(amount).isEqualTo(maxAmountAndFee.maxAmount); + + try { + const fee = isMaxAmountAndFeeExists + ? maxAmountAndFee.fee + : await estimateFee(address, [msg]); + const res = await stargateClient.signAndBroadcast(address, [msg], fee); + showToast(res.code); + updateData(); + setTimeout(() => { + onModalClose(); + }, 1000); + } catch (error) { + console.log(error); + showToast(TransactionResult.Failed, error); + } finally { + stargateClient.disconnect(); + setIsDelegating(false); + setMaxAmountAndFee(undefined); + } + }; + + const handleMaxClick = async () => { + if (!address || !currentValidator) return; + + if (Number(balance) === 0) { + setAmount(0); + return; + } + + setIsSimulating(true); + + const delegationAmount = new BigNumber(balance).shiftedBy(exp).toString(); + const msg = delegate({ + delegatorAddress: address, + validatorAddress: currentValidator.operatorAddress, + amount: { + amount: delegationAmount, + denom: stakecoin.base, + }, + }); + + try { + const fee = await estimateFee(address, [msg]); + const feeAmount = new BigNumber(fee.amount[0].amount).shiftedBy(-exp); + const balanceAfterFee = new BigNumber(balance) + .minus(feeAmount) + .toString(); + setMaxAmountAndFee({ fee, maxAmount: balanceAfterFee }); + setAmount(balanceAfterFee); + } catch (error) { + console.log(error); + } finally { + setIsSimulating(false); + } + }; + + const isAmountZero = new BigNumber(amount || 0).isEqualTo(0); + + return ( + <> + + All Validators + + + + + + Validator + + + + + + + + + + + {renderInputBox( + 'Amount to Delegate', + stakecoin.symbol, + handleMaxClick, + isSimulating + )} + + + + + + + + + + + + + + + + + + + + + {validators.map((validator: Validator, index: number) => ( + + + + + + + ))} + +
ValidatorVoting PowerCommissionAPR
+ + {index + 1} + + {validator?.description?.moniker} + + + {Math.floor( + exponentiate(validator.tokens, -exp) + ).toLocaleString()} +   + + + {validator.commission?.commissionRates?.rate && + exponentiate( + validator.commission.commissionRates.rate, + -16 + ).toFixed(0)} + % + + + {/* {validator.apr} */} + Live Data Coming Soon + + +
+
+ + ); +}; + +export default AllValidators; \ No newline at end of file diff --git a/components/apps/staking/react/chain-dropdown.tsx b/components/apps/staking/react/chain-dropdown.tsx new file mode 100644 index 0000000..3d0f60e --- /dev/null +++ b/components/apps/staking/react/chain-dropdown.tsx @@ -0,0 +1,277 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import React from 'react'; +import { + Box, + Text, + Stack, + useColorModeValue, + Image, + Icon, + useBreakpointValue, + SystemStyleObject, + SkeletonCircle, + Skeleton +} from '@chakra-ui/react'; +import { Searcher } from 'fast-fuzzy'; +import { FiChevronDown } from 'react-icons/fi'; +import { + AsyncSelect, + OptionProps, + chakraComponents, + GroupBase, + DropdownIndicatorProps, + PlaceholderProps +} from 'chakra-react-select'; +import { + ChainOption, + ChangeChainDropdownType, + ChangeChainMenuType +} from '../../../types'; + +const SkeletonOptions = () => { + return ( + + + + + ); +}; + +const SelectOptions = ({ data, value, onChange }: ChangeChainMenuType) => { + const menuHeight = useBreakpointValue({ base: 60, md: 56 }); + const customStyles = { + control: (provided: SystemStyleObject) => ({ + ...provided, + height: 12 + }), + menu: (provided: SystemStyleObject) => ({ + ...provided, + h: menuHeight, + mt: 4, + mb: 0, + bg: useColorModeValue('white', 'gray.900'), + boxShadow: useColorModeValue('0 1px 5px #e3e3e3', '0 0px 4px #4b4b4b'), + borderRadius: '0.3rem' + }), + menuList: (provided: SystemStyleObject) => ({ + ...provided, + h: menuHeight, + bg: 'transparent', + border: 'none', + borderRadius: 'none', + p: 2, + // For Firefox + scrollbarWidth: 'auto', + scrollbarColor: useColorModeValue( + 'rgba(0,0,0,0.3) rgba(0,0,0,0.2)', + 'rgba(255,255,255,0.2) rgba(255,255,255,0.1)' + ), + // For Chrome and other browsers except Firefox + '&::-webkit-scrollbar': { + width: '14px', + background: useColorModeValue( + 'rgba(220,220,220,0.1)', + 'rgba(60,60,60,0.1)' + ), + borderRadius: '3px' + }, + '&::-webkit-scrollbar-thumb': { + background: useColorModeValue( + 'rgba(0,0,0,0.1)', + 'rgba(255,255,255,0.1)' + ), + borderRadius: '10px', + border: '3px solid transparent', + backgroundClip: 'content-box' + } + }), + clearIndicator: (provided: SystemStyleObject) => ({ + ...provided, + borderRadius: 'full', + color: useColorModeValue('blackAlpha.600', 'whiteAlpha.600') + }), + dropdownIndicator: (provided: SystemStyleObject) => ({ + ...provided, + bg: 'transparent', + pl: 1.5 + }), + option: ( + provided: SystemStyleObject, + state: { isSelected: boolean; isFocused: boolean } + ) => { + return { + ...provided, + borderRadius: 'lg', + h: 14, + color: 'inherit', + bg: useColorModeValue( + state.isSelected + ? state.isFocused + ? 'primary.200' + : 'primary.100' + : state.isFocused + ? 'blackAlpha.200' + : 'transparent', + state.isSelected + ? state.isFocused + ? 'primary.600' + : 'primary.500' + : state.isFocused + ? 'whiteAlpha.200' + : 'transparent' + ), + _notFirst: { + mt: 2 + }, + _active: { + bg: 'primary.50' + }, + _disabled: { bg: 'transparent', _hover: { bg: 'transparent' } } + }; + } + }; + const IndicatorSeparator = () => { + return null; + }; + const DropdownIndicator = ({ + ...props + }: DropdownIndicatorProps>) => { + return ( + + + + ); + }; + const Placeholder = (props: PlaceholderProps) => { + if (props.hasValue) { + return ( + + + + + + + {props.getValue()[0].label} + + + + ); + } + return ; + }; + const CustomOption = ({ + children, + ...props + }: OptionProps>) => { + return ( + + + + + + + {children} + + + + ); + }; + + return ( + option.isDisabled || false} + blurInputOnSelect={true} + controlShouldRenderValue={false} + loadingMessage={() => } + value={value} + defaultOptions={data} + loadOptions={(inputValue, callback) => { + const searcher = new Searcher(data, { + keySelector: (obj) => obj.label + }); + callback(searcher.search(inputValue)); + }} + onChange={onChange} + components={{ + DropdownIndicator, + IndicatorSeparator, + Placeholder, + Option: CustomOption + }} + /> + ); +}; + +export const ChangeChainDropdown = ({ + data, + selectedItem, + onChange +}: ChangeChainDropdownType) => { + return ( + + + + ); +}; \ No newline at end of file diff --git a/components/apps/staking/react/choose-chain.tsx b/components/apps/staking/react/choose-chain.tsx new file mode 100644 index 0000000..934a97a --- /dev/null +++ b/components/apps/staking/react/choose-chain.tsx @@ -0,0 +1,33 @@ +import React,{ useState, useEffect } from 'react'; +import { ChangeChainDropdown } from './chain-dropdown'; +import { + ChooseChainInfo, + ChainOption, + handleSelectChainDropdown +} from '../../../types'; + +export function ChooseChain({ + chainName, + chainInfos, + onChange +}: { + chainName?: string; + chainInfos: ChooseChainInfo[]; + onChange: handleSelectChainDropdown; +}) { + const [selectedItem, setSelectedItem] = useState(); + useEffect(() => { + if (chainName && chainInfos.length > 0) + setSelectedItem( + chainInfos.filter((options) => options.chainName === chainName)[0] + ); + if (!chainName) setSelectedItem(undefined); + }, [chainInfos, chainName]); + return ( + + ); +} \ No newline at end of file diff --git a/components/apps/staking/react/delegate-modal.tsx b/components/apps/staking/react/delegate-modal.tsx new file mode 100644 index 0000000..fb5d0a6 --- /dev/null +++ b/components/apps/staking/react/delegate-modal.tsx @@ -0,0 +1,162 @@ +import React,{ ReactElement } from 'react'; +import { Token } from './stats'; +import { + Flex, + Heading, + Stack, + Text, + Image, + AlertDescription, + Alert, + AlertIcon, + AlertTitle, + Box, + Stat, + StatLabel, + StatNumber, + ListItem, + UnorderedList, + useColorModeValue, + Center, +} from '@chakra-ui/react'; + + +export const ValidatorInfo = ({ + imgUrl, + name, + commission, + apr, +}: { + imgUrl: string; + name: string; + commission: number | string; + apr: number; +}) => ( + + {imgUrl ? ( + {name} + ) : ( +
+ {name.slice(0, 1).toUpperCase()} +
+ )} + + + {name} + + + Commission {commission}% | APR {apr}% + + +
+); + +export const ValidatorDesc = ({ description }: { description: string }) => ( + {description} +); + +export const DelegateWarning = ({ + unbondingDays, +}: { + unbondingDays: number; +}) => { + if (!unbondingDays) return <>; + + return ( + + + + + Staking will lock your funds for {unbondingDays} days + + + + You will need to undelegate in order for your staked assets to be liquid + again. This process will take {unbondingDays} days to complete. + + + ); +}; + +export const UndelegateWarning = ({ + unbondingDays, +}: { + unbondingDays: number; +}) => { + if (!unbondingDays) return <>; + + return ( + + + + + Once the unbonding period begins you will: + + + + + not receive staking rewards + not be able to cancel the unbonding + + need to wait {unbondingDays} days for the amount to be liquid + + + + + ); +}; + +export const StatBox = ({ + label, + number, + input, + token, +}: { + label: string; + number?: number; + input?: ReactElement; + token: string; +}) => { + return ( + + + {label} + {input ? ( + input + ) : ( + + {number} + + )} + + + ); +}; \ No newline at end of file diff --git a/components/apps/staking/react/handleChangeColor.tsx b/components/apps/staking/react/handleChangeColor.tsx new file mode 100644 index 0000000..7c7ff46 --- /dev/null +++ b/components/apps/staking/react/handleChangeColor.tsx @@ -0,0 +1,9 @@ +// use for let color mode value fit Rules of Hooks +export function handleChangeColorModeValue( + colorMode: string, + light: any, + dark: any + ) { + if (colorMode === "light") return light; + if (colorMode === "dark") return dark; + } \ No newline at end of file diff --git a/components/apps/staking/react/index.ts b/components/apps/staking/react/index.ts new file mode 100644 index 0000000..66d228b --- /dev/null +++ b/components/apps/staking/react/index.ts @@ -0,0 +1,7 @@ +export * from './wallet-connect'; +export * from './warn-block'; +export * from './user-card'; +export * from './address-card'; +export * from './staking'; +export * from './choose-chain'; +export * from './chain-dropdown'; diff --git a/components/apps/staking/react/my-validators.tsx b/components/apps/staking/react/my-validators.tsx new file mode 100644 index 0000000..eea71ea --- /dev/null +++ b/components/apps/staking/react/my-validators.tsx @@ -0,0 +1,735 @@ +import { + Heading, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + Button, + Box, + Icon, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Stack, + useDisclosure, + Text, + Image, + useColorMode, + } from '@chakra-ui/react'; + import { Token } from './stats'; + import { IoArrowForward } from 'react-icons/io5'; + import { + ValidatorInfo, + ValidatorDesc, + DelegateWarning, + StatBox, + UndelegateWarning, + } from './delegate-modal'; + import { exponentiate, getExponent } from './staking'; + import { decodeCosmosSdkDecFromProto } from '@cosmjs/stargate'; + import React,{ useState } from 'react'; + import { cosmos } from 'interchain'; + import { useChain } from '@cosmos-kit/react'; + import { getGasCoin, getStakeCoin } from '../../../../config'; + import { MyValidator, TransactionResult } from '../../../../types'; + import type { + Validator, + DelegationResponse as Delegation, + } from 'interchain/types/codegen/cosmos/staking/v1beta1/staking'; + import type { DelegationDelegatorReward as Reward } from 'interchain/types/codegen/cosmos/distribution/v1beta1/distribution'; + import { ChainName } from '@cosmos-kit/core'; + import { MaxAmountAndFee, Thumbnail } from './all-validators'; + import { + useFeeEstimation, + useInputBox, + useTransactionToast, + } from '../hooks'; + import BigNumber from 'bignumber.js'; + + const { delegate } = cosmos.staking.v1beta1.MessageComposer.fromPartial; + const { undelegate } = cosmos.staking.v1beta1.MessageComposer.fromPartial; + const { beginRedelegate } = cosmos.staking.v1beta1.MessageComposer.fromPartial; + + const isAmountZero = (amount: string | number | undefined) => + new BigNumber(amount || 0).isEqualTo(0); + + const MyValidators = ({ + validators, + allValidator, + delegations, + rewards, + balance, + updateData, + unbondingDays, + chainName, + thumbnails, + }: { + validators: Validator[]; + allValidator: Validator[]; + delegations: Delegation[]; + rewards: Reward[]; + balance: number; + updateData: () => void; + unbondingDays: number; + chainName: ChainName; + thumbnails: { + [key: string]: string; + }; + }) => { + const { getSigningStargateClient, address } = useChain(chainName); + + const [isDelegating, setIsDelegating] = useState(false); + const [isUndelegating, setIsUndelegating] = useState(false); + const [isRedelegating, setIsRedelegating] = useState(false); + const [currentValidator, setCurrentValidator] = useState(); + const [selectedValidator, setSelectedValidator] = useState(); + const [isSimulating, setIsSimulating] = useState(false); + const [maxDelegateAmountAndFee, setMaxDelegateAmountAndFee] = + useState(); + + const gascoin = getGasCoin(chainName); + const stakecoin = getStakeCoin(chainName); + + const exp = getExponent(chainName); + + const { colorMode } = useColorMode(); + const { showToast } = useTransactionToast(); + const { estimateFee } = useFeeEstimation(chainName); + + const { + renderInputBox: renderDelegateInputBox, + amount: delegateAmount, + setAmount: setDelegateAmount, + } = useInputBox(balance); + + const { + renderInputBox: renderUndelegateInputBox, + amount: undelegateAmount, + setAmount: setUndelegateAmount, + setMax: setMaxUndelegateAmount, + } = useInputBox(); + + const { + renderInputBox: renderRedelegateInputBox, + amount: redelegateAmount, + setAmount: setRedelegateAmount, + setMax: setMaxRedelegateAmount, + } = useInputBox(); + + const { + isOpen: isValidatorModalOpen, + onOpen: onValidatorModalOpen, + onClose: onValidatorModalClose, + } = useDisclosure(); + + const { + isOpen: isDelegateModalOpen, + onOpen: onDelegateModalOpen, + onClose: onDelegateModalClose, + } = useDisclosure(); + + const { + isOpen: isUndelegateModalOpen, + onOpen: onUndelegateModalOpen, + onClose: onUndelegateModalClose, + } = useDisclosure(); + + const { + isOpen: isSelectValidatorModalOpen, + onOpen: onSelectValidatorModalOpen, + onClose: onSelectValidatorModalClose, + } = useDisclosure(); + + const { + isOpen: isRedelegateModalOpen, + onOpen: onRedelegateModalOpen, + onClose: onRedelegateModalClose, + } = useDisclosure(); + + const myValidators = validators.map((validator: Validator) => { + const delegation = delegations.filter( + (d) => d?.delegation?.validatorAddress === validator?.operatorAddress + )[0]; + + const delegatorReward = rewards.filter( + (r) => r.validatorAddress === validator?.operatorAddress + )[0]; + + const reward = delegatorReward.reward.find( + (item) => item.denom === gascoin.base + ); + + const rewardAmount = + delegatorReward.reward.length > 0 + ? decodeCosmosSdkDecFromProto(reward ? reward.amount : '0').toString() + : 0; + + return { + details: validator?.description?.details, + name: validator?.description?.moniker, + identity: validator?.description?.identity, + address: validator.operatorAddress, + staked: exponentiate(delegation.balance!.amount, -exp), + reward: Number(exponentiate(rewardAmount, -exp).toFixed(6)), + commission: validator?.commission?.commissionRates?.rate, + }; + }); + + const closeDelegateModal = () => { + setDelegateAmount(''); + setIsDelegating(false); + onDelegateModalClose(); + }; + + const closeUndelegateModal = () => { + setUndelegateAmount(''); + setIsUndelegating(false); + onUndelegateModalClose(); + }; + + const closeRedelegateModal = () => { + setRedelegateAmount(''); + setIsRedelegating(false); + onRedelegateModalClose(); + }; + + const onDelegateClick = async () => { + setIsDelegating(true); + + const stargateClient = await getSigningStargateClient(); + + if (!stargateClient || !address || !currentValidator?.address) { + console.error('stargateClient undefined or address undefined.'); + return; + } + + const amountToDelegate = (Number(delegateAmount) * 10 ** exp).toString(); + + const msg = delegate({ + delegatorAddress: address, + validatorAddress: currentValidator.address, + amount: { + amount: amountToDelegate, + denom: stakecoin.base, + }, + }); + + const isMaxAmountAndFeeExists = + maxDelegateAmountAndFee && + new BigNumber(delegateAmount).isEqualTo( + maxDelegateAmountAndFee.maxAmount + ); + + try { + const fee = isMaxAmountAndFeeExists + ? maxDelegateAmountAndFee.fee + : await estimateFee(address, [msg]); + const res = await stargateClient.signAndBroadcast(address, [msg], fee); + showToast(res.code); + onValidatorModalClose(); + updateData(); + setTimeout(() => { + closeDelegateModal(); + }, 1000); + } catch (error) { + console.log(error); + showToast(TransactionResult.Failed, error); + } finally { + stargateClient.disconnect(); + setIsDelegating(false); + } + }; + + const handleMaxClick = async () => { + if (!address || !currentValidator) return; + + if (Number(balance) === 0) { + setDelegateAmount(0); + return; + } + + setIsSimulating(true); + + const delegationAmount = new BigNumber(balance).shiftedBy(exp).toString(); + const msg = delegate({ + delegatorAddress: address, + validatorAddress: currentValidator.address, + amount: { + amount: delegationAmount, + denom: stakecoin.base, + }, + }); + + try { + const fee = await estimateFee(address, [msg]); + const feeAmount = new BigNumber(fee.amount[0].amount).shiftedBy(-exp); + const balanceAfterFee = new BigNumber(balance) + .minus(feeAmount) + .toString(); + setMaxDelegateAmountAndFee({ fee, maxAmount: balanceAfterFee }); + setDelegateAmount(balanceAfterFee); + } catch (error) { + console.log(error); + } finally { + setIsSimulating(false); + } + }; + + const onUndelegateClick = async () => { + setIsUndelegating(true); + + const stargateClient = await getSigningStargateClient(); + + if (!stargateClient || !address || !currentValidator?.address) { + console.error('stargateClient undefined or address undefined.'); + return; + } + + const amountToUndelegate = ( + Number(undelegateAmount) * + 10 ** exp + ).toString(); + + const msg = undelegate({ + delegatorAddress: address, + validatorAddress: currentValidator.address, + amount: { + amount: amountToUndelegate, + denom: stakecoin.base, + }, + }); + + try { + const fee = await estimateFee(address, [msg]); + const res = await stargateClient.signAndBroadcast(address, [msg], fee); + showToast(res.code); + onValidatorModalClose(); + updateData(); + setTimeout(() => { + closeUndelegateModal(); + }, 1000); + } catch (error) { + console.log(error); + showToast(TransactionResult.Failed, error); + } finally { + stargateClient.disconnect(); + setIsUndelegating(false); + } + }; + + const onRedelegateClick = async () => { + setIsRedelegating(true); + + const stargateClient = await getSigningStargateClient(); + + if ( + !stargateClient || + !address || + !currentValidator?.address || + !selectedValidator?.operatorAddress + ) { + console.error('stargateClient undefined or address undefined.'); + return; + } + + const amountToRedelegate = ( + Number(redelegateAmount) * + 10 ** exp + ).toString(); + + const msg = beginRedelegate({ + delegatorAddress: address, + validatorSrcAddress: currentValidator.address, + validatorDstAddress: selectedValidator.operatorAddress, + amount: { + denom: stakecoin.base, + amount: amountToRedelegate, + }, + }); + + try { + const fee = await estimateFee(address, [msg]); + const res = await stargateClient.signAndBroadcast(address, [msg], fee); + showToast(res.code); + updateData(); + setTimeout(() => { + closeRedelegateModal(); + }, 1000); + } catch (error) { + console.log(error); + showToast(TransactionResult.Failed, error); + } finally { + stargateClient.disconnect(); + setIsRedelegating(false); + } + }; + + return ( + <> + + My Validators + + + + + + Validator + + + + + + + + + + + + + + + + + + + + + Delegate + + + + + + + + + + {renderDelegateInputBox( + 'Amount to Delegate', + stakecoin.symbol, + handleMaxClick, + isSimulating + )} + + + + + + + + + + + + Undelegate + + + + + + + + {renderUndelegateInputBox('Amount to Undelegate', stakecoin.symbol)} + + + + + + + + + + + + + Redelegate to + + + + + + + + + + + + + + + {allValidator.map((validator: Validator, index: number) => ( + { + onRedelegateModalOpen(); + onSelectValidatorModalClose(); + setSelectedValidator(validator); + }} + _hover={{ + background: + colorMode === 'light' ? 'gray.100' : 'gray.800', + cursor: 'pointer', + }} + > + + + + + + ))} + +
ValidatorVoting PowerCommissionAPR
+ + {index + 1} + + {validator?.description?.moniker} + + + {Math.floor( + exponentiate(validator.tokens, -exp) + ).toLocaleString()} +   + + + {validator.commission?.commissionRates?.rate && + exponentiate( + validator.commission.commissionRates.rate, + -16 + ).toFixed(0)} + % + + + {/* {validator.apr} */} + Live Data Coming Soon + +
+
+
+
+
+ + + + + + + + + From + + {currentValidator?.name} + + + + + + To + + {selectedValidator?.description?.moniker} + + + {renderRedelegateInputBox('Amount to Redelegate', stakecoin.symbol)} + + + + + + + + + + + + + + + + + + + {myValidators.map((validator, index) => ( + + + + + + ))} + +
ValidatorAmount StakedClaimable Rewards
+ + {index + 1} + + {validator.name} + + + {validator.staked}  + + + + + {validator.reward}  + + + + +
+
+ + ); + }; + + export default MyValidators; \ No newline at end of file diff --git a/components/apps/staking/react/staking.tsx b/components/apps/staking/react/staking.tsx new file mode 100644 index 0000000..718318c --- /dev/null +++ b/components/apps/staking/react/staking.tsx @@ -0,0 +1,350 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import React,{ useCallback, useEffect, useState } from 'react'; +import { useChain } from '@cosmos-kit/react'; +import { Box, SkeletonText } from '@chakra-ui/react'; +import { cosmos } from 'interchain'; +import BigNumber from 'bignumber.js'; +import { decodeCosmosSdkDecFromProto } from '@cosmjs/stargate'; +import Long from 'long'; +import type { + Validator, + DelegationResponse as Delegation, +} from 'interchain/types/codegen/cosmos/staking/v1beta1/staking'; +import type { DelegationDelegatorReward as Reward } from 'interchain/types/codegen/cosmos/distribution/v1beta1/distribution'; +import Stats from './stats'; +import MyValidators from './my-validators'; +import AllValidators from './all-validators'; +import { getGasCoin, getStakeCoin } from '../../../../config'; +import { ChainName } from '@cosmos-kit/core'; +import { ImageSource } from '../../../../types'; + +export const exponentiate = (num: number | string, exp: number) => { + return new BigNumber(num) + .multipliedBy(new BigNumber(10).exponentiatedBy(exp)) + .toNumber(); +}; + +export const getExponent = (chainName: string) => { + return getGasCoin(chainName).denom_units.find( + (unit) => unit.denom === getGasCoin(chainName).display + )?.exponent as number; +}; + +const splitIntoChunks = (arr: any[], chunkSize: number) => { + const res = []; + for (let i = 0; i < arr.length; i += chunkSize) { + const chunk = arr.slice(i, i + chunkSize); + res.push(chunk); + } + return res; +}; + +const convertChainName = (chainName: string) => { + if (chainName.endsWith('testnet')) { + return chainName.replace('testnet', '-testnet'); + } + + switch (chainName) { + case 'cosmoshub': + return 'cosmos'; + case 'assetmantle': + return 'asset-mantle'; + case 'cryptoorgchain': + return 'crypto-org'; + case 'dig': + return 'dig-chain'; + case 'gravitybridge': + return 'gravity-bridge'; + case 'kichain': + return 'ki-chain'; + case 'oraichain': + return 'orai-chain'; + case 'terra': + return 'terra-classic'; + default: + return chainName; + } +}; + +const isUrlValid = async (url: string) => { + const res = await fetch(url, { method: 'HEAD' }); + const contentType = res?.headers?.get('Content-Type') || ''; + return contentType.startsWith('image'); +}; + +const getCosmostationUrl = (chainName: string, validatorAddr: string) => { + const cosmostationChainName = convertChainName(chainName); + return `https://raw.githubusercontent.com/cosmostation/chainlist/main/chain/${cosmostationChainName}/moniker/${validatorAddr}.png`; +}; + +const addImageSource = async ( + validator: Validator, + chainName: string +): Promise => { + const url = getCosmostationUrl(chainName, validator.operatorAddress); + const isValid = await isUrlValid(url); + return { ...validator, imageSource: isValid ? 'cosmostation' : 'keybase' }; +}; + +const getKeybaseUrl = (identity: string) => { + return `https://keybase.io/_/api/1.0/user/lookup.json?key_suffix=${identity}&fields=pictures`; +}; + +const getImgUrls = async (validators: Validator[], chainName: string) => { + const validatorsWithImgSource = await Promise.all( + validators.map((validator) => addImageSource(validator, chainName)) + ); + + // cosmostation urls + const cosmostationUrls = validatorsWithImgSource + .filter((validator) => validator.imageSource === 'cosmostation') + .map(({ operatorAddress }) => { + return { + address: operatorAddress, + url: getCosmostationUrl(chainName, operatorAddress), + }; + }); + + // keybase urls + const keybaseIdentities = validatorsWithImgSource + .filter((validator) => validator.imageSource === 'keybase') + .map((validator) => ({ + address: validator.operatorAddress, + identity: validator.description?.identity || '', + })); + + const chunkedIdentities = splitIntoChunks(keybaseIdentities, 20); + + let responses: any[] = []; + + for (const chunk of chunkedIdentities) { + const thumbnailRequests = chunk.map(({ address, identity }) => { + if (!identity) return { address, url: '' }; + + return fetch(getKeybaseUrl(identity)) + .then((response) => response.json()) + .then((res) => ({ + address, + url: res.them?.[0]?.pictures?.primary.url || '', + })); + }); + responses = [...responses, await Promise.all(thumbnailRequests)]; + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + const keybaseUrls = responses.flat(); + + const allUrls = [...cosmostationUrls, ...keybaseUrls].reduce( + (prev, cur) => ({ ...prev, [cur.address]: cur.url }), + {} + ); + + return allUrls; +}; + +interface StakingTokens { + balance: number; + rewards: Reward[]; + totalReward: number; + staked: number; + delegations: Delegation[]; + myValidators: Validator[]; + allValidators: Validator[]; + unbondingDays: number; + thumbnails: { + [key: string]: string; + }; +} + +export const StakingSection = ({ chainName }: { chainName: ChainName }) => { + const { address, getRpcEndpoint } = useChain(chainName); + const [isLoading, setIsLoading] = useState(false); + const [data, setData] = useState({ + balance: 0, + rewards: [], + totalReward: 0, + staked: 0, + delegations: [], + myValidators: [], + allValidators: [], + unbondingDays: 0, + thumbnails: {}, + }); + + const gascoin = getGasCoin(chainName); + const stakecoin = getStakeCoin(chainName); + const exp = getExponent(chainName); + + const getData = useCallback(async () => { + if (!address) { + setData({ + balance: 0, + rewards: [], + totalReward: 0, + staked: 0, + delegations: [], + myValidators: [], + allValidators: [], + unbondingDays: 0, + thumbnails: {}, + }); + return; + } + + setIsLoading(true); + + let rpcEndpoint = await getRpcEndpoint(); + + if (!rpcEndpoint) { + console.log('no rpc endpoint — using a fallback'); + rpcEndpoint = `https://rpc.cosmos.directory/${chainName}`; + } + + // get RPC client + const client = await cosmos.ClientFactory.createRPCQueryClient({ + rpcEndpoint: + typeof rpcEndpoint === 'string' ? rpcEndpoint : rpcEndpoint.url, + }); + + // AVAILABLE BALANCE + const { balance } = await client.cosmos.bank.v1beta1.balance({ + address, + denom: stakecoin.base, + }); + + const amount = exponentiate(balance!.amount, -exp); + + // MY VALIDATORS + const { validators: myValidators } = + await client.cosmos.staking.v1beta1.delegatorValidators({ + delegatorAddr: address, + }); + + // REWARDS + const { rewards, total } = + await client.cosmos.distribution.v1beta1.delegationTotalRewards({ + delegatorAddress: address, + }); + + const delegatorReward = total.find((item) => item.denom === gascoin.base); + + const reward = decodeCosmosSdkDecFromProto( + delegatorReward ? delegatorReward.amount : '0' + ).toString(); + + const totalReward = Number(exponentiate(reward, -exp).toFixed(6)); + + // ALL VALIDATORS + const { validators } = await client.cosmos.staking.v1beta1.validators({ + status: cosmos.staking.v1beta1.bondStatusToJSON( + cosmos.staking.v1beta1.BondStatus.BOND_STATUS_BONDED + ), + pagination: { + key: new Uint8Array(), + offset: Long.fromNumber(0), + limit: Long.fromNumber(200), + countTotal: false, + reverse: false, + }, + }); + + const allValidators = validators.sort((a, b) => + new BigNumber(b.tokens).minus(new BigNumber(a.tokens)).toNumber() + ); + + // DELEGATIONS + const { delegationResponses: delegations } = + await client.cosmos.staking.v1beta1.delegatorDelegations({ + delegatorAddr: address, + }); + + const stakedAmount = delegations + .map((delegation) => exponentiate(delegation.balance!.amount, -exp)) + .reduce((a, b) => a + b, 0); + + // UNBONDING DAYS + const { params } = await client.cosmos.staking.v1beta1.params(); + const unbondingDays = params?.unbondingTime + ? Number((params?.unbondingTime?.seconds.low / 86400).toFixed(0)) + : 0; + + // THUMBNAILS + let thumbnails = {}; + + const validatorThumbnails = localStorage.getItem( + `${chainName}-validator-thumbnails` + ); + + if (validatorThumbnails) { + thumbnails = JSON.parse(validatorThumbnails); + } else { + thumbnails = await getImgUrls(validators, chainName); + localStorage.setItem( + `${chainName}-validator-thumbnails`, + JSON.stringify(thumbnails) + ); + } + + setData({ + rewards, + totalReward, + balance: amount, + staked: stakedAmount, + delegations, + myValidators, + allValidators, + unbondingDays, + thumbnails, + }); + setIsLoading(false); + }, [address]); + + useEffect(() => { + getData(); + }, [getData]); + + return ( + + + + {data.myValidators.length > 0 && ( + + )} + {data.allValidators.length > 0 && ( + + )} + + + ); +}; \ No newline at end of file diff --git a/components/apps/staking/react/stats.tsx b/components/apps/staking/react/stats.tsx new file mode 100644 index 0000000..b2c2dd4 --- /dev/null +++ b/components/apps/staking/react/stats.tsx @@ -0,0 +1,170 @@ +import { + Stat, + StatLabel, + StatNumber, + StatGroup, + Button, + useColorModeValue, + Text, + } from '@chakra-ui/react'; + import { useChain } from '@cosmos-kit/react'; + import { useState } from 'react'; + import { cosmos } from 'interchain'; + import { getStakeCoin, getGasCoin } from '../../../../config'; + import type { DelegationDelegatorReward as Reward } from 'interchain/types/codegen/cosmos/distribution/v1beta1/distribution'; + import { TransactionResult } from '../../../../types'; + import { ChainName } from '@cosmos-kit/core'; + import { useFeeEstimation, useTransactionToast } from '../hooks'; +import React from 'react'; + + export const Token = ({ token, color }: { token: string; color?: string }) => ( + + {token} + + ); + + const Stats = ({ + balance, + rewards, + staked, + totalReward, + updateData, + chainName, + }: { + balance: number; + rewards: Reward[]; + staked: number; + totalReward: number; + updateData: () => void; + chainName: ChainName; + }) => { + const [isClaiming, setIsClaiming] = useState(false); + const { getSigningStargateClient, address } = useChain(chainName); + const { showToast } = useTransactionToast(); + const { estimateFee } = useFeeEstimation(chainName); + + const totalAmount = balance + staked + totalReward; + const stakecoin = getStakeCoin(chainName); + const gascoin = getGasCoin(chainName); + + const onClaimClick = async () => { + setIsClaiming(true); + + const stargateClient = await getSigningStargateClient(); + + if (!stargateClient || !address) { + console.error('stargateClient undefined or address undefined.'); + return; + } + + const { withdrawDelegatorReward } = + cosmos.distribution.v1beta1.MessageComposer.fromPartial; + + const msgs = rewards.map(({ validatorAddress }) => + withdrawDelegatorReward({ + delegatorAddress: address, + validatorAddress, + }) + ); + + try { + const fee = await estimateFee(address, msgs); + const res = await stargateClient.signAndBroadcast(address, msgs, fee); + showToast(res.code); + updateData(); + } catch (error) { + console.log(error); + showToast(TransactionResult.Failed, error); + } finally { + stargateClient.disconnect(); + setIsClaiming(false); + } + }; + + return ( + + + + Total {stakecoin.symbol} Amount + + + {totalAmount === 0 ? totalAmount : totalAmount.toFixed(6)}  + + + + + + + Available Balance + + + {balance} + + + + + + Staked Amount + + + {staked === 0 ? staked : staked.toFixed(6)}  + + + + + + + Claimable Rewards + + + {totalReward}  + + + + + + ); + }; + + export default Stats; \ No newline at end of file diff --git a/components/apps/staking/react/user-card.tsx b/components/apps/staking/react/user-card.tsx new file mode 100644 index 0000000..e7d3c19 --- /dev/null +++ b/components/apps/staking/react/user-card.tsx @@ -0,0 +1,34 @@ +import { Box,Stack, Text } from "@chakra-ui/react"; +import React from "react"; + +import { ConnectedUserCardType } from "../../../types"; + +export const ConnectedUserInfo = ({ + username, + icon, +}: ConnectedUserCardType) => { + return ( + + {username && ( + <> + + {icon} + + + {username} + + + )} + + ); +}; \ No newline at end of file diff --git a/components/apps/staking/react/wallet-connect.tsx b/components/apps/staking/react/wallet-connect.tsx new file mode 100644 index 0000000..2961580 --- /dev/null +++ b/components/apps/staking/react/wallet-connect.tsx @@ -0,0 +1,202 @@ +import { Button, Icon, Stack, Text, useColorModeValue } from "@chakra-ui/react"; +import { WalletStatus } from "@cosmos-kit/core"; +import React, { MouseEventHandler, ReactNode } from "react"; +import { FiAlertTriangle } from "react-icons/fi"; +import { IoWallet } from "react-icons/io5"; + +import { ConnectWalletType } from "../../../types"; + +export const ConnectWalletButton = ({ + buttonText, + isLoading, + isDisabled, + icon, + onClickConnectBtn, +}: ConnectWalletType) => { + return ( + + ); +}; + +export const Disconnected = ({ + buttonText, + onClick, +}: { + buttonText: string; + onClick: MouseEventHandler; +}) => { + return ( + + ); +}; + +export const Connected = ({ + buttonText, + onClick, +}: { + buttonText: string; + onClick: MouseEventHandler; +}) => { + return ( + + ); +}; + +export const Connecting = () => { + return ; +}; + +export const Rejected = ({ + buttonText, + wordOfWarning, + onClick, +}: { + buttonText: string; + wordOfWarning?: string; + onClick: MouseEventHandler; +}) => { + const bg = useColorModeValue("orange.200", "orange.300"); + + return ( + + + {wordOfWarning && ( + + + + + Warning:  + + {wordOfWarning} + + + )} + + ); +}; + +export const Error = ({ + buttonText, + wordOfWarning, + onClick, +}: { + buttonText: string; + wordOfWarning?: string; + onClick: MouseEventHandler; +}) => { + const bg = useColorModeValue("orange.200", "orange.300"); + + return ( + + + {wordOfWarning && ( + + + + + Warning:  + + {wordOfWarning} + + + )} + + ); +}; + +export const NotExist = ({ + buttonText, + onClick, +}: { + buttonText: string; + onClick: MouseEventHandler; +}) => { + return ( + + ); +}; + +export const WalletConnectComponent = ({ + walletStatus, + disconnect, + connecting, + connected, + rejected, + error, + notExist, +}: { + walletStatus: WalletStatus; + disconnect: ReactNode; + connecting: ReactNode; + connected: ReactNode; + rejected: ReactNode; + error: ReactNode; + notExist: ReactNode; +}) => { + switch (walletStatus) { + case "Disconnected": + return <>{disconnect}; + case "Connecting": + return <>{connecting}; + case "Connected": + return <>{connected}; + case "Rejected": + return <>{rejected}; + case "Error": + return <>{error}; + case "NotExist": + return <>{notExist}; + default: + return <>{disconnect}; + } +}; \ No newline at end of file diff --git a/components/apps/staking/react/warn-block.tsx b/components/apps/staking/react/warn-block.tsx new file mode 100644 index 0000000..ccf67f3 --- /dev/null +++ b/components/apps/staking/react/warn-block.tsx @@ -0,0 +1,90 @@ +import { Box, Stack, Text, useColorModeValue } from "@chakra-ui/react"; +import { WalletStatus } from "@cosmos-kit/core"; +import React, { ReactNode } from "react"; + +export const WarnBlock = ({ + wordOfWarning, + icon, +}: { + wordOfWarning?: string; + icon?: ReactNode; +}) => { + return ( + + + + {icon} + + {wordOfWarning} + + + ); +}; + +export const RejectedWarn = ({ + wordOfWarning, + icon, +}: { + wordOfWarning?: string; + icon?: ReactNode; +}) => { + return ; +}; + +export const ConnectStatusWarn = ({ + walletStatus, + rejected, + error, +}: { + walletStatus: WalletStatus; + rejected: ReactNode; + error: ReactNode; +}) => { + switch (walletStatus) { + case "Rejected": + return <>{rejected}; + case "Error": + return <>{error}; + default: + return <>; + } +}; \ No newline at end of file diff --git a/components/apps/store.ts b/components/apps/store.ts index 3ed3505..f13e0aa 100644 --- a/components/apps/store.ts +++ b/components/apps/store.ts @@ -8,7 +8,7 @@ import { KeyResponse, OwnerResponse, } from "@steak-enjoyers/badges.js/types/codegen/Hub.types"; -import create from "zustand"; +import {create} from "zustand"; import { Network, NetworkConfig, NETWORK_CONFIGS, PUBLIC_ACCOUNTS } from "./configs"; export type State = { diff --git a/components/ecosystem/content.tsx b/components/ecosystem/content.tsx index d42e0be..184a7bb 100644 --- a/components/ecosystem/content.tsx +++ b/components/ecosystem/content.tsx @@ -1,6 +1,7 @@ import { Button, Center } from '@chakra-ui/react'; import { networkData } from './networks'; -import React,{ useState, useEffect } from "react"; +import React, { useState, useEffect } from "react"; +import Link from 'next/link'; type Network = { network: string; @@ -23,31 +24,31 @@ const EcosystemContent = () => { } if (event.currentTarget.id === '2') { - setAvailableNetwork(networkData.filter(function(item){ + setAvailableNetwork(networkData.filter(function (item) { return item.name === "dApp"; })); } if (event.currentTarget.id === '3') { - setAvailableNetwork(networkData.filter(function(item){ + setAvailableNetwork(networkData.filter(function (item) { return item.name === "Explorer"; })); } if (event.currentTarget.id === '4') { - setAvailableNetwork(networkData.filter(function(item){ + setAvailableNetwork(networkData.filter(function (item) { return item.name === "Wallet"; })); } if (event.currentTarget.id === '5') { - setAvailableNetwork(networkData.filter(function(item){ + setAvailableNetwork(networkData.filter(function (item) { return item.name === "Defi"; })); } if (event.currentTarget.id === '6') { - setAvailableNetwork(networkData.filter(function(item){ + setAvailableNetwork(networkData.filter(function (item) { return item.name === "Tools"; })); } @@ -58,75 +59,72 @@ const EcosystemContent = () => { }, []); return ( -
-
-

TerpNET ecosystem

-
-

Discover the suite of applications, wallets, explorers and tools within the ecosystem -

-
- - - - - - +
+
+

TerpNET ecosystem

+
+

Discover the suite of applications, wallets, explorers and tools within the ecosystem +

+
+ + + + + + + +
+
-
- {availableNetwork && availableNetwork.map((item, i) => ( - -
+
- -
{item.network}
-
{item.description}
-{item.name} - - - { - item.status==='ComingSoon' ? - : - } -  {item.status} -
- ))} - -
- - -
- -) + ) } export default EcosystemContent; \ No newline at end of file diff --git a/components/ecosystem/index.ts b/components/ecosystem/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/components/ecosystem/networks.tsx b/components/ecosystem/networks.tsx index 8d46bf4..290edba 100644 --- a/components/ecosystem/networks.tsx +++ b/components/ecosystem/networks.tsx @@ -17,7 +17,7 @@ export const networkData: NetworkData[] = [ icon: "", name: "Network", status: "Live", - wikilink: "https://akash.network/" + wikilink: "https://agoric.com/" }, { network: "Akash Network", @@ -33,7 +33,7 @@ export const networkData: NetworkData[] = [ icon: "", name: "Network", status: "Live", - wikilink: "https://akash.network/" + wikilink: "https://anoma.net/" }, { network: "Axelar Network ", @@ -43,13 +43,29 @@ export const networkData: NetworkData[] = [ status: "Live", wikilink: "https://axelar.network/" }, + { + network: "Archway Network ", + description: "EVM Intechain router", + icon: "", + name: "Network", + status: "Live", + wikilink: "https://axelar.network/" + }, + { + network: "Babylon Network ", + description: "Bringing Bitcoin security to Cosmos and beyond", + icon: "", + name: "Network", + status: "Live", + wikilink: "https://www.babylonchain.io/" + }, { network: "Bitsong ", description: "Tokenizing music & fan interaction.", icon: "", name: "Network", status: "Live", - wikilink: "https://osmosis.zone/" + wikilink: "https://bitsong.io/" }, { network: "Bitcanna Network ", @@ -65,7 +81,7 @@ export const networkData: NetworkData[] = [ icon: "", name: "Network", status: "Live", - wikilink: "https://nomic.io" + wikilink: "https://junonetwork.io" }, { network: "Nomic", @@ -89,7 +105,7 @@ export const networkData: NetworkData[] = [ icon: "", name: "Network", status: "Live", - wikilink: "https://sentinel.co/" + wikilink: "https://secret.network/" }, { network: "Sentienel DVPN ", @@ -129,7 +145,7 @@ export const networkData: NetworkData[] = [ icon: "", name: "Tools", status: "Coming Soon", - wikilink: "https://sifchain.network/" + wikilink: "https://interchaininfo.zone/" }, { network: "Abstract.io", @@ -137,7 +153,7 @@ export const networkData: NetworkData[] = [ icon: "", name: "Tools", status: "Coming Soon", - wikilink: "https://sifchain.network/" + wikilink: "https://abstract.money/" }, { network: "Komple.io ", @@ -145,7 +161,7 @@ export const networkData: NetworkData[] = [ icon: "", name: "Tools", status: "Coming Soon", - wikilink: "https://sifchain.network/" + wikilink: "https://komple.io/" }, { network: "Keplr", @@ -161,7 +177,7 @@ export const networkData: NetworkData[] = [ icon: "", name: "Defi", status: "Coming Soon", - wikilink: "https://sifchain.network/" + wikilink: "https://www.wynddao.com//" }, { network: "Gelotto.io", @@ -169,7 +185,7 @@ export const networkData: NetworkData[] = [ icon: "", name: "Defi", status: "Live TestNET", - wikilink: "https://wallet.keplr.app/" + wikilink: "https://gelotto.io/" }, { network: "Leap", @@ -272,6 +288,14 @@ export const networkData: NetworkData[] = [ wikilink: "https://spacestation.zone/" }, // Community Developed Apps + { + network: "Area 52", + description: " interactive coding platform that teaches you all things CosmWasm, Rust smart contracts, and how to build and deploy your own multichain applications.", + icon: "", + name: "Tools", + status: "Live", + wikilink: "https://area-52.io/" + }, { network: "Judging App: Legends of Hashish", description: "Decentralised voting framework", @@ -310,7 +334,7 @@ export const networkData: NetworkData[] = [ icon: "", name: "dApp", status: "Coming Soon", - wikilink: "https://terp.network/stake" + wikilink: "https://terp.network/gov" }, { network: "Poap Badges", diff --git a/components/homepage/terpEcosystem.tsx b/components/homepage/terpEcosystem.tsx index 1ebdceb..e21ca18 100644 --- a/components/homepage/terpEcosystem.tsx +++ b/components/homepage/terpEcosystem.tsx @@ -9,6 +9,7 @@ import { TERPNET_TWITTER_URL,TERPNET_YOUTUBE_URL } from "../../config/defaults"; import { Icon } from "@chakra-ui/react"; +import Link from "next/link"; @@ -100,10 +101,10 @@ export const TerpEcosystem = () => {
- - DOCS - + + DOCS +
@@ -126,13 +127,13 @@ export const TerpEcosystem = () => { diff --git a/components/index.tsx b/components/index.tsx index 9cd8c23..dbd491f 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -1,7 +1,5 @@ -export * from './types'; export * from './react'; export * from './features'; -export * from './chain-wallet-card' export * from './wallet'; export * from './head'; export * from './main-menu' diff --git a/components/layout/twoColumnLayout.tsx b/components/layout/twoColumnLayout.tsx index 87e1052..0a8efe3 100644 --- a/components/layout/twoColumnLayout.tsx +++ b/components/layout/twoColumnLayout.tsx @@ -2,28 +2,49 @@ import React, { useState } from 'react'; import DashboardContent from '../apps/dashboard'; import { SideBarContent } from 'components/sidebar'; + + const TwoColumnLayout = () => { - const [activeTab, setActiveTab] = useState(undefined); + const [activeTab, setActiveTab] = useState(); - const handleTabClick = (tab: React.SetStateAction) => { + const handleTabClick = (tab: string) => { setActiveTab(tab); }; + return ( -
+
+ {activeTab !== undefined ? ( + <> {activeTab === 'dashboard' && } {activeTab === 'badges' &&

Badges Content

} {activeTab === 'bridge' &&

Bridge Content

} - {activeTab === 'calendar' &&

Calendar Content

} + {activeTab === 'events' &&

Event Content

} + {activeTab === 'governance' &&

Governance Content

} + {activeTab === 'marketplace' &&

Marketplace Content

} + {activeTab === 'multisigs' &&

Multisigs Content

} + {activeTab === 'staking' &&

Staking Content

} + {activeTab === 'swap' &&

Swap Content

} + {activeTab === 'widgets' &&

Widgets Content

} + + ) : ( +
+ + {activeTab === 'dashboard' && } + {activeTab === 'badges' &&

Badges Content

} + {activeTab === 'bridge' &&

Bridge Content

} + {activeTab === 'events' &&

Event Content

} {activeTab === 'governance' &&

Governance Content

} {activeTab === 'marketplace' &&

Marketplace Content

} {activeTab === 'multisigs' &&

Multisigs Content

} {activeTab === 'staking' &&

Staking Content

} {activeTab === 'swap' &&

Swap Content

} {activeTab === 'widgets' &&

Widgets Content

} -
- ); +
+ )} +
+ ); }; export default TwoColumnLayout; \ No newline at end of file diff --git a/components/main-menu.tsx b/components/main-menu.tsx index eeba7e0..70529c3 100644 --- a/components/main-menu.tsx +++ b/components/main-menu.tsx @@ -1,6 +1,3 @@ - - - export const MainMenu = () => { return (
    diff --git a/components/react/address-card.tsx b/components/react/address-card.tsx index 0ead568..4c54fdf 100644 --- a/components/react/address-card.tsx +++ b/components/react/address-card.tsx @@ -8,7 +8,7 @@ import { useColorMode, } from "@chakra-ui/react"; import { WalletStatus } from "@cosmos-kit/core"; - import React, { ReactNode, useEffect, useState } from "react"; + import React, { ReactNode, useEffect, useMemo, useState } from "react"; import { FaCheckCircle } from "react-icons/fa"; import { FiCopy } from "react-icons/fi"; @@ -61,24 +61,29 @@ import { const { hasCopied, onCopy } = useClipboard(address ? address : ""); const [displayAddress, setDisplayAddress] = useState(""); // const { colorMode } = useColorMode(); - const defaultMaxLength = { - lg: 14, - md: 16, - sm: 18, - }; + const defaultMaxLength = useMemo(() => ({ + xs: 12, + sm: 16, + md: 20, + lg: 24, + xl: 28, + "2xl": 32, + "3xl": 40, + "4xl": 48, + }), []); - useEffect(() => { - if (!address) setDisplayAddress("address not identified yet"); - if (address && maxDisplayLength) - setDisplayAddress(stringTruncateFromCenter(address, maxDisplayLength)); - if (address && !maxDisplayLength) - setDisplayAddress( - stringTruncateFromCenter( - address, - defaultMaxLength[size as keyof typeof defaultMaxLength] - ) - ); - }, [address]); + useEffect(() => { + if (!address) setDisplayAddress("address not identified yet"); + if (address && maxDisplayLength) + setDisplayAddress(stringTruncateFromCenter(address, maxDisplayLength)); + if (address && !maxDisplayLength) + setDisplayAddress( + stringTruncateFromCenter( + address, + defaultMaxLength[size as keyof typeof defaultMaxLength] + ) + ); + }, [address, size, maxDisplayLength, defaultMaxLength]); return ( ); diff --git a/components/sidebar/sidebar.tsx b/components/sidebar/sidebar.tsx index be47e54..7a22ad8 100644 --- a/components/sidebar/sidebar.tsx +++ b/components/sidebar/sidebar.tsx @@ -3,22 +3,32 @@ import { BsApp } from "react-icons/bs"; import { FaNetworkWired, FaTicketAlt } from "react-icons/fa"; import { FiCalendar, FiHome, FiSettings } from "react-icons/fi"; +type IconWrapperProps = { + icon: React.ElementType; +}; + const links = [ - { href: "/poap", label: "Badges", icon: FaTicketAlt }, - { href: "/bridge", label: "Bridge", icon: FaNetworkWired }, - { href: "/events", label: "Calendar", icon: FiCalendar }, + { href: "/poap", label: "Badges (Coming Soon)", icon: FaTicketAlt, disabled: true }, + { href: "/bridge", label: "Bridge (Coming Soon)", icon: FaNetworkWired, disabled: true}, + { href: "/events", label: "Events (Coming Soon)", icon: FiCalendar, disabled: true }, { href: "/dashboard", label: "Dashboard", icon: BsApp }, { href: "/gov", label: "Governance", icon: FiHome }, - { href: "/merch", label: "Marketplace", icon: FiHome }, - { href: "/multi-sigs", label: "Multisigs", icon: FiHome }, + { href: "/merch", label: "Store (Coming Soon)", icon: FiHome, disabled: true }, + { href: "/multi-sigs", label: "Multi-Sig (Coming Soon)", icon: FiHome, disabled: true }, { href: "/stake", label: "Staking", icon: FiHome }, - { href: "/swap", label: "Swap", icon: FiHome }, - { href: "/widgets", label: "Widgets", icon: FiHome }, + { href: "/swap", label: "Swap ", icon: FiHome, disabled: true }, + { href: "/widgets", label: "Widgets (Coming Soon)", icon: FiHome, disabled: true }, ]; -const IconWrapper = ({ icon }) => ; + const IconWrapper = ({ icon }: IconWrapperProps) => ; + + interface SidebarContentProps { + activeTab: string | undefined; + onTabClick: (tab: string) => void; + } + -export const SideBarContent = ({ activeTab, onTabClick }) => { +export const SideBarContent = ({ activeTab, onTabClick }: SidebarContentProps) => { return (
    @@ -29,18 +39,20 @@ export const SideBarContent = ({ activeTab, onTabClick }) => { {links.map((link) => ( -
    - onTabClick(link.label)} - > - - {link.label} - +
    + onTabClick(link.label)} + pointerEvents={link.disabled ? "none" : "auto"} + opacity={link.disabled ? "0.5" : "1"} +> + + {link.label} +
    ))} diff --git a/components/types.tsx b/components/types.tsx deleted file mode 100644 index 8c951c7..0000000 --- a/components/types.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { MouseEventHandler, ReactElement, ReactNode } from "react"; - -import { AmplitudeEvent } from "../config"; - -export interface ChooseChainInfo { - chainName: string - chainRoute?: string - label: string - value: string - icon?: string - disabled?: boolean -} - -export enum WalletStatus { - NotInit = 'NotInit', - Loading = 'Loading', - Loaded = 'Loaded', - NotExist = 'NotExist', - Rejected = 'Rejected', -} - -export interface ConnectWalletType { - buttonText?: string - isLoading?: boolean - isDisabled?: boolean - icon?: ReactNode - onClickConnectBtn?: MouseEventHandler -} - -export interface ConnectedUserCardType { - walletIcon?: string - username?: string - icon?: ReactNode -} - -export interface FeatureProps { - title: string - text: string - href: string -} - -export interface ChainCardProps { - prettyName: string - icon?: string - -} - - -export type CopyAddressType = { - address?: string; - walletIcon?: string; - isLoading?: boolean; - maxDisplayLength?: number; - isRound?: boolean; - size?: string; -}; -export type MainLayoutMenu = { - label: string; - link: string | MouseEventHandler; - icon: string | ReactNode; - iconSelected?: string; - selectionTest?: RegExp; - amplitudeEvent?: AmplitudeEvent; -}; - -/** PROPS */ -export interface InputProps { - currentValue: T; - onInput: (value: T) => void; - autoFocus?: boolean; - onFocus?: (e: any) => void; - placeholder?: T; -} - -export interface CustomClasses { - className?: string; -} - -export interface LoadingProps { - isLoading?: boolean; -} - -export interface Disableable { - disabled?: boolean; -} - -export type SortDirection = "ascending" | "descending"; - -export interface Metric { - label: string; - value: string | ReactElement; -} - -export interface MobileProps { - isMobile?: boolean; -} - -/** Should match settings in tailwind.config.js - * - * https://tailwindcss.com/docs/responsive-design - */ -export const enum Breakpoint { - SM = 640, - MD = 768, - LG = 1024, - XLG = 1152, - XL = 1280, - XLHALF = 1408, - XXL = 1536, -} - -export enum VoteOption { - YES = 'YES', - NO = 'NO', - NWV = 'NWV', - ABSTAIN = 'ABSTAIN', -} - -export enum TransactionResult { - Success = 0, - Failed = 1, -} diff --git a/components/wallet.tsx b/components/wallet.tsx index 68bd1b6..8d08769 100644 --- a/components/wallet.tsx +++ b/components/wallet.tsx @@ -35,6 +35,10 @@ const buttons = { }, }; +type WalletSectionProps = { + chainName?: string; + setChainName?: React.Dispatch>; +}; export const WalletSection = () => { const { connect, diff --git a/config/defaults.ts b/config/defaults.ts index ce70f2d..0d0e0d1 100644 --- a/config/defaults.ts +++ b/config/defaults.ts @@ -1,4 +1,10 @@ -export const chainName = process.env.NEXT_PUBLIC_CHAIN ?? 'stargaze';; + +import { assets } from 'chain-registry'; +import { AssetList, Asset } from '@chain-registry/types'; + + + +export const chainName = process.env.NEXT_PUBLIC_CHAIN ?? 'terpnetwork';; export const TERPNET_TWITTER_URL = process.env.NEXT_PUBLIC_TERPNET_TWITTER_URL ?? 'https://twitter.com/terpculture'; export const TERPNET_YOUTUBE_URL = process.env.NEXT_PUBLIC_TERPNET_YOUTUBE_URL ?? 'https://youtube.com/terpnetwork'; export const TERPNET_ELEMENT_URL = process.env.NEXT_PUBLIC_TERPNET_ELEMENT_URL ?? 'https://matrix.to/#/!MIEDknobAODITdWMZi:matrix.org?via=matrix.org'; @@ -6,3 +12,20 @@ export const TERPNET_DISCORD_URL = process.env.NEXT_PUBLIC_TERPNET_DISCORD_URL ? export const TERPNET_GITHUB_URL = process.env.NEXT_PUBLIC_TERPNET_GITHUB_URL ?? 'https://github.com/terpnetwork'; export const TERPNET_MEDIUM_URL = process.env.NEXT_PUBLIC_TERPNET_MEDIUM_URL ?? 'https://terpnetwork.medium.com'; export const TERPNET_REDDIT_URL = process.env.NEXT_PUBLIC_TERPNET_REDDIT_URL ?? 'https://www.reddit.com/r/terpnetwork/'; + + + +export const defaultChainName = 'terpnetwork'; + +export const getChainAssets = (chainName: string = defaultChainName) => { + return assets.find((chain) => chain.chain_name === chainName) as AssetList; +}; +export const getStakeCoin = (chainName: string = defaultChainName) => { + const chainAssets = getChainAssets(chainName); + return chainAssets.assets[0] as Asset; + }; + + export const getGasCoin = (chainName: string = defaultChainName) => { + const chainAssets = getChainAssets(chainName); + return chainAssets.assets[1] as Asset; + }; diff --git a/config/index.ts b/config/index.ts index 01a4b2d..2e19833 100644 --- a/config/index.ts +++ b/config/index.ts @@ -1,3 +1,2 @@ export * from './features'; export * from './defaults'; -export * from './user-analytics-v2'; diff --git a/config/user-analytics-v2.ts b/config/user-analytics-v2.ts deleted file mode 100644 index 30da56c..0000000 --- a/config/user-analytics-v2.ts +++ /dev/null @@ -1,93 +0,0 @@ -export type EventProperties = { - fromToken: string; - toToken: string; - isOnHome: boolean; - percentage: string; - filteredBy: string; - isFilterOn: boolean; - sortedBy: string; - sortedOn: "table-head" | "dropdown" | "table"; - sortDirection: string; - isSingleAsset: boolean; - unbondingPeriod: number; - validatorName: string; - validatorCommission: number; - isOn: boolean; - tokenName: string; - tokenAmount: number; - bridge: string; - hasExternalUrl: boolean; - avatar: "ammelia" | "wosmongton"; - }; - - export type UserProperties = { - isWalletConnected: boolean; - connectedWallet: string; - totalAssetsPrice: number; - unbondedAssetsPrice: number; - bondedAssetsPrice: number; - stakedTerpPrice: number; - terpBalance: number; - thiolBalance: number; - }; - - export type AmplitudeEvent = - | [ - eventName: string, - eventProperties: Partial> | undefined - ] - | [eventName: string]; - - export const EventName = { - // Events in Swap UI and page - Swap: { - pageViewed: "Swap: Page viewed", - maxClicked: "Swap: Max clicked", - halfClicked: "Swap: Half clicked", - inputEntered: "Swap: Input entered", - switchClicked: "Swap: Switch clicked", - dropdownAssetSelected: "Swap: Dropdown asset selected", - }, - // Events in Sidebar UI - Sidebar: { - stakeClicked: "Sidebar: Stake clicked", - voteClicked: "Sidebar: Vote clicked", - infoClicked: "Sidebar: Info clicked", - supportClicked: "Sidebar: Support clicked", - }, - // Events in Topnav UI - Topnav: { - connectWalletClicked: "Topnav: Connect wallet clicked", - signOutClicked: "Topnav: Sign out clicked", - }, - // Events in Pools page - - // Events in Pool detail page - - // Events in assets page - Assets: { - pageViewed: "Assets: Page viewed", - depositClicked: "Assets: Deposit clicked", - withdrawClicked: "Assets: Withdraw clicked", - - assetsListFiltered: "Assets: Assets list filtered", - assetsListSorted: "Assets: Assets list sorted", - assetsListMoreClicked: "Assets: Assets list more clicked", - assetsItemDepositClicked: "Assets: Assets item deposit clicked", - assetsItemWithdrawClicked: "Assets: Assets item withdraw clicked", - depositAssetStarted: "Deposit asset: Deposit started", - depositAssetCompleted: "Deposit asset: Deposit completed", - withdrawAssetStarted: "Withdraw asset: Withdraw started", - withdrawAssetCompleted: "Withdraw asset: Withdraw completed", - }, - // Events in profile modal - ProfileModal: { - selectAvatarClicked: "Profile Modal: Select Avatar clicked", - qrCodeClicked: "Profile Modal: QR code clicked", - logOutClicked: "Profile Modal: Log out clicked", - copyWalletAddressClicked: "Profile Modal: Copy wallet address clicked", - buyTokensClicked: "Profile Modal: Buy tokens clicked", - blockExplorerLinkOutClicked: - "Profile Modal: Block explorer link-out clicked", - }, - }; \ No newline at end of file diff --git a/hooks/use-dimension.ts b/hooks/use-dimension.ts deleted file mode 100644 index e69de29..0000000 diff --git a/hooks/window/use-window-size.ts b/hooks/window/use-window-size.ts index 4278de1..dc09059 100644 --- a/hooks/window/use-window-size.ts +++ b/hooks/window/use-window-size.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { Breakpoint } from "../../components/types"; +import { Breakpoint } from "../../types"; export interface WindowSize { width: number; diff --git a/package.json b/package.json index ba376f3..515b502 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@amplitude/analytics-browser": "^1.10.3", + "@chain-registry/assets": "^1.14.0", "@chakra-ui/icons": "2.0.12", "@chakra-ui/react": "2.5.1", "@cosmjs/cosmwasm-stargate": "0.29.5", diff --git a/pages/_app.tsx b/pages/_app.tsx index b8ccc00..bb87dd5 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -32,9 +32,9 @@ function CreateCosmosApp({ Component, pageProps }: AppProps) { projectId: 'a8510432ebb71e6948cfd6cde54b70f7', relayUrl: 'wss://relay.walletconnect.org', metadata: { - name: 'CosmosKit Template', + name: 'Terp Network', description: 'CosmosKit dapp template', - url: 'https://docs.cosmoskit.com/', + url: 'https://terp.network/', icons: [], }, }, @@ -45,8 +45,7 @@ function CreateCosmosApp({ Component, pageProps }: AppProps) { > -
    - +
    diff --git a/pages/bridge.tsx b/pages/bridge.tsx index d706bff..a9f037c 100644 --- a/pages/bridge.tsx +++ b/pages/bridge.tsx @@ -1,9 +1,49 @@ import TwoColumnLayout from "components/layout/twoColumnLayout" - +import Head from 'next/head'; +import { + Box, + Divider, + Grid, + Heading, + Text, + Stack, + Container, + Link, + Button, + Flex, + Icon, + useColorMode, + useColorModeValue, +} from '@chakra-ui/react'; const IBCBridgePage = () => { return ( +
    + + + Create Cosmos App + + + + + + IBC Transfer + + + + + + + + + +
    ) } diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index bf766bf..979ac38 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -1,4 +1,4 @@ -import DashboardContent from "components/apps/dashboard"; +import DashboardContent from "components/apps/dashboard/dashboard"; import TwoColumnLayout from "components/layout/twoColumnLayout" diff --git a/pages/gov.tsx b/pages/gov.tsx index 703e1ef..6363601 100644 --- a/pages/gov.tsx +++ b/pages/gov.tsx @@ -17,26 +17,22 @@ import { useColorModeValue, } from '@chakra-ui/react'; import { BsFillMoonStarsFill, BsFillSunFill } from 'react-icons/bs'; -import {WalletSection} from '../components/wallet' import {VotingSection} from '../components/apps/gov/react/vote' import { useState } from 'react'; import { ChainName } from '@cosmos-kit/core'; +import { WalletSection } from "components/wallet"; export default function GovernancePage(){ const { colorMode, toggleColorMode } = useColorMode(); - const [chainName, setChainName] = useState('osmosis'); + const [chainName, setChainName] = useState('terpnetwork'); return (
    - + Governance Dashboard - {chainName && } diff --git a/pages/index.tsx b/pages/index.tsx index 4f8f504..b669101 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -40,6 +40,7 @@ export default function Home() { cosmology diff --git a/pages/poap.tsx b/pages/poap.tsx index 6d1b46c..4600761 100644 --- a/pages/poap.tsx +++ b/pages/poap.tsx @@ -1,5 +1,4 @@ import TwoColumnLayout from "components/layout/twoColumnLayout" -import PoapContent from "../components/apps/poap/poapDashboard"; import React from "react"; @@ -7,7 +6,10 @@ export default function PoapPage(){ return (
    - +
    + + +
    ) } diff --git a/pages/posts/[slug].tsx b/pages/posts/[slug].tsx index 8086bdd..7ca4ff5 100644 --- a/pages/posts/[slug].tsx +++ b/pages/posts/[slug].tsx @@ -64,6 +64,9 @@ const PostPage = ({ source, frontMatter }: PostPageProps): JSX.Element => { }; export const getStaticProps: GetStaticProps = async ({ params }) => { + if (!params) { + return { notFound: true}; + } const postFilePath = path.join(POSTS_PATH, `${params.slug}.mdx`); const source = fs.readFileSync(postFilePath); diff --git a/pages/stake.tsx b/pages/stake.tsx new file mode 100644 index 0000000..0bccb9f --- /dev/null +++ b/pages/stake.tsx @@ -0,0 +1,16 @@ +import StakingContent from "components/apps/staking"; +import TwoColumnLayout from "components/layout/twoColumnLayout" +import React from "react"; + + +const DashboardPage = () => { + return ( +
    + + +
    + ) +} + + +export default DashboardPage; \ No newline at end of file diff --git a/styles/globals.css b/styles/globals.css index d81ce89..3d9c324 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -36,9 +36,8 @@ height: 100%; background-repeat: no-repeat; background-size: cover; - background: linear-gradient(-45deg, #C2C2C2, #F8F8F8, #9B4DCA, #F28705, #E8D095, #F28705, #9B4DCA, #F8F8F8, #C2C2C2); - background-size: 400% 400%; - animation: gradient 15s ease infinite; + background: linear-gradient(-45deg,#9B4DCA, #F28705, #c9b070, #F28705, #9B4DCA); + background-size: 100% 100%; } .frosted{ @@ -721,12 +720,57 @@ flex: 0 0 64px; overflow-y: auto; } +.container { + position: relative; + z-index: 1; +} + +.cover { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + backdrop-filter: blur(8px); + z-index: -1; +} + +.centered-component { + justify-content: center; + align-items: center; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.hidden { + visibility: hidden; +} .two-column-layout { display: flex; color: rgba(255, 255, 255, 0.89); + backdrop-filter: blur(10px) opacity(0.9); + background-color: rgba(5, 5, 6, 0.956); + position: relative; + pointer-events: none; } +.two-column-layout::before { + font-size: 24px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + z-index: 1; +} + +.two-column-layout * { + pointer-events: auto; +} + .left-sidebar { flex: 0 0 64px; color: transparent; @@ -890,3 +934,35 @@ a:hover { .token.variable { color: #f6e05e; } + +.wallet-list { + margin-top: 2rem; + text-align: center +} + +@media (min-width: 640px) { + .wallet-list { + margin-top: 0.5rem; + text-align: left; + + } +} +.flex-row-center { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding-left: 3px; +} +.medium-text { + font-weight: 500; + font-size: 1.5rem; + line-height: 1.5; /* this value can vary depending on your design */ + color: #333333; +} + +@media (prefers-color-scheme: dark) { + .medium-text { + color: #f8f8f8; + } +} \ No newline at end of file diff --git a/types/chain.ts b/types/chain.ts new file mode 100644 index 0000000..9f42b09 --- /dev/null +++ b/types/chain.ts @@ -0,0 +1,50 @@ +import { RefObject } from "react" + +export interface ChooseChainInfo { + chainName: string + chainRoute?: string + label: string + value: string + icon?: string + disabled?: boolean + } + + export interface ChainCardProps { + prettyName: string + icon?: string + + } + + export interface OptionBase { + variant?: string; + colorScheme?: string; + isFixed?: boolean; + isDisabled?: boolean; + } + + export interface ChainOption extends OptionBase { + isDisabled?: boolean; + label: string; + value: string; + icon?: string; + chainName: string; + chainRoute?: string; + id?: string; + } + + export type handleSelectChainDropdown = (value: ChainOption | null) => void; + +export interface ChangeChainDropdownType { + data: ChainOption[]; + selectedItem?: ChainOption; + onChange: handleSelectChainDropdown; + chainDropdownLoading?: boolean; +} + +export interface ChangeChainMenuType { + data: ChainOption[]; + value?: ChainOption; + onClose?: () => void; + onChange: handleSelectChainDropdown; + innerRef?: RefObject; +} \ No newline at end of file diff --git a/types/common.ts b/types/common.ts new file mode 100644 index 0000000..4ebf42f --- /dev/null +++ b/types/common.ts @@ -0,0 +1,29 @@ + +export enum TransactionResult { + Success = 0, + Failed = 1, + } + + +export interface Balance { + denom: string; + symbol: string; + amount: string; + displayAmount: number; + logoUrl?: string; + } + + export interface FeatureProps { + title: string + text: string + href: string + } + + export type CopyAddressType = { + address?: string; + walletIcon?: string; + isLoading?: boolean; + maxDisplayLength?: number; + isRound?: boolean; + size?: string; + }; \ No newline at end of file diff --git a/components/blog/index.tsx b/types/dashboard.ts similarity index 100% rename from components/blog/index.tsx rename to types/dashboard.ts diff --git a/types/gov.ts b/types/gov.ts new file mode 100644 index 0000000..3c71418 --- /dev/null +++ b/types/gov.ts @@ -0,0 +1,8 @@ + + +export enum VoteOption { + YES = 'YES', + NO = 'NO', + NWV = 'NWV', + ABSTAIN = 'ABSTAIN', + } \ No newline at end of file diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 0000000..4a76a66 --- /dev/null +++ b/types/index.ts @@ -0,0 +1,8 @@ +export * from './chain' +export * from './common' +// export * from '.dashboard' +export * from './gov' +export * from './layout' +export * from './post' +export * from './staking' +export * from './wallet' \ No newline at end of file diff --git a/types/layout.ts b/types/layout.ts index 24e15a8..8ddda52 100644 --- a/types/layout.ts +++ b/types/layout.ts @@ -6,4 +6,18 @@ export interface MetaProps * For the meta tag `og:type` */ type?: string; +} + +export interface MobileProps { + isMobile?: boolean; +} + +export const enum Breakpoint { + SM = 640, + MD = 768, + LG = 1024, + XLG = 1152, + XL = 1280, + XLHALF = 1408, + XXL = 1536, } \ No newline at end of file diff --git a/types/staking.ts b/types/staking.ts new file mode 100644 index 0000000..5302d4c --- /dev/null +++ b/types/staking.ts @@ -0,0 +1,15 @@ + + +export type ImageSource = { + imageSource: 'cosmostation' | 'keybase'; +}; + +export interface MyValidator { + details: string | undefined; + name: string | undefined; + address: string; + staked: number; + reward: number; + identity: string | undefined; + commission: string | undefined; +} \ No newline at end of file diff --git a/types/wallet.ts b/types/wallet.ts new file mode 100644 index 0000000..9de8cd3 --- /dev/null +++ b/types/wallet.ts @@ -0,0 +1,25 @@ +import { MouseEventHandler, ReactNode, RefObject } from "react"; + +export enum WalletStatus { + NotInit = 'NotInit', + Loading = 'Loading', + Loaded = 'Loaded', + NotExist = 'NotExist', + Rejected = 'Rejected', + } + + export interface ConnectWalletType { + buttonText?: string + isLoading?: boolean + isDisabled?: boolean + icon?: ReactNode + onClickConnectBtn?: MouseEventHandler + } + + export interface ConnectedUserCardType { + walletIcon?: string + username?: string + icon?: ReactNode + } + + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 051c350..19724ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1226,6 +1226,14 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@chain-registry/assets@^1.14.0": + version "1.14.0" + resolved "https://registry.yarnpkg.com/@chain-registry/assets/-/assets-1.14.0.tgz#0e7cd7c976ac1b6831d58648ec281878601d54ca" + integrity sha512-L8yAy227wXTFfwMfr14wEZLpnx52fkn6x+krU704F1YIeqHYPp5Y1KBzIOcdQ1oq45hhYwuHf7xoMwoXcd/sTQ== + dependencies: + "@babel/runtime" "^7.21.0" + "@chain-registry/types" "^0.16.0" + "@chain-registry/cosmostation@1.8.0": version "1.8.0" resolved "https://registry.npmjs.org/@chain-registry/cosmostation/-/cosmostation-1.8.0.tgz"