bridge_ui: stats page enhancements
Change-Id: I1a4c3fd2145f51d7bdeb80bdd173769db2d8daad
This commit is contained in:
parent
a1bc316590
commit
94f26e1637
|
@ -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<any> = () => {
|
||||
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 ? (
|
||||
<SmartAddress
|
||||
chainId={value.row?.original?.chainId}
|
||||
address={value.row?.original?.tokenAddress}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: "NFT Address",
|
||||
id: "nftAddress",
|
||||
accessor: "address",
|
||||
disableGroupBy: true,
|
||||
Cell: (value: any) =>
|
||||
value.row?.original?.nftAddress && value.row?.original?.chainId ? (
|
||||
<SmartAddress
|
||||
chainId={value.row?.original?.chainId}
|
||||
address={value.row?.original?.nftAddress}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
),
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
const header = (
|
||||
<div className={classes.flexBox}>
|
||||
<div className={classes.explainerContainer}>
|
||||
<Typography variant="h5">Custody Addresses</Typography>
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
These are the custody addresses which hold collateralized assets for
|
||||
the token bridge.
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.grower} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const table = (
|
||||
<MuiReactTable
|
||||
columns={tvlColumns}
|
||||
data={data || []}
|
||||
skipPageReset={false}
|
||||
initialState={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{header}
|
||||
{table}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustodyAddresses;
|
|
@ -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<any> = () => {
|
||||
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<number | null>(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 ? (
|
||||
// <SmartAddress
|
||||
// chainId={value.row?.original?.chainId}
|
||||
// address={value.row?.original?.contractAddress}
|
||||
// />
|
||||
// ) : (
|
||||
// ""
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
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 = (
|
||||
<div className={classes.flexBox}>
|
||||
<div className={classes.explainerContainer}>
|
||||
<Typography variant="h5">Total NFTs Locked</Typography>
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
These NFTs are currently locked by the NFT Bridge contracts.
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.grower} />
|
||||
{!nftTVL.isFetching ? (
|
||||
<div
|
||||
className={clsx(classes.explainerContainer, classes.totalContainer)}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
component="div"
|
||||
noWrap
|
||||
>
|
||||
{"Total "}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h3"
|
||||
component="div"
|
||||
noWrap
|
||||
className={classes.totalValue}
|
||||
>
|
||||
{nftTVL.data?.length || "0"}
|
||||
</Typography>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
const table = (
|
||||
<MuiReactTable
|
||||
columns={tvlColumns}
|
||||
data={data || []}
|
||||
skipPageReset={false}
|
||||
initialState={{ sortBy: [{ id: "nftCount", desc: true }] }}
|
||||
/>
|
||||
);
|
||||
|
||||
const randomNFTContent =
|
||||
enableRandomNFT && randomNft ? (
|
||||
<div className={classes.randomNftContainer}>
|
||||
<Button
|
||||
className={classes.randomButton}
|
||||
variant="contained"
|
||||
onClick={genRandomNumber}
|
||||
color="primary"
|
||||
>
|
||||
Load Random Wormhole NFT
|
||||
</Button>
|
||||
<NFTViewer chainId={randomNft.chainId} value={randomNft} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// const allNfts =
|
||||
// nftTVL?.data?.map((thing) => (
|
||||
// <NFTViewer chainId={thing.chainId} value={thing} />
|
||||
// )) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{header}
|
||||
{nftTVL.isFetching ? (
|
||||
<CircularProgress className={classes.alignCenter} />
|
||||
) : (
|
||||
<div className={classes.tableBox}>
|
||||
{table}
|
||||
{randomNFTContent}
|
||||
</div>
|
||||
)}
|
||||
{/* {allNfts} */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NFTStats;
|
|
@ -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<any> = () => {
|
||||
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<any> = () => {
|
|||
return (
|
||||
<Container maxWidth="lg">
|
||||
<Paper className={classes.mainPaper}>
|
||||
{tvl.isFetching ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
<>
|
||||
<div className={classes.flexBox}>
|
||||
<div className={classes.explainerContainer}>
|
||||
<Typography variant="h5">Total Value Locked</Typography>
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
These assets are currently locked by the Token Bridge
|
||||
contracts.
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.grower} />
|
||||
<>
|
||||
<div className={classes.flexBox}>
|
||||
<div className={classes.explainerContainer}>
|
||||
<Typography variant="h5">Total Value Locked</Typography>
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
These assets are currently locked by the Token Bridge contracts.
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.grower} />
|
||||
{!tvl.isFetching ? (
|
||||
<div
|
||||
className={clsx(
|
||||
classes.explainerContainer,
|
||||
|
@ -231,15 +235,25 @@ const StatsRoot: React.FC<any> = () => {
|
|||
{tvlString}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{!tvl.isFetching ? (
|
||||
<MuiReactTable
|
||||
columns={tvlColumns}
|
||||
data={tvl.data}
|
||||
skipPageReset={false}
|
||||
initialState={{ sortBy: [{ id: "totalValue", desc: true }] }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
) : (
|
||||
<CircularProgress className={classes.alignCenter} />
|
||||
)}
|
||||
</>
|
||||
</Paper>
|
||||
<Paper className={classes.mainPaper}>
|
||||
<NFTStats />
|
||||
</Paper>
|
||||
<Paper className={classes.mainPaper}>
|
||||
<CustodyAddresses />
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
|
|
|
@ -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 (
|
||||
<div className={classes.wormholePositioner}>
|
||||
<Skeleton
|
||||
width="400px"
|
||||
height="500px"
|
||||
variant="rect"
|
||||
animation="wave"
|
||||
className={classes.skeleton}
|
||||
/>
|
||||
<img src={Wormhole} alt="Wormhole" className={classes.wormholeIcon} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 = (
|
||||
<img
|
||||
src={safeIPFS(metadata.image || "")}
|
||||
alt={metadata.nftName || ""}
|
||||
style={{ maxWidth: "100%" }}
|
||||
onLoad={onLoad}
|
||||
onError={onLoad}
|
||||
/>
|
||||
);
|
||||
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 ? (
|
||||
<video
|
||||
autoPlay
|
||||
controls
|
||||
loop
|
||||
style={{ maxWidth: "100%" }}
|
||||
onLoad={onLoad}
|
||||
onError={onLoad}
|
||||
>
|
||||
<source src={safeIPFS(metadata.animation_url || "")} />
|
||||
{image}
|
||||
</video>
|
||||
) : hasImage ? (
|
||||
image
|
||||
) : null}
|
||||
{hasAudio ? (
|
||||
<audio
|
||||
controls
|
||||
src={safeIPFS(metadata.animation_url || "")}
|
||||
onLoad={onLoad}
|
||||
onError={onLoad}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={clsx(classes.card, {
|
||||
[classes.solanaBorder]: chainId === CHAIN_ID_SOLANA,
|
||||
})}
|
||||
elevation={10}
|
||||
>
|
||||
<div
|
||||
className={clsx(classes.cardInset, {
|
||||
[classes.eth]: chainId === CHAIN_ID_ETH,
|
||||
[classes.bsc]: chainId === CHAIN_ID_BSC,
|
||||
[classes.solana]: chainId === CHAIN_ID_SOLANA,
|
||||
<>
|
||||
<div className={!isLoading ? classes.hidden : ""}>
|
||||
<ViewerLoader />
|
||||
</div>
|
||||
<Card
|
||||
className={clsx(classes.card, {
|
||||
[classes.solanaBorder]: chainId === CHAIN_ID_SOLANA,
|
||||
[classes.hidden]: isLoading,
|
||||
})}
|
||||
elevation={10}
|
||||
>
|
||||
<CardContent className={classes.textContent}>
|
||||
{metadata.nftName ? (
|
||||
<Typography className={classes.title}>
|
||||
{metadata.nftName}
|
||||
</Typography>
|
||||
) : (
|
||||
<div className={classes.title} />
|
||||
)}
|
||||
<SmartAddress
|
||||
chainId={chainId}
|
||||
parsedTokenAccount={value}
|
||||
noGutter
|
||||
noUnderline
|
||||
/>
|
||||
<LogoIcon chainId={chainId} />
|
||||
</CardContent>
|
||||
<CardMedia
|
||||
className={clsx(classes.mediaContent, {
|
||||
[classes.solanaMediaBorder]: chainId === CHAIN_ID_SOLANA,
|
||||
<div
|
||||
className={clsx(classes.cardInset, {
|
||||
[classes.eth]: chainId === CHAIN_ID_ETH,
|
||||
[classes.bsc]: chainId === CHAIN_ID_BSC,
|
||||
[classes.solana]: chainId === CHAIN_ID_SOLANA,
|
||||
})}
|
||||
>
|
||||
{hasVideo ? (
|
||||
<video autoPlay controls loop style={{ maxWidth: "100%" }}>
|
||||
<source src={safeIPFS(metadata.animation_url || "")} />
|
||||
{image}
|
||||
</video>
|
||||
) : (
|
||||
image
|
||||
)}
|
||||
{hasAudio ? (
|
||||
<audio controls src={safeIPFS(metadata.animation_url || "")} />
|
||||
) : null}
|
||||
</CardMedia>
|
||||
<CardContent className={classes.detailsContent}>
|
||||
{metadata.description ? (
|
||||
<Typography variant="body2" className={classes.description}>
|
||||
{metadata.description}
|
||||
</Typography>
|
||||
) : null}
|
||||
{value.tokenId ? (
|
||||
<Typography className={classes.tokenId} align="right">
|
||||
<Tooltip title="Copy" arrow>
|
||||
<span onClick={copyTokenId}>
|
||||
{value.tokenId.length > 18
|
||||
? `#${value.tokenId.substr(0, 16)}...`
|
||||
: `#${value.tokenId}`}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
<CardContent className={classes.textContent}>
|
||||
{metadata.nftName ? (
|
||||
<Typography className={classes.title}>
|
||||
{metadata.nftName}
|
||||
</Typography>
|
||||
) : (
|
||||
<div className={classes.title} />
|
||||
)}
|
||||
<SmartAddress
|
||||
chainId={chainId}
|
||||
parsedTokenAccount={value}
|
||||
noGutter
|
||||
noUnderline
|
||||
/>
|
||||
<LogoIcon chainId={chainId} />
|
||||
</CardContent>
|
||||
<CardMedia
|
||||
className={clsx(classes.mediaContent, {
|
||||
[classes.solanaMediaBorder]: chainId === CHAIN_ID_SOLANA,
|
||||
})}
|
||||
>
|
||||
{media}
|
||||
</CardMedia>
|
||||
<CardContent className={classes.detailsContent}>
|
||||
{metadata.description ? (
|
||||
<Typography variant="body2" className={classes.description}>
|
||||
{metadata.description}
|
||||
</Typography>
|
||||
) : null}
|
||||
{value.tokenId ? (
|
||||
<Typography className={classes.tokenId} align="right">
|
||||
<Tooltip title="Copy" arrow>
|
||||
<span onClick={copyTokenId}>
|
||||
{value.tokenId.length > 18
|
||||
? `#${value.tokenId.substr(0, 16)}...`
|
||||
: `#${value.tokenId}`}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ export type GenericMetadata = {
|
|||
tokenName?: string;
|
||||
decimals?: number;
|
||||
//TODO more items
|
||||
raw?: any;
|
||||
};
|
||||
|
||||
const constructSolanaMetadata = (
|
||||
|
@ -39,6 +40,7 @@ const constructSolanaMetadata = (
|
|||
logo: tokenInfo?.logoURI || metaplex?.data.uri || undefined, //TODO is URI on metaplex actually the logo? If not, where is it?
|
||||
tokenName: tokenInfo?.name || metaplex?.data.name || undefined,
|
||||
decimals: tokenInfo?.decimals || undefined, //TODO decimals are actually on the mint, not the metaplex account.
|
||||
raw: metaplex,
|
||||
};
|
||||
data.set(address, obj);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,264 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_BSC,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||
import {
|
||||
AccountInfo,
|
||||
Connection,
|
||||
ParsedAccountData,
|
||||
PublicKey,
|
||||
} from "@solana/web3.js";
|
||||
import axios from "axios";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { DataWrapper } from "../store/helpers";
|
||||
import { NFTParsedTokenAccount } from "../store/nftSlice";
|
||||
import {
|
||||
BSC_NFT_BRIDGE_ADDRESS,
|
||||
COVALENT_GET_TOKENS_URL,
|
||||
ETH_NFT_BRIDGE_ADDRESS,
|
||||
getNFTBridgeAddressForChain,
|
||||
SOLANA_HOST,
|
||||
SOL_NFT_CUSTODY_ADDRESS,
|
||||
} from "../utils/consts";
|
||||
import { Metadata } from "../utils/metaplex";
|
||||
import useMetadata, { GenericMetadata } from "./useMetadata";
|
||||
|
||||
export type NFTTVL = NFTParsedTokenAccount & { chainId: ChainId };
|
||||
|
||||
const calcEvmTVL = (covalentReport: any, chainId: ChainId): NFTTVL[] => {
|
||||
const output: NFTTVL[] = [];
|
||||
if (!covalentReport?.data?.items?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
covalentReport.data.items.forEach((item: any) => {
|
||||
//TODO remove non nfts
|
||||
if (item.balance > 0 && item.contract_address && item.nft_data) {
|
||||
item.nft_data.forEach((nftData: any) => {
|
||||
if (nftData.token_id) {
|
||||
output.push({
|
||||
amount: item.balance,
|
||||
mintKey: item.contract_address,
|
||||
tokenId: nftData.token_id,
|
||||
publicKey: getNFTBridgeAddressForChain(chainId),
|
||||
decimals: 0,
|
||||
uiAmount: 0,
|
||||
uiAmountString: item.balance.toString(),
|
||||
chainId: chainId,
|
||||
uri: nftData.token_url,
|
||||
animation_url: nftData.external_data?.animation_url,
|
||||
external_url: nftData.external_data?.external_url,
|
||||
image: nftData.external_data?.image,
|
||||
image_256: nftData.external_data?.image_256,
|
||||
nftName: nftData.external_data?.name,
|
||||
description: nftData.external_data?.description,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
const calcSolanaTVL = (
|
||||
accounts:
|
||||
| { pubkey: PublicKey; account: AccountInfo<ParsedAccountData> }[]
|
||||
| undefined,
|
||||
metaData: DataWrapper<Map<string, GenericMetadata>>
|
||||
) => {
|
||||
const output: NFTTVL[] = [];
|
||||
if (
|
||||
!accounts ||
|
||||
!accounts.length ||
|
||||
metaData.isFetching ||
|
||||
metaData.error ||
|
||||
!metaData.data
|
||||
) {
|
||||
return output;
|
||||
}
|
||||
|
||||
accounts.forEach((item) => {
|
||||
const genericMetadata = metaData.data?.get(
|
||||
item.account.data.parsed?.info?.mint?.toString()
|
||||
);
|
||||
const raw: Metadata | undefined = genericMetadata?.raw;
|
||||
|
||||
if (
|
||||
item.account.data.parsed?.info?.tokenAmount?.uiAmount > 0 &&
|
||||
item.account.data.parsed?.info?.tokenAmount?.decimals === 0
|
||||
) {
|
||||
output.push({
|
||||
amount: item.account.data.parsed?.info?.tokenAmount?.amount,
|
||||
mintKey: item.account.data.parsed?.info?.mint,
|
||||
publicKey: getNFTBridgeAddressForChain(CHAIN_ID_SOLANA),
|
||||
decimals: 0,
|
||||
uiAmount: 0,
|
||||
uiAmountString:
|
||||
item.account.data.parsed?.info?.tokenAmount?.uiAmountString,
|
||||
chainId: CHAIN_ID_SOLANA,
|
||||
uri: raw?.data?.uri,
|
||||
symbol: raw?.data?.symbol,
|
||||
// external_url: nftData.external_data?.external_url,
|
||||
// image: nftData.external_data?.image,
|
||||
// image_256: nftData.external_data?.image_256,
|
||||
// nftName: nftData.external_data?.name,
|
||||
// description: nftData.external_data?.description,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
const useNFTTVL = (): DataWrapper<NFTTVL[]> => {
|
||||
const [ethCovalentData, setEthCovalentData] = useState(undefined);
|
||||
const [ethCovalentIsLoading, setEthCovalentIsLoading] = useState(false);
|
||||
const [ethCovalentError, setEthCovalentError] = useState("");
|
||||
|
||||
const [bscCovalentData, setBscCovalentData] = useState(undefined);
|
||||
const [bscCovalentIsLoading, setBscCovalentIsLoading] = useState(false);
|
||||
const [bscCovalentError, setBscCovalentError] = useState("");
|
||||
|
||||
const [solanaCustodyTokens, setSolanaCustodyTokens] = useState<
|
||||
{ pubkey: PublicKey; account: AccountInfo<ParsedAccountData> }[] | undefined
|
||||
>(undefined);
|
||||
const [solanaCustodyTokensLoading, setSolanaCustodyTokensLoading] =
|
||||
useState(false);
|
||||
const [solanaCustodyTokensError, setSolanaCustodyTokensError] = useState("");
|
||||
const mintAddresses = useMemo(() => {
|
||||
const addresses: string[] = [];
|
||||
solanaCustodyTokens?.forEach((item) => {
|
||||
const mintKey = item.account.data.parsed?.info?.mint?.toString();
|
||||
if (mintKey) {
|
||||
addresses.push(mintKey);
|
||||
}
|
||||
});
|
||||
return addresses;
|
||||
}, [solanaCustodyTokens]);
|
||||
|
||||
const solanaMetadata = useMetadata(CHAIN_ID_SOLANA, mintAddresses);
|
||||
|
||||
const solanaTVL = useMemo(
|
||||
() => calcSolanaTVL(solanaCustodyTokens, solanaMetadata),
|
||||
[solanaCustodyTokens, solanaMetadata]
|
||||
);
|
||||
const ethTVL = useMemo(
|
||||
() => calcEvmTVL(ethCovalentData, CHAIN_ID_ETH),
|
||||
[ethCovalentData]
|
||||
);
|
||||
const bscTVL = useMemo(
|
||||
() => calcEvmTVL(bscCovalentData, CHAIN_ID_BSC),
|
||||
[bscCovalentData]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setEthCovalentIsLoading(true);
|
||||
axios
|
||||
.get(
|
||||
COVALENT_GET_TOKENS_URL(
|
||||
CHAIN_ID_ETH,
|
||||
ETH_NFT_BRIDGE_ADDRESS,
|
||||
true,
|
||||
false
|
||||
)
|
||||
)
|
||||
.then(
|
||||
(results) => {
|
||||
if (!cancelled) {
|
||||
setEthCovalentData(results.data);
|
||||
setEthCovalentIsLoading(false);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (!cancelled) {
|
||||
setEthCovalentError("Unable to retrieve Ethereum TVL.");
|
||||
setEthCovalentIsLoading(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setBscCovalentIsLoading(true);
|
||||
axios
|
||||
.get(
|
||||
COVALENT_GET_TOKENS_URL(
|
||||
CHAIN_ID_BSC,
|
||||
BSC_NFT_BRIDGE_ADDRESS,
|
||||
true,
|
||||
false
|
||||
)
|
||||
)
|
||||
.then(
|
||||
(results) => {
|
||||
if (!cancelled) {
|
||||
setBscCovalentData(results.data);
|
||||
setBscCovalentIsLoading(false);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (!cancelled) {
|
||||
setBscCovalentError("Unable to retrieve BSC TVL.");
|
||||
setBscCovalentIsLoading(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
setSolanaCustodyTokensLoading(true);
|
||||
connection
|
||||
.getParsedTokenAccountsByOwner(new PublicKey(SOL_NFT_CUSTODY_ADDRESS), {
|
||||
programId: TOKEN_PROGRAM_ID,
|
||||
})
|
||||
.then(
|
||||
(results) => {
|
||||
if (!cancelled) {
|
||||
setSolanaCustodyTokens(results.value);
|
||||
setSolanaCustodyTokensLoading(false);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (!cancelled) {
|
||||
setSolanaCustodyTokensLoading(false);
|
||||
setSolanaCustodyTokensError(
|
||||
"Unable to retrieve Solana locked tokens."
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
return useMemo(() => {
|
||||
const tvlArray = [...ethTVL, ...bscTVL, ...solanaTVL];
|
||||
|
||||
return {
|
||||
isFetching:
|
||||
ethCovalentIsLoading ||
|
||||
bscCovalentIsLoading ||
|
||||
solanaCustodyTokensLoading,
|
||||
error: ethCovalentError || bscCovalentError || solanaCustodyTokensError,
|
||||
receivedAt: null,
|
||||
data: tvlArray,
|
||||
};
|
||||
}, [
|
||||
ethCovalentError,
|
||||
ethCovalentIsLoading,
|
||||
bscCovalentError,
|
||||
bscCovalentIsLoading,
|
||||
ethTVL,
|
||||
bscTVL,
|
||||
solanaTVL,
|
||||
solanaCustodyTokensError,
|
||||
solanaCustodyTokensLoading,
|
||||
]);
|
||||
};
|
||||
|
||||
export default useNFTTVL;
|
|
@ -267,6 +267,7 @@ const useTVL = (): DataWrapper<TVL[]> => {
|
|||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
setSolanaCustodyTokensLoading(true);
|
||||
connection
|
||||
.getParsedTokenAccountsByOwner(new PublicKey(SOL_CUSTODY_ADDRESS), {
|
||||
programId: TOKEN_PROGRAM_ID,
|
||||
|
|
|
@ -225,6 +225,8 @@ export const SOL_TOKEN_BRIDGE_ADDRESS =
|
|||
|
||||
export const SOL_CUSTODY_ADDRESS =
|
||||
"GugU1tP7doLeTw9hQP51xRJyS8Da1fWxuiy2rVrnMD2m";
|
||||
export const SOL_NFT_CUSTODY_ADDRESS =
|
||||
"D63bhHo634eXSj4Jq3xgu2fjB5XKc8DFHzDY9iZk7fv1";
|
||||
export const TERRA_TEST_TOKEN_ADDRESS =
|
||||
"terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh";
|
||||
export const TERRA_BRIDGE_ADDRESS =
|
||||
|
@ -278,7 +280,8 @@ export const COVALENT_BSC_MAINNET = "56";
|
|||
export const COVALENT_GET_TOKENS_URL = (
|
||||
chainId: ChainId,
|
||||
walletAddress: string,
|
||||
nft?: boolean
|
||||
nft?: boolean,
|
||||
noNftMetadata?: boolean
|
||||
) => {
|
||||
const chainNum =
|
||||
chainId === CHAIN_ID_ETH
|
||||
|
@ -289,7 +292,7 @@ export const COVALENT_GET_TOKENS_URL = (
|
|||
// https://www.covalenthq.com/docs/api/#get-/v1/{chain_id}/address/{address}/balances_v2/
|
||||
return `https://api.covalenthq.com/v1/${chainNum}/address/${walletAddress}/balances_v2/?key=${COVALENT_API_KEY}${
|
||||
nft ? "&nft=true" : ""
|
||||
}`;
|
||||
}${noNftMetadata ? "&no-nft-fetch=true" : ""}`;
|
||||
};
|
||||
export const TERRA_SWAPRATE_URL =
|
||||
"https://fcd.terra.dev/v1/market/swaprate/uusd";
|
||||
|
|
Loading…
Reference in New Issue