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 { COLORS } from "../../muiTheme";
import SmartAddress from "../SmartAddress"; import SmartAddress from "../SmartAddress";
import { balancePretty } from "../TokenSelectors/TokenPicker"; import { balancePretty } from "../TokenSelectors/TokenPicker";
import CustodyAddresses from "./CustodyAddresses";
import NFTStats from "./NFTStats";
import MuiReactTable from "./tableComponents/MuiReactTable"; import MuiReactTable from "./tableComponents/MuiReactTable";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
@ -36,11 +38,11 @@ const useStyles = makeStyles((theme) => ({
}, },
mainPaper: { mainPaper: {
backgroundColor: COLORS.nearBlackWithMinorTransparency, backgroundColor: COLORS.nearBlackWithMinorTransparency,
textAlign: "center",
padding: "2rem", padding: "2rem",
"& > h, p ": { "& > h, & > p ": {
margin: ".5rem", margin: ".5rem",
}, },
marginBottom: theme.spacing(2),
}, },
flexBox: { flexBox: {
display: "flex", display: "flex",
@ -68,11 +70,16 @@ const useStyles = makeStyles((theme) => ({
marginLeft: theme.spacing(0.5), marginLeft: theme.spacing(0.5),
marginBottom: "-.125em", // line up number with label marginBottom: "-.125em", // line up number with label
}, },
alignCenter: {
margin: "0 auto",
display: "block",
},
})); }));
const StatsRoot: React.FC<any> = () => { const StatsRoot: React.FC<any> = () => {
const classes = useStyles(); const classes = useStyles();
const tvl = useTVL(); const tvl = useTVL();
const sortTokens = useMemo(() => { const sortTokens = useMemo(() => {
return (rowA: any, rowB: any) => { return (rowA: any, rowB: any) => {
if (rowA.isGrouped && rowB.isGrouped) { if (rowA.isGrouped && rowB.isGrouped) {
@ -195,19 +202,16 @@ const StatsRoot: React.FC<any> = () => {
return ( return (
<Container maxWidth="lg"> <Container maxWidth="lg">
<Paper className={classes.mainPaper}> <Paper className={classes.mainPaper}>
{tvl.isFetching ? (
<CircularProgress />
) : (
<> <>
<div className={classes.flexBox}> <div className={classes.flexBox}>
<div className={classes.explainerContainer}> <div className={classes.explainerContainer}>
<Typography variant="h5">Total Value Locked</Typography> <Typography variant="h5">Total Value Locked</Typography>
<Typography variant="subtitle2" color="textSecondary"> <Typography variant="subtitle2" color="textSecondary">
These assets are currently locked by the Token Bridge These assets are currently locked by the Token Bridge contracts.
contracts.
</Typography> </Typography>
</div> </div>
<div className={classes.grower} /> <div className={classes.grower} />
{!tvl.isFetching ? (
<div <div
className={clsx( className={clsx(
classes.explainerContainer, classes.explainerContainer,
@ -231,15 +235,25 @@ const StatsRoot: React.FC<any> = () => {
{tvlString} {tvlString}
</Typography> </Typography>
</div> </div>
) : null}
</div> </div>
{!tvl.isFetching ? (
<MuiReactTable <MuiReactTable
columns={tvlColumns} columns={tvlColumns}
data={tvl.data} data={tvl.data}
skipPageReset={false} skipPageReset={false}
initialState={{ sortBy: [{ id: "totalValue", desc: true }] }} initialState={{ sortBy: [{ id: "totalValue", desc: true }] }}
/> />
</> ) : (
<CircularProgress className={classes.alignCenter} />
)} )}
</>
</Paper>
<Paper className={classes.mainPaper}>
<NFTStats />
</Paper>
<Paper className={classes.mainPaper}>
<CustodyAddresses />
</Paper> </Paper>
</Container> </Container>
); );

View File

@ -8,7 +8,7 @@ import {
Typography, Typography,
} from "@material-ui/core"; } from "@material-ui/core";
import axios from "axios"; import axios from "axios";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useLayoutEffect, useState } from "react";
import { NFTParsedTokenAccount } from "../../store/nftSlice"; import { NFTParsedTokenAccount } from "../../store/nftSlice";
import clsx from "clsx"; import clsx from "clsx";
import { import {
@ -22,6 +22,8 @@ import bscIcon from "../../icons/bsc.svg";
import ethIcon from "../../icons/eth.svg"; import ethIcon from "../../icons/eth.svg";
import solanaIcon from "../../icons/solana.svg"; import solanaIcon from "../../icons/solana.svg";
import useCopyToClipboard from "../../hooks/useCopyToClipboard"; import useCopyToClipboard from "../../hooks/useCopyToClipboard";
import { Skeleton } from "@material-ui/lab";
import Wormhole from "../../icons/wormhole-network.svg";
const safeIPFS = (uri: string) => const safeIPFS = (uri: string) =>
uri.startsWith("ipfs://ipfs/") uri.startsWith("ipfs://ipfs/")
@ -128,12 +130,16 @@ const useStyles = makeStyles((theme) => ({
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
background: "transparent", background: "transparent",
border: "1px solid #ffb347",
margin: theme.spacing(0, 2), margin: theme.spacing(0, 2),
"& > img, & > video": {
border: "1px solid #ffb347",
},
}, },
solanaMediaBorder: { solanaMediaBorder: {
"& > img, & > video": {
borderColor: "#D7DDE8", borderColor: "#D7DDE8",
}, },
},
// thanks https://cssgradient.io/ // thanks https://cssgradient.io/
eth: { eth: {
// colors from https://en.wikipedia.org/wiki/Ethereum#/media/File:Ethereum-icon-purple.svg // colors from https://en.wikipedia.org/wiki/Ethereum#/media/File:Ethereum-icon-purple.svg
@ -153,8 +159,54 @@ const useStyles = makeStyles((theme) => ({
background: background:
"linear-gradient(45deg, rgba(153,69,255,1) 0%, rgba(121,98,231,1) 20%, rgba(0,209,140,1) 100%)", "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({ export default function NFTViewer({
value, value,
chainId, chainId,
@ -168,6 +220,7 @@ export default function NFTViewer({
animation_url: value.animation_url, animation_url: value.animation_url,
nftName: value.nftName, nftName: value.nftName,
description: value.description, description: value.description,
isLoading: !!uri,
}); });
useEffect(() => { useEffect(() => {
setMetadata({ setMetadata({
@ -175,12 +228,14 @@ export default function NFTViewer({
animation_url: value.animation_url, animation_url: value.animation_url,
nftName: value.nftName, nftName: value.nftName,
description: value.description, description: value.description,
isLoading: !!uri,
}); });
}, [value]); }, [value, uri]);
useEffect(() => { useEffect(() => {
if (uri) { if (uri) {
let cancelled = false; let cancelled = false;
(async () => { (async () => {
try {
const result = await axios.get(uri); const result = await axios.get(uri);
if (!cancelled && result && result.data) { if (!cancelled && result && result.data) {
setMetadata({ setMetadata({
@ -188,7 +243,13 @@ export default function NFTViewer({
animation_url: result.data.animation_url, animation_url: result.data.animation_url,
nftName: result.data.name, nftName: result.data.name,
description: result.data.description, description: result.data.description,
isLoading: false,
}); });
} else if (!cancelled) {
setIsLoading(false);
}
} catch (e) {
setIsLoading(false);
} }
})(); })();
return () => { return () => {
@ -196,6 +257,14 @@ export default function NFTViewer({
}; };
} }
}, [uri]); }, [uri]);
const [isLoading, setIsLoading] = useState(true);
const onLoad = useCallback(() => {
setIsLoading(false);
}, []);
useLayoutEffect(() => {
setIsLoading(true);
}, [value, chainId]);
const classes = useStyles(); const classes = useStyles();
const animLower = metadata.animation_url?.toLowerCase(); const animLower = metadata.animation_url?.toLowerCase();
// const has3DModel = animLower?.endsWith('gltf') || animLower?.endsWith('glb') // const has3DModel = animLower?.endsWith('gltf') || animLower?.endsWith('glb')
@ -212,18 +281,62 @@ export default function NFTViewer({
animLower?.endsWith("flac") || animLower?.endsWith("flac") ||
animLower?.endsWith("wav") || animLower?.endsWith("wav") ||
animLower?.endsWith("oga"); animLower?.endsWith("oga");
const hasImage = metadata.image;
const image = ( const image = (
<img <img
src={safeIPFS(metadata.image || "")} src={safeIPFS(metadata.image || "")}
alt={metadata.nftName || ""} alt={metadata.nftName || ""}
style={{ maxWidth: "100%" }} style={{ maxWidth: "100%" }}
onLoad={onLoad}
onError={onLoad}
/> />
); );
const copyTokenId = useCopyToClipboard(value.tokenId || ""); 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 ( return (
<>
<div className={!isLoading ? classes.hidden : ""}>
<ViewerLoader />
</div>
<Card <Card
className={clsx(classes.card, { className={clsx(classes.card, {
[classes.solanaBorder]: chainId === CHAIN_ID_SOLANA, [classes.solanaBorder]: chainId === CHAIN_ID_SOLANA,
[classes.hidden]: isLoading,
})} })}
elevation={10} elevation={10}
> >
@ -255,17 +368,7 @@ export default function NFTViewer({
[classes.solanaMediaBorder]: chainId === CHAIN_ID_SOLANA, [classes.solanaMediaBorder]: chainId === CHAIN_ID_SOLANA,
})} })}
> >
{hasVideo ? ( {media}
<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> </CardMedia>
<CardContent className={classes.detailsContent}> <CardContent className={classes.detailsContent}>
{metadata.description ? ( {metadata.description ? (
@ -287,5 +390,6 @@ export default function NFTViewer({
</CardContent> </CardContent>
</div> </div>
</Card> </Card>
</>
); );
} }

View File

@ -19,6 +19,7 @@ export type GenericMetadata = {
tokenName?: string; tokenName?: string;
decimals?: number; decimals?: number;
//TODO more items //TODO more items
raw?: any;
}; };
const constructSolanaMetadata = ( 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? 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, tokenName: tokenInfo?.name || metaplex?.data.name || undefined,
decimals: tokenInfo?.decimals || undefined, //TODO decimals are actually on the mint, not the metaplex account. decimals: tokenInfo?.decimals || undefined, //TODO decimals are actually on the mint, not the metaplex account.
raw: metaplex,
}; };
data.set(address, obj); 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(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
const connection = new Connection(SOLANA_HOST, "confirmed"); const connection = new Connection(SOLANA_HOST, "confirmed");
setSolanaCustodyTokensLoading(true);
connection connection
.getParsedTokenAccountsByOwner(new PublicKey(SOL_CUSTODY_ADDRESS), { .getParsedTokenAccountsByOwner(new PublicKey(SOL_CUSTODY_ADDRESS), {
programId: TOKEN_PROGRAM_ID, programId: TOKEN_PROGRAM_ID,

View File

@ -225,6 +225,8 @@ export const SOL_TOKEN_BRIDGE_ADDRESS =
export const SOL_CUSTODY_ADDRESS = export const SOL_CUSTODY_ADDRESS =
"GugU1tP7doLeTw9hQP51xRJyS8Da1fWxuiy2rVrnMD2m"; "GugU1tP7doLeTw9hQP51xRJyS8Da1fWxuiy2rVrnMD2m";
export const SOL_NFT_CUSTODY_ADDRESS =
"D63bhHo634eXSj4Jq3xgu2fjB5XKc8DFHzDY9iZk7fv1";
export const TERRA_TEST_TOKEN_ADDRESS = export const TERRA_TEST_TOKEN_ADDRESS =
"terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh"; "terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh";
export const TERRA_BRIDGE_ADDRESS = export const TERRA_BRIDGE_ADDRESS =
@ -278,7 +280,8 @@ export const COVALENT_BSC_MAINNET = "56";
export const COVALENT_GET_TOKENS_URL = ( export const COVALENT_GET_TOKENS_URL = (
chainId: ChainId, chainId: ChainId,
walletAddress: string, walletAddress: string,
nft?: boolean nft?: boolean,
noNftMetadata?: boolean
) => { ) => {
const chainNum = const chainNum =
chainId === CHAIN_ID_ETH 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/ // 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}${ return `https://api.covalenthq.com/v1/${chainNum}/address/${walletAddress}/balances_v2/?key=${COVALENT_API_KEY}${
nft ? "&nft=true" : "" nft ? "&nft=true" : ""
}`; }${noNftMetadata ? "&no-nft-fetch=true" : ""}`;
}; };
export const TERRA_SWAPRATE_URL = export const TERRA_SWAPRATE_URL =
"https://fcd.terra.dev/v1/market/swaprate/uusd"; "https://fcd.terra.dev/v1/market/swaprate/uusd";