bridge_ui: smart addresses

Change-Id: I3039c7988f7571df4785df218f93ac54dd5ef427
This commit is contained in:
Chase Moran 2021-09-19 04:11:54 -04:00
parent 01d84dc7ba
commit 6ab6c95fdf
8 changed files with 284 additions and 76 deletions

View File

@ -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 ? (
<>
<span>You will attest</span>
<SmartAddress chainId={sourceChain} address={sourceAsset} />
<span>on {CHAINS_BY_ID[sourceChain].name}</span>
</>
) : (
""
);
return (
<Typography
@ -30,7 +35,7 @@ export default function SourcePreview() {
variant="subtitle2"
className={classes.description}
>
{explainerString}
{explainerContent}
</Typography>
);
}

View File

@ -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 = (
<SmartAddress
chainId={CHAIN_ID_SOLANA}
address={toMint}
symbol={toMetadata?.symbol}
tokenName={toMetadata?.name}
/>
);
const fromMintPretty = (
<SmartAddress
chainId={CHAIN_ID_SOLANA}
address={fromMint}
symbol={fromMetadata?.symbol}
tokenName={fromMetadata?.name}
/>
);
return (
<Container maxWidth="md">
@ -407,28 +414,40 @@ export default function Workflow({
{fromTokenAccount && toTokenAccount && fromTokenAccountBalance ? (
<>
<Typography variant="body2">
This will migrate {fromMintPrettyString} tokens in this account:
<span>This will migrate</span>
{fromMintPretty}
<span>tokens in this account:</span>
</Typography>
<Typography variant="h5">
{shortenAddress(fromTokenAccount) +
` (Balance: ${fromTokenAccountBalance}${
fromMetadata.symbol && " " + fromMetadata.symbol
})`}
<SmartAddress
address={fromTokenAccount}
chainId={CHAIN_ID_SOLANA}
/>
{`(Balance: ${fromTokenAccountBalance}${
fromMetadata.symbol && " " + fromMetadata.symbol
})`}
</Typography>
<div className={classes.spacer} />
<Typography variant="body2">
into {toMintPrettyString} tokens in this account:
<span>into </span>
{toMintPretty}
<span> tokens in this account:</span>
</Typography>
<Typography
variant="h5"
color={toTokenAccountExists ? "textPrimary" : "textSecondary"}
>
{shortenAddress(toTokenAccount) +
(toTokenAccountExists
<SmartAddress
address={toTokenAccount}
chainId={CHAIN_ID_SOLANA}
/>
<span>
{toTokenAccountExists
? ` (Balance: ${toTokenAccountBalance}${
(toMetadata.symbol && " " + toMetadata.symbol) || ""
})`
: " (Not created yet)")}
: " (Not created yet)"}
</span>
</Typography>
<SolanaCreateAssociatedAddress
mintAddress={toMint}
@ -440,14 +459,21 @@ export default function Workflow({
<>
<div className={classes.spacer} />
<Typography variant="body2">
Using pool {shortenAddress(poolAddress)} holding tokens in
this account:
<span>Using pool </span>
<SmartAddress
address={poolAddress}
chainId={CHAIN_ID_SOLANA}
/>
<span> holding tokens in this account:</span>
</Typography>
<Typography variant="h5">
{shortenAddress(toCustodyAddress) +
` (Balance: ${toCustodyBalance}${
toMetadata.symbol && " " + toMetadata.symbol
})`}
<SmartAddress
address={toCustodyAddress}
chainId={CHAIN_ID_SOLANA}
/>
<span>{` (Balance: ${toCustodyBalance}${
toMetadata.symbol && " " + toMetadata.symbol
})`}</span>
</Typography>
</>
) : null}

View File

@ -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 ? (
<>
<span>You will transfer 1 NFT of</span>
<SmartAddress
chainId={sourceChain}
parsedTokenAccount={sourceParsedTokenAccount}
/>
<span>from</span>
<SmartAddress
chainId={sourceChain}
address={sourceParsedTokenAccount?.publicKey}
/>
<span>on {CHAINS_BY_ID[sourceChain].name}</span>
</>
) : (
""
);
return (
<>
@ -36,7 +47,7 @@ export default function SourcePreview() {
variant="subtitle2"
className={classes.description}
>
{explainerString}
{explainerContent}
</Typography>
{sourceParsedTokenAccount ? (
<NFTViewer value={sourceParsedTokenAccount} />

View File

@ -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 ? (
<>
<span>to</span>
<SmartAddress chainId={targetChain} address={targetAddressNative} />
<span>on {CHAINS_BY_ID[targetChain].name}</span>
</>
) : (
""
);
return (
<Typography
@ -32,7 +37,7 @@ export default function TargetPreview() {
variant="subtitle2"
className={classes.description}
>
{explainerString}
{explainerContent}
</Typography>
);
}

View File

@ -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 : (
<Button
size="small"
variant="outlined"
endIcon={<OpenInNew />}
className={classes.buttons}
href={explorerAddress}
target="_blank"
>
{"View on " + explorerName}
</Button>
);
//TODO add icon here
const copyButton = isNative ? null : (
<Button
size="small"
variant="outlined"
endIcon={<FileCopy />}
onClick={copyToClipboard}
className={classes.buttons}
>
Copy
</Button>
);
const tooltipContent = (
<>
{useableName && <Typography>{useableName}</Typography>}
{useableSymbol && !isNative && (
<Typography noWrap variant="body2">
{addressShort}
</Typography>
)}
<div>
{explorerButton}
{copyButton}
</div>
</>
);
return (
<StyledTooltip
title={tooltipContent}
interactive={true}
className={classes.mainTypog}
>
<Typography
variant={variant || "body1"}
className={classes.mainTypog}
component="div"
>
{useableSymbol || addressShort}
</Typography>
</StyledTooltip>
);
}

View File

@ -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 ? (
<>
<span>You will transfer {sourceAmount}</span>
<SmartAddress
chainId={sourceChain}
parsedTokenAccount={sourceParsedTokenAccount}
/>
<span>from {CHAINS_BY_ID[sourceChain].name}</span>
</>
) : (
""
);
return (
<>
@ -47,7 +44,7 @@ export default function SourcePreview() {
variant="subtitle2"
className={classes.description}
>
{explainerString}
{explainerContent}
</Typography>
<TokenBlacklistWarning
sourceChain={sourceChain}

View File

@ -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(selectTransferTargetAddressHex);
const targetAddressNative = hexToNativeString(targetAddress, targetChain);
const explainerString = targetAddressNative
? `to ${shortenAddress(targetAddressNative)} on ${
CHAINS_BY_ID[targetChain].name
}`
: "Step complete.";
const explainerContent =
targetChain && targetAddressNative ? (
<>
<span>to</span>
<SmartAddress chainId={targetChain} address={targetAddressNative} />
<span>on {CHAINS_BY_ID[targetChain].name}</span>
</>
) : (
""
);
return (
<Typography
@ -32,7 +37,7 @@ export default function TargetPreview() {
variant="subtitle2"
className={classes.description}
>
{explainerString}
{explainerContent}
</Typography>
);
}

View File

@ -317,7 +317,7 @@ export const MIGRATION_ASSET_MAP = new Map<string, string>(
: [
// [
// "2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ",
// "ApgUoB1467PXXofoLWFELH2Kz9DKB8WXdU2szGSsFKhX",
// "GcdupcwxkmVGM6s9F8bHSjNoznXAb3hRJTioABNYkn31",
// ],
]
);