diff --git a/bridge_ui/src/components/Stats/CustodyAddresses.tsx b/bridge_ui/src/components/Stats/CustodyAddresses.tsx new file mode 100644 index 00000000..da4935e1 --- /dev/null +++ b/bridge_ui/src/components/Stats/CustodyAddresses.tsx @@ -0,0 +1,132 @@ +import { + CHAIN_ID_BSC, + CHAIN_ID_ETH, + CHAIN_ID_SOLANA, + CHAIN_ID_TERRA, +} from "@certusone/wormhole-sdk"; +import { makeStyles, Typography } from "@material-ui/core"; +import { useMemo } from "react"; +import { + getNFTBridgeAddressForChain, + getTokenBridgeAddressForChain, + SOL_CUSTODY_ADDRESS, + SOL_NFT_CUSTODY_ADDRESS, +} from "../../utils/consts"; +import SmartAddress from "../SmartAddress"; +import MuiReactTable from "./tableComponents/MuiReactTable"; + +const useStyles = makeStyles((theme) => ({ + flexBox: { + display: "flex", + alignItems: "flex-end", + marginBottom: theme.spacing(1), + textAlign: "left", + [theme.breakpoints.down("sm")]: { + flexDirection: "column", + alignItems: "unset", + }, + }, + grower: { + flexGrow: 1, + }, + explainerContainer: {}, +})); + +const CustodyAddresses: React.FC = () => { + const classes = useStyles(); + const data = useMemo(() => { + return [ + { + chainName: "Ethereum", + chainId: CHAIN_ID_ETH, + tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_ETH), + nftAddress: getNFTBridgeAddressForChain(CHAIN_ID_ETH), + }, + { + chainName: "Solana", + chainId: CHAIN_ID_SOLANA, + tokenAddress: SOL_CUSTODY_ADDRESS, + nftAddress: SOL_NFT_CUSTODY_ADDRESS, + }, + { + chainName: "Binance Smart Chain", + chainId: CHAIN_ID_BSC, + tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_BSC), + nftAddress: getNFTBridgeAddressForChain(CHAIN_ID_BSC), + }, + { + chainName: "Terra", + chainId: CHAIN_ID_TERRA, + tokenAddress: getTokenBridgeAddressForChain(CHAIN_ID_TERRA), + nftAddress: null, + }, + ]; + }, []); + + const tvlColumns = useMemo(() => { + return [ + { Header: "Chain", accessor: "chainName", disableGroupBy: true }, + { + Header: "Token Address", + id: "tokenAddress", + accessor: "address", + disableGroupBy: true, + Cell: (value: any) => + value.row?.original?.tokenAddress && value.row?.original?.chainId ? ( + + ) : ( + "" + ), + }, + { + Header: "NFT Address", + id: "nftAddress", + accessor: "address", + disableGroupBy: true, + Cell: (value: any) => + value.row?.original?.nftAddress && value.row?.original?.chainId ? ( + + ) : ( + "" + ), + }, + ]; + }, []); + + const header = ( +
+
+ Custody Addresses + + These are the custody addresses which hold collateralized assets for + the token bridge. + +
+
+
+ ); + + const table = ( + + ); + + return ( + <> + {header} + {table} + + ); +}; + +export default CustodyAddresses; diff --git a/bridge_ui/src/components/Stats/NFTStats.tsx b/bridge_ui/src/components/Stats/NFTStats.tsx new file mode 100644 index 00000000..96b2dcd1 --- /dev/null +++ b/bridge_ui/src/components/Stats/NFTStats.tsx @@ -0,0 +1,253 @@ +import { + Button, + CircularProgress, + makeStyles, + Typography, +} from "@material-ui/core"; +import clsx from "clsx"; +import numeral from "numeral"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import useNFTTVL from "../../hooks/useNFTTVL"; +import { + CHAINS_WITH_NFT_SUPPORT, + getNFTBridgeAddressForChain, +} from "../../utils/consts"; +import NFTViewer from "../TokenSelectors/NFTViewer"; +import MuiReactTable from "./tableComponents/MuiReactTable"; + +const useStyles = makeStyles((theme) => ({ + logoPositioner: { + height: "30px", + width: "30px", + maxWidth: "30px", + marginRight: theme.spacing(1), + display: "flex", + alignItems: "center", + }, + logo: { + maxHeight: "100%", + maxWidth: "100%", + }, + tokenContainer: { + display: "flex", + justifyContent: "flex-start", + alignItems: "center", + }, + flexBox: { + display: "flex", + alignItems: "flex-end", + marginBottom: theme.spacing(1), + textAlign: "left", + [theme.breakpoints.down("sm")]: { + flexDirection: "column", + alignItems: "unset", + }, + }, + grower: { + flexGrow: 1, + }, + explainerContainer: {}, + totalContainer: { + display: "flex", + alignItems: "flex-end", + paddingBottom: 1, // line up with left text bottom + [theme.breakpoints.down("sm")]: { + marginTop: theme.spacing(1), + }, + }, + totalValue: { + marginLeft: theme.spacing(0.5), + marginBottom: "-.125em", // line up number with label + }, + tableBox: { + display: "flex", + justifyContent: "flex-start", + "& > *": { + margin: theme.spacing(1), + }, + }, + randomButton: { + margin: "0px auto 8px", + display: "block", + }, + randomNftContainer: { + minHeight: "550px", + }, + alignCenter: { + margin: "0 auto", + display: "block", + }, +})); + +const BLACKLIST = ["D9cX654dGb4GFzqq3RY7rhZbRkQqUkfggDZdnYxqv97g"]; + +const NFTStats: React.FC = () => { + const classes = useStyles(); + const nftTVL = useNFTTVL(); + + //Disable this to quickly turn off + //TODO also change what data is fetched off this + const enableRandomNFT = true; + + const [randomNumber, setRandomNumber] = useState(null); + const randomNft = useMemo( + () => + (randomNumber !== null && nftTVL.data && nftTVL.data[randomNumber]) || + null, + [randomNumber, nftTVL.data] + ); + const genRandomNumber = useCallback(() => { + if (!nftTVL || !nftTVL.data || !nftTVL.data?.length || nftTVL.isFetching) { + setRandomNumber(null); + } else { + let found = false; + let nextNumber = Math.floor(Math.random() * nftTVL.data.length); + + while (!found) { + if (!nftTVL.data) { + return null; + } + const item = nftTVL?.data[nextNumber]?.mintKey?.toLowerCase() || null; + if (!BLACKLIST.find((x) => x.toLowerCase() === item)) { + found = true; + } else { + nextNumber = Math.floor(Math.random() * nftTVL.data.length); + } + } + + setRandomNumber(nextNumber); + } + }, [nftTVL]); + useEffect(() => { + genRandomNumber(); + }, [nftTVL.isFetching, genRandomNumber]); + + const data = useMemo(() => { + const output: any[] = []; + if (nftTVL.data && !nftTVL.isFetching) { + CHAINS_WITH_NFT_SUPPORT.forEach((chain) => { + output.push({ + nfts: nftTVL?.data?.filter((x) => x.chainId === chain.id), + chainName: chain.name, + chainId: chain.id, + chainLogo: chain.logo, + contractAddress: getNFTBridgeAddressForChain(chain.id), + }); + }); + } + + return output; + }, [nftTVL]); + + const tvlColumns = useMemo(() => { + return [ + { Header: "Chain", accessor: "chainName", disableGroupBy: true }, + // { + // Header: "Address", + // accessor: "contractAddress", + // disableGroupBy: true, + // Cell: (value: any) => + // value.row?.original?.contractAddress && + // value.row?.original?.chainId ? ( + // + // ) : ( + // "" + // ), + // }, + { + Header: "NFTs Locked", + id: "nftCount", + accessor: "nftCount", + align: "right", + disableGroupBy: true, + Cell: (value: any) => + value.row?.original?.nfts?.length !== undefined + ? numeral(value.row?.original?.nfts?.length).format("0 a") + : "", + }, + ]; + }, []); + + const header = ( +
+
+ Total NFTs Locked + + These NFTs are currently locked by the NFT Bridge contracts. + +
+
+ {!nftTVL.isFetching ? ( +
+ + {"Total "} + + + {nftTVL.data?.length || "0"} + +
+ ) : null} +
+ ); + + const table = ( + + ); + + const randomNFTContent = + enableRandomNFT && randomNft ? ( +
+ + +
+ ) : null; + + // const allNfts = + // nftTVL?.data?.map((thing) => ( + // + // )) || []; + + return ( + <> + {header} + {nftTVL.isFetching ? ( + + ) : ( +
+ {table} + {randomNFTContent} +
+ )} + {/* {allNfts} */} + + ); +}; + +export default NFTStats; diff --git a/bridge_ui/src/components/Stats/index.tsx b/bridge_ui/src/components/Stats/index.tsx index cd71e31d..e1c6c938 100644 --- a/bridge_ui/src/components/Stats/index.tsx +++ b/bridge_ui/src/components/Stats/index.tsx @@ -14,6 +14,8 @@ import useTVL from "../../hooks/useTVL"; import { COLORS } from "../../muiTheme"; import SmartAddress from "../SmartAddress"; import { balancePretty } from "../TokenSelectors/TokenPicker"; +import CustodyAddresses from "./CustodyAddresses"; +import NFTStats from "./NFTStats"; import MuiReactTable from "./tableComponents/MuiReactTable"; const useStyles = makeStyles((theme) => ({ @@ -36,11 +38,11 @@ const useStyles = makeStyles((theme) => ({ }, mainPaper: { backgroundColor: COLORS.nearBlackWithMinorTransparency, - textAlign: "center", padding: "2rem", - "& > h, p ": { + "& > h, & > p ": { margin: ".5rem", }, + marginBottom: theme.spacing(2), }, flexBox: { display: "flex", @@ -68,11 +70,16 @@ const useStyles = makeStyles((theme) => ({ marginLeft: theme.spacing(0.5), marginBottom: "-.125em", // line up number with label }, + alignCenter: { + margin: "0 auto", + display: "block", + }, })); const StatsRoot: React.FC = () => { const classes = useStyles(); const tvl = useTVL(); + const sortTokens = useMemo(() => { return (rowA: any, rowB: any) => { if (rowA.isGrouped && rowB.isGrouped) { @@ -195,19 +202,16 @@ const StatsRoot: React.FC = () => { return ( - {tvl.isFetching ? ( - - ) : ( - <> -
-
- Total Value Locked - - These assets are currently locked by the Token Bridge - contracts. - -
-
+ <> +
+
+ Total Value Locked + + These assets are currently locked by the Token Bridge contracts. + +
+
+ {!tvl.isFetching ? (
= () => { {tvlString}
-
+ ) : null} +
+ {!tvl.isFetching ? ( - - )} + ) : ( + + )} + + + + + + + ); diff --git a/bridge_ui/src/components/TokenSelectors/NFTViewer.tsx b/bridge_ui/src/components/TokenSelectors/NFTViewer.tsx index fb5dd7e5..fd359b96 100644 --- a/bridge_ui/src/components/TokenSelectors/NFTViewer.tsx +++ b/bridge_ui/src/components/TokenSelectors/NFTViewer.tsx @@ -8,7 +8,7 @@ import { Typography, } from "@material-ui/core"; import axios from "axios"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useState } from "react"; import { NFTParsedTokenAccount } from "../../store/nftSlice"; import clsx from "clsx"; import { @@ -22,6 +22,8 @@ import bscIcon from "../../icons/bsc.svg"; import ethIcon from "../../icons/eth.svg"; import solanaIcon from "../../icons/solana.svg"; import useCopyToClipboard from "../../hooks/useCopyToClipboard"; +import { Skeleton } from "@material-ui/lab"; +import Wormhole from "../../icons/wormhole-network.svg"; const safeIPFS = (uri: string) => uri.startsWith("ipfs://ipfs/") @@ -128,11 +130,15 @@ const useStyles = makeStyles((theme) => ({ alignItems: "center", justifyContent: "center", background: "transparent", - border: "1px solid #ffb347", margin: theme.spacing(0, 2), + "& > img, & > video": { + border: "1px solid #ffb347", + }, }, solanaMediaBorder: { - borderColor: "#D7DDE8", + "& > img, & > video": { + borderColor: "#D7DDE8", + }, }, // thanks https://cssgradient.io/ eth: { @@ -153,8 +159,54 @@ const useStyles = makeStyles((theme) => ({ background: "linear-gradient(45deg, rgba(153,69,255,1) 0%, rgba(121,98,231,1) 20%, rgba(0,209,140,1) 100%)", }, + hidden: { + display: "none", + }, + skeleton: { + borderRadius: 9, + display: "grid", + placeItems: "center", + position: "absolute", + }, + wormholeIcon: { + height: 48, + width: 48, + filter: "contrast(0)", + transition: "filter 0.5s", + "&:hover": { + filter: "contrast(1)", + }, + verticalAlign: "middle", + marginRight: theme.spacing(1), + zIndex: 10, + }, + wormholePositioner: { + display: "grid", + placeItems: "center", + position: "relative", + height: "500px", + width: "400px", + margin: `${theme.spacing(1)}px auto`, + }, })); +const ViewerLoader = () => { + const classes = useStyles(); + + return ( +
+ + Wormhole +
+ ); +}; + export default function NFTViewer({ value, chainId, @@ -168,6 +220,7 @@ export default function NFTViewer({ animation_url: value.animation_url, nftName: value.nftName, description: value.description, + isLoading: !!uri, }); useEffect(() => { setMetadata({ @@ -175,20 +228,28 @@ export default function NFTViewer({ animation_url: value.animation_url, nftName: value.nftName, description: value.description, + isLoading: !!uri, }); - }, [value]); + }, [value, uri]); useEffect(() => { if (uri) { let cancelled = false; (async () => { - const result = await axios.get(uri); - if (!cancelled && result && result.data) { - setMetadata({ - image: result.data.image, - animation_url: result.data.animation_url, - nftName: result.data.name, - description: result.data.description, - }); + try { + const result = await axios.get(uri); + if (!cancelled && result && result.data) { + setMetadata({ + image: result.data.image, + animation_url: result.data.animation_url, + nftName: result.data.name, + description: result.data.description, + isLoading: false, + }); + } else if (!cancelled) { + setIsLoading(false); + } + } catch (e) { + setIsLoading(false); } })(); return () => { @@ -196,6 +257,14 @@ export default function NFTViewer({ }; } }, [uri]); + const [isLoading, setIsLoading] = useState(true); + const onLoad = useCallback(() => { + setIsLoading(false); + }, []); + useLayoutEffect(() => { + setIsLoading(true); + }, [value, chainId]); + const classes = useStyles(); const animLower = metadata.animation_url?.toLowerCase(); // const has3DModel = animLower?.endsWith('gltf') || animLower?.endsWith('glb') @@ -212,80 +281,115 @@ export default function NFTViewer({ animLower?.endsWith("flac") || animLower?.endsWith("wav") || animLower?.endsWith("oga"); + const hasImage = metadata.image; const image = ( {metadata.nftName ); const copyTokenId = useCopyToClipboard(value.tokenId || ""); + + //report that loading is done, if the item has no reasonable media + useEffect(() => { + if (!metadata.isLoading && !hasVideo && !hasAudio && !hasImage) { + setIsLoading(false); + } + }, [metadata.isLoading, hasVideo, hasAudio, hasImage]); + + const media = ( + <> + {hasVideo ? ( + + ) : hasImage ? ( + image + ) : null} + {hasAudio ? ( +