diff --git a/bridge_ui/src/App.js b/bridge_ui/src/App.js index 8fc82b2df..254e1eb00 100644 --- a/bridge_ui/src/App.js +++ b/bridge_ui/src/App.js @@ -18,11 +18,12 @@ import { } from "react-router-dom"; import Attest from "./components/Attest"; import Home from "./components/Home"; -import NFT from "./components/NFT"; -import Transfer from "./components/Transfer"; import Migration from "./components/Migration"; +import NFT from "./components/NFT"; +import NFTOriginVerifier from "./components/NFTOriginVerifier"; +import Transfer from "./components/Transfer"; import wormholeLogo from "./icons/wormhole.svg"; -import { CLUSTER, ENABLE_NFT } from "./utils/consts"; +import { CLUSTER } from "./utils/consts"; const useStyles = makeStyles((theme) => ({ appBar: { @@ -78,22 +79,11 @@ function App() {
- {ENABLE_NFT ? ( - - - NFTs - - - ) : ( - - - NFTs - - - )} + + + NFTs + + - {ENABLE_NFT ? ( - - - - ) : null} + + + + + + diff --git a/bridge_ui/src/components/NFT/Source.tsx b/bridge_ui/src/components/NFT/Source.tsx index 3c336dc0c..ec76afb5a 100644 --- a/bridge_ui/src/components/NFT/Source.tsx +++ b/bridge_ui/src/components/NFT/Source.tsx @@ -1,6 +1,6 @@ import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk"; import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core"; -import { Restore } from "@material-ui/icons"; +import { Restore, VerifiedUser } from "@material-ui/icons"; import { useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import useIsWalletReady from "../../hooks/useIsWalletReady"; @@ -19,11 +19,18 @@ import StepDescription from "../StepDescription"; import { TokenSelector } from "../TokenSelectors/SourceTokenSelector"; import { Alert } from "@material-ui/lab"; import LowBalanceWarning from "../LowBalanceWarning"; +import { Link } from "react-router-dom"; const useStyles = makeStyles((theme) => ({ transferField: { marginTop: theme.spacing(5), }, + buttonWrapper: { + textAlign: "right", + }, + nftOriginVerifierButton: { + marginTop: theme.spacing(0.5), + }, })); function Source({ @@ -54,14 +61,30 @@ function Source({
Select an NFT to send through the Wormhole NFT Bridge.
- +
+
+ +
+
+ +
+
({ + centeredContainer: { + textAlign: "center", + width: "100%", + }, + header: { + marginTop: theme.spacing(12), + marginBottom: theme.spacing(4), + [theme.breakpoints.down("sm")]: { + marginBottom: theme.spacing(4), + }, + }, + linearGradient: { + background: `linear-gradient(to left, ${COLORS.blue}, ${COLORS.green});`, + WebkitBackgroundClip: "text", + backgroundClip: "text", + WebkitTextFillColor: "transparent", + MozBackgroundClip: "text", + MozTextFillColor: "transparent", + filter: `drop-shadow( 0px 0px 8px ${COLORS.nearBlack}) drop-shadow( 0px 0px 14px ${COLORS.nearBlack}) drop-shadow( 0px 0px 24px ${COLORS.nearBlack})`, + }, + mainCard: { + padding: theme.spacing(1), + borderRadius: "5px", + backgroundColor: COLORS.nearBlackWithMinorTransparency, + }, + originHeader: { + marginTop: theme.spacing(4), + }, + viewButtonWrapper: { + textAlign: "center", + }, + viewButton: { + marginTop: theme.spacing(1), + }, + loaderWrapper: { + margin: theme.spacing(2), + textAlign: "center", + }, +})); + +export default function NFTOriginVerifier() { + const classes = useStyles(); + const { provider, signerAddress } = useEthereumProvider(); + const [lookupChain, setLookupChain] = useState(CHAIN_ID_ETH); + const { isReady, statusMessage } = useIsWalletReady(lookupChain); + const [lookupAsset, setLookupAsset] = useState(""); + const [lookupTokenId, setLookupTokenId] = useState(""); + const [lookupError, setLookupError] = useState(""); + const [parsedTokenAccount, setParsedTokenAccount] = useState< + NFTParsedTokenAccount | undefined + >(undefined); + const [originInfo, setOriginInfo] = useState< + WormholeWrappedNFTInfo | undefined + >(undefined); + const [isLoading, setIsLoading] = useState(false); + const handleChainChange = useCallback((event) => { + setLookupChain(event.target.value); + }, []); + const handleAssetChange = useCallback((event) => { + setLookupAsset(event.target.value); + }, []); + const handleTokenIdChange = useCallback((event) => { + setLookupTokenId(event.target.value); + }, []); + useEffect(() => { + let cancelled = false; + setLookupError(""); + setParsedTokenAccount(undefined); + setOriginInfo(undefined); + if ( + isReady && + provider && + signerAddress && + lookupChain === CHAIN_ID_ETH && + lookupAsset && + lookupTokenId + ) { + if (isValidEthereumAddress(lookupAsset)) { + (async () => { + setIsLoading(true); + try { + const token = await getEthereumNFT(lookupAsset, provider); + const result = await isNFT(token); + if (result) { + const newParsedTokenAccount = await ethNFTToNFTParsedTokenAccount( + token, + lookupTokenId, + signerAddress + ); + const info = await getOriginalAssetEth( + ETH_NFT_BRIDGE_ADDRESS, + provider, + lookupAsset, + lookupTokenId + ); + if (!cancelled) { + setIsLoading(false); + setParsedTokenAccount(newParsedTokenAccount); + setOriginInfo(info); + } + } else if (!cancelled) { + setIsLoading(false); + setLookupError( + "This token does not support ERC-165, ERC-721, and ERC-721 metadata" + ); + } + } catch (e) { + console.error(e); + if (!cancelled) { + setIsLoading(false); + setLookupError( + "This token does not support ERC-165, ERC-721, and ERC-721 metadata" + ); + } + } + })(); + } else { + setLookupError("Invalid address"); + } + } else if (lookupChain === CHAIN_ID_SOLANA && lookupAsset) { + (async () => { + try { + setIsLoading(true); + const [metadata] = await getMetaplexData([lookupAsset]); + if (metadata) { + const connection = new Connection(SOLANA_HOST, "confirmed"); + const info = await getOriginalAssetSol( + connection, + SOL_NFT_BRIDGE_ADDRESS, + lookupAsset + ); + if (!cancelled) { + setIsLoading(false); + setParsedTokenAccount({ + amount: "0", + decimals: 0, + mintKey: lookupAsset, + publicKey: "", + uiAmount: 0, + uiAmountString: "0", + uri: metadata.data.uri, + }); + setOriginInfo(info); + } + } else { + if (!cancelled) { + setIsLoading(false); + setLookupError("Error fetching metadata"); + } + } + } catch (e) { + console.error(e); + if (!cancelled) { + setIsLoading(false); + setLookupError("Invalid token"); + } + } + })(); + } + return () => { + cancelled = true; + }; + }, [ + isReady, + provider, + signerAddress, + lookupChain, + lookupAsset, + lookupTokenId, + ]); + const readableAddress = + originInfo && + originInfo.chainId && + originInfo.assetAddress && + hexToNativeString( + uint8ArrayToHex(originInfo.assetAddress), + originInfo.chainId + ); + const displayError = + (lookupChain === CHAIN_ID_ETH && statusMessage) || lookupError; + return ( +
+ +
+ + NFT Origin Verifier + +
+
+ + + + This page allows you to find where a Wormhole-bridged NFT was + originally minted so you can verify its authenticity. + + + {CHAINS.filter( + ({ id }) => id === CHAIN_ID_ETH || id === CHAIN_ID_SOLANA + ).map(({ id, name }) => ( + + {name} + + ))} + + {lookupChain === CHAIN_ID_ETH || lookupChain === CHAIN_ID_BSC ? ( + + ) : null} + + {lookupChain === CHAIN_ID_ETH ? ( + + ) : null} + {displayError ? ( + {displayError} + ) : null} + {isLoading ? ( +
+ +
+ ) : null} + {parsedTokenAccount ? ( + + ) : null} + {originInfo ? ( + <> + + Origin Info + + + Address: {readableAddress} + + {originInfo.chainId === CHAIN_ID_SOLANA ? null : ( + + Token ID: {originInfo.tokenId} + + )} +
+ {originInfo.chainId === CHAIN_ID_SOLANA ? ( + + ) : ( + + )} +
+ + ) : null} +
+
+
+ ); +} diff --git a/bridge_ui/src/components/TokenSelectors/NFTViewer.tsx b/bridge_ui/src/components/TokenSelectors/NFTViewer.tsx index 654860b84..849e3c50a 100644 --- a/bridge_ui/src/components/TokenSelectors/NFTViewer.tsx +++ b/bridge_ui/src/components/TokenSelectors/NFTViewer.tsx @@ -110,6 +110,9 @@ const useStyles = makeStyles((theme) => ({ }, mediaContent: { display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", background: "transparent", border: "1px solid #ffb347", margin: theme.spacing(0, 2), diff --git a/bridge_ui/src/hooks/useMetaplexData.ts b/bridge_ui/src/hooks/useMetaplexData.ts index 1d7177c6a..fc5510dcd 100644 --- a/bridge_ui/src/hooks/useMetaplexData.ts +++ b/bridge_ui/src/hooks/useMetaplexData.ts @@ -9,7 +9,7 @@ import { } from "../utils/metaplex"; import { getMultipleAccountsRPC } from "../utils/solana"; -const getMetaplexData = async (mintAddresses: string[]) => { +export const getMetaplexData = async (mintAddresses: string[]) => { const promises = []; for (const address of mintAddresses) { promises.push(getMetadataAddress(address)); diff --git a/bridge_ui/src/utils/consts.ts b/bridge_ui/src/utils/consts.ts index 7f605954f..e926588a8 100644 --- a/bridge_ui/src/utils/consts.ts +++ b/bridge_ui/src/utils/consts.ts @@ -15,7 +15,6 @@ export const CLUSTER: Cluster = : process.env.REACT_APP_CLUSTER === "testnet" ? "testnet" : "devnet"; -export const ENABLE_NFT = true; export interface ChainInfo { id: ChainId; name: string;