bridge_ui: stats page enhancements

Change-Id: I1a4c3fd2145f51d7bdeb80bdd173769db2d8daad
This commit is contained in:
Chase Moran 2021-10-25 12:39:26 -04:00
parent a1bc316590
commit 94f26e1637
8 changed files with 867 additions and 94 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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>
);

View File

@ -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>
</>
);
}

View File

@ -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);
});

View File

@ -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;

View File

@ -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,

View File

@ -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";