diff --git a/bridge_ui/src/components/Attest/SourcePreview.tsx b/bridge_ui/src/components/Attest/SourcePreview.tsx index cba94a2de..2cd17949e 100644 --- a/bridge_ui/src/components/Attest/SourcePreview.tsx +++ b/bridge_ui/src/components/Attest/SourcePreview.tsx @@ -5,7 +5,7 @@ import { selectAttestSourceChain, } from "../../store/selectors"; import { CHAINS_BY_ID } from "../../utils/consts"; -import { shortenAddress } from "../../utils/solana"; +import SmartAddress from "../SmartAddress"; const useStyles = makeStyles((theme) => ({ description: { @@ -18,11 +18,16 @@ export default function SourcePreview() { const sourceChain = useSelector(selectAttestSourceChain); const sourceAsset = useSelector(selectAttestSourceAsset); - const explainerString = sourceAsset - ? `You will attest ${shortenAddress(sourceAsset)} on ${ - CHAINS_BY_ID[sourceChain].name - }` - : "Step complete."; + const explainerContent = + sourceChain && sourceAsset ? ( + <> + You will attest + + on {CHAINS_BY_ID[sourceChain].name} + + ) : ( + "" + ); return ( - {explainerString} + {explainerContent} ); } diff --git a/bridge_ui/src/components/Migration/Workflow.tsx b/bridge_ui/src/components/Migration/Workflow.tsx index 663fb4997..7489dd85c 100644 --- a/bridge_ui/src/components/Migration/Workflow.tsx +++ b/bridge_ui/src/components/Migration/Workflow.tsx @@ -23,14 +23,11 @@ import useIsWalletReady from "../../hooks/useIsWalletReady"; import useMetaplexData from "../../hooks/useMetaplexData"; import useSolanaTokenMap from "../../hooks/useSolanaTokenMap"; import { MIGRATION_PROGRAM_ADDRESS, SOLANA_HOST } from "../../utils/consts"; -import { - getMultipleAccounts, - shortenAddress, - signSendAndConfirm, -} from "../../utils/solana"; +import { getMultipleAccounts, signSendAndConfirm } from "../../utils/solana"; import ButtonWithLoader from "../ButtonWithLoader"; import LowBalanceWarning from "../LowBalanceWarning"; import ShowTx from "../ShowTx"; +import SmartAddress from "../SmartAddress"; import SolanaCreateAssociatedAddress, { useAssociatedAccountExistsState, } from "../SolanaCreateAssociatedAddress"; @@ -41,7 +38,7 @@ const useStyles = makeStyles(() => ({ textAlign: "center", padding: "2rem", "& > h, p ": { - margin: "1rem", + margin: ".5rem", }, }, divider: { @@ -386,12 +383,22 @@ export default function Workflow({ const toMetadata = getMetadata(toMint); const fromMetadata = getMetadata(fromMint); - const toMintPrettyString = toMetadata.symbol - ? toMetadata.symbol + " (" + shortenAddress(toMint) + ")" - : shortenAddress(toMint); - const fromMintPrettyString = fromMetadata.symbol - ? fromMetadata.symbol + " (" + shortenAddress(fromMint) + ")" - : shortenAddress(fromMint); + const toMintPretty = ( + + ); + const fromMintPretty = ( + + ); return ( @@ -407,28 +414,40 @@ export default function Workflow({ {fromTokenAccount && toTokenAccount && fromTokenAccountBalance ? ( <> - This will migrate {fromMintPrettyString} tokens in this account: + This will migrate + {fromMintPretty} + tokens in this account: - {shortenAddress(fromTokenAccount) + - ` (Balance: ${fromTokenAccountBalance}${ - fromMetadata.symbol && " " + fromMetadata.symbol - })`} + + {`(Balance: ${fromTokenAccountBalance}${ + fromMetadata.symbol && " " + fromMetadata.symbol + })`}
- into {toMintPrettyString} tokens in this account: + into + {toMintPretty} + tokens in this account: - {shortenAddress(toTokenAccount) + - (toTokenAccountExists + + + {toTokenAccountExists ? ` (Balance: ${toTokenAccountBalance}${ (toMetadata.symbol && " " + toMetadata.symbol) || "" })` - : " (Not created yet)")} + : " (Not created yet)"} +
- Using pool {shortenAddress(poolAddress)} holding tokens in - this account: + Using pool + + holding tokens in this account: - {shortenAddress(toCustodyAddress) + - ` (Balance: ${toCustodyBalance}${ - toMetadata.symbol && " " + toMetadata.symbol - })`} + + {` (Balance: ${toCustodyBalance}${ + toMetadata.symbol && " " + toMetadata.symbol + })`} ) : null} diff --git a/bridge_ui/src/components/NFT/SourcePreview.tsx b/bridge_ui/src/components/NFT/SourcePreview.tsx index d4d4a3ea4..7ac099f6f 100644 --- a/bridge_ui/src/components/NFT/SourcePreview.tsx +++ b/bridge_ui/src/components/NFT/SourcePreview.tsx @@ -5,7 +5,7 @@ import { selectNFTSourceParsedTokenAccount, } from "../../store/selectors"; import { CHAINS_BY_ID } from "../../utils/consts"; -import { shortenAddress } from "../../utils/solana"; +import SmartAddress from "../SmartAddress"; import NFTViewer from "../TokenSelectors/NFTViewer"; const useStyles = makeStyles((theme) => ({ @@ -21,13 +21,24 @@ export default function SourcePreview() { selectNFTSourceParsedTokenAccount ); - const explainerString = sourceParsedTokenAccount - ? `You will transfer 1 NFT of ${shortenAddress( - sourceParsedTokenAccount?.mintKey - )}, from ${shortenAddress(sourceParsedTokenAccount?.publicKey)} on ${ - CHAINS_BY_ID[sourceChain].name - }` - : "Step complete."; + const explainerContent = + sourceChain && sourceParsedTokenAccount ? ( + <> + You will transfer 1 NFT of + + from + + on {CHAINS_BY_ID[sourceChain].name} + + ) : ( + "" + ); return ( <> @@ -36,7 +47,7 @@ export default function SourcePreview() { variant="subtitle2" className={classes.description} > - {explainerString} + {explainerContent} {sourceParsedTokenAccount ? ( diff --git a/bridge_ui/src/components/NFT/TargetPreview.tsx b/bridge_ui/src/components/NFT/TargetPreview.tsx index fd665e5d0..ccb049438 100644 --- a/bridge_ui/src/components/NFT/TargetPreview.tsx +++ b/bridge_ui/src/components/NFT/TargetPreview.tsx @@ -6,7 +6,7 @@ import { } from "../../store/selectors"; import { hexToNativeString } from "../../utils/array"; import { CHAINS_BY_ID } from "../../utils/consts"; -import { shortenAddress } from "../../utils/solana"; +import SmartAddress from "../SmartAddress"; const useStyles = makeStyles((theme) => ({ description: { @@ -20,11 +20,16 @@ export default function TargetPreview() { const targetAddress = useSelector(selectNFTTargetAddressHex); const targetAddressNative = hexToNativeString(targetAddress, targetChain); - const explainerString = targetAddressNative - ? `to ${shortenAddress(targetAddressNative)} on ${ - CHAINS_BY_ID[targetChain].name - }` - : "Step complete."; + const explainerContent = + targetChain && targetAddressNative ? ( + <> + to + + on {CHAINS_BY_ID[targetChain].name} + + ) : ( + "" + ); return ( - {explainerString} + {explainerContent} ); } diff --git a/bridge_ui/src/components/SmartAddress.tsx b/bridge_ui/src/components/SmartAddress.tsx new file mode 100644 index 000000000..23536d010 --- /dev/null +++ b/bridge_ui/src/components/SmartAddress.tsx @@ -0,0 +1,159 @@ +import { + ChainId, + CHAIN_ID_ETH, + CHAIN_ID_SOLANA, +} from "@certusone/wormhole-sdk"; +import { Button, makeStyles, Tooltip, Typography } from "@material-ui/core"; +import { withStyles } from "@material-ui/styles"; +import { useSnackbar } from "notistack"; +import { useCallback } from "react"; +import { ParsedTokenAccount } from "../store/transferSlice"; +import { CLUSTER } from "../utils/consts"; +import { shortenAddress } from "../utils/solana"; +import { FileCopy, OpenInNew } from "@material-ui/icons"; + +const useStyles = makeStyles((theme) => ({ + mainTypog: { + display: "inline-block", + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + textDecoration: "underline", + textUnderlineOffset: "2px", + }, + buttons: { + marginLeft: ".5rem", + marginRight: ".5rem", + }, +})); + +function pushToClipboard(content: any) { + if (!navigator.clipboard) { + // Clipboard API not available + return; + } + return navigator.clipboard.writeText(content); +} + +const tooltipStyles = { + tooltip: { + minWidth: "max-content", + textAlign: "center", + "& > *": { + margin: ".25rem", + }, + }, +}; + +// @ts-ignore +const StyledTooltip = withStyles(tooltipStyles)(Tooltip); + +export default function SmartAddress({ + chainId, + parsedTokenAccount, + address, + symbol, + tokenName, + variant, +}: { + chainId: ChainId; + parsedTokenAccount?: ParsedTokenAccount; + address?: string; + logo?: string; + tokenName?: string; + symbol?: string; + variant?: any; +}) { + const classes = useStyles(); + const useableAddress = parsedTokenAccount?.mintKey || address || ""; + const useableSymbol = parsedTokenAccount?.symbol || symbol || ""; + const isNative = parsedTokenAccount?.isNativeAsset || false; + const addressShort = shortenAddress(useableAddress) || ""; + const { enqueueSnackbar } = useSnackbar(); + + const useableName = isNative + ? "Native Currency" + : parsedTokenAccount?.name + ? parsedTokenAccount.name + : tokenName + ? tokenName + : ""; + //TODO terra + const explorerAddress = isNative + ? null + : chainId === CHAIN_ID_ETH + ? `https://${ + CLUSTER === "testnet" ? "goerli." : "" + }etherscan.io/address/${useableAddress}` + : chainId === CHAIN_ID_SOLANA + ? `https://explorer.solana.com/address/${useableAddress}${ + CLUSTER === "testnet" + ? "?cluster=testnet" + : CLUSTER === "devnet" + ? "?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899" + : "" + }` + : undefined; + const explorerName = chainId === CHAIN_ID_ETH ? "Etherscan" : "Explorer"; + + const copyToClipboard = useCallback(() => { + pushToClipboard(useableAddress)?.then(() => { + enqueueSnackbar("Copied address to clipboard.", { variant: "success" }); + }); + }, [useableAddress, enqueueSnackbar]); + + const explorerButton = !explorerAddress ? null : ( + + ); + //TODO add icon here + const copyButton = isNative ? null : ( + + ); + + const tooltipContent = ( + <> + {useableName && {useableName}} + {useableSymbol && !isNative && ( + + {addressShort} + + )} +
+ {explorerButton} + {copyButton} +
+ + ); + + return ( + + + {useableSymbol || addressShort} + + + ); +} diff --git a/bridge_ui/src/components/Transfer/SourcePreview.tsx b/bridge_ui/src/components/Transfer/SourcePreview.tsx index 598a6ccc9..1bd17bb3c 100644 --- a/bridge_ui/src/components/Transfer/SourcePreview.tsx +++ b/bridge_ui/src/components/Transfer/SourcePreview.tsx @@ -6,7 +6,7 @@ import { selectTransferSourceParsedTokenAccount, } from "../../store/selectors"; import { CHAINS_BY_ID } from "../../utils/consts"; -import { shortenAddress } from "../../utils/solana"; +import SmartAddress from "../SmartAddress"; import TokenBlacklistWarning from "./TokenBlacklistWarning"; const useStyles = makeStyles((theme) => ({ @@ -23,22 +23,19 @@ export default function SourcePreview() { ); const sourceAmount = useSelector(selectTransferAmount); - const plural = parseInt(sourceAmount) !== 1; - - const tokenExplainer = !sourceParsedTokenAccount - ? "" - : sourceParsedTokenAccount.isNativeAsset - ? sourceParsedTokenAccount.symbol - : `token${plural ? "s" : ""} of ${ - sourceParsedTokenAccount.symbol || - shortenAddress(sourceParsedTokenAccount.mintKey) - }`; - - const explainerString = sourceParsedTokenAccount - ? `You will transfer ${sourceAmount} ${tokenExplainer}, from ${shortenAddress( - sourceParsedTokenAccount?.publicKey - )} on ${CHAINS_BY_ID[sourceChain].name}` - : ""; + const explainerContent = + sourceChain && sourceParsedTokenAccount ? ( + <> + You will transfer {sourceAmount} + + from {CHAINS_BY_ID[sourceChain].name} + + ) : ( + "" + ); return ( <> @@ -47,7 +44,7 @@ export default function SourcePreview() { variant="subtitle2" className={classes.description} > - {explainerString} + {explainerContent} ({ description: { @@ -20,11 +20,16 @@ export default function TargetPreview() { const targetAddress = useSelector(selectTransferTargetAddressHex); const targetAddressNative = hexToNativeString(targetAddress, targetChain); - const explainerString = targetAddressNative - ? `to ${shortenAddress(targetAddressNative)} on ${ - CHAINS_BY_ID[targetChain].name - }` - : "Step complete."; + const explainerContent = + targetChain && targetAddressNative ? ( + <> + to + + on {CHAINS_BY_ID[targetChain].name} + + ) : ( + "" + ); return ( - {explainerString} + {explainerContent} ); } diff --git a/bridge_ui/src/utils/consts.ts b/bridge_ui/src/utils/consts.ts index 4dc5cc9c7..ac3e811a3 100644 --- a/bridge_ui/src/utils/consts.ts +++ b/bridge_ui/src/utils/consts.ts @@ -317,7 +317,7 @@ export const MIGRATION_ASSET_MAP = new Map( : [ // [ // "2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ", - // "ApgUoB1467PXXofoLWFELH2Kz9DKB8WXdU2szGSsFKhX", + // "GcdupcwxkmVGM6s9F8bHSjNoznXAb3hRJTioABNYkn31", // ], ] );