bridge_ui: updated warnings & confirm dialog

Change-Id: I6b5e471ddf2d4188932b72647f92fcf4adfa7555
This commit is contained in:
Chase Moran 2021-11-04 20:32:14 -04:00 committed by Evan Gray
parent c824a99636
commit 4b510d8aa1
5 changed files with 269 additions and 142 deletions

View File

@ -1,3 +1,4 @@
import { isEVMChain } from "@certusone/wormhole-sdk";
import { import {
Button, Button,
Dialog, Dialog,
@ -7,26 +8,87 @@ import {
Typography, Typography,
} from "@material-ui/core"; } from "@material-ui/core";
import { ArrowDownward } from "@material-ui/icons"; import { ArrowDownward } from "@material-ui/icons";
import { Alert } from "@material-ui/lab"; import { useEffect, useMemo, useState } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { import {
selectTransferOriginChain,
selectTransferSourceChain, selectTransferSourceChain,
selectTransferSourceParsedTokenAccount, selectTransferSourceParsedTokenAccount,
} from "../../store/selectors"; } from "../../store/selectors";
import { CHAINS_BY_ID } from "../../utils/consts"; import { CHAINS_BY_ID, MULTI_CHAIN_TOKENS } from "../../utils/consts";
import SmartAddress from "../SmartAddress"; import SmartAddress from "../SmartAddress";
import { useTargetInfo } from "./Target"; import { useTargetInfo } from "./Target";
import TokenWarning from "./TokenWarning";
function SendConfirmationContent() { function SendConfirmationContent({
open,
onClose,
onClick,
}: {
open: boolean;
onClose: () => void;
onClick: () => void;
}) {
const sourceChain = useSelector(selectTransferSourceChain); const sourceChain = useSelector(selectTransferSourceChain);
const sourceParsedTokenAccount = useSelector( const sourceParsedTokenAccount = useSelector(
selectTransferSourceParsedTokenAccount selectTransferSourceParsedTokenAccount
); );
const { targetChain, targetAsset, symbol, tokenName, logo } = useTargetInfo(); const { targetChain, targetAsset, symbol, tokenName, logo } = useTargetInfo();
const originChain = useSelector(selectTransferOriginChain);
//TODO this check is essentially duplicated.
const deservesTimeout = useMemo(() => {
if (originChain && sourceParsedTokenAccount?.mintKey) {
const searchableAddress = isEVMChain(originChain)
? sourceParsedTokenAccount.mintKey.toLowerCase()
: sourceParsedTokenAccount.mintKey;
return ( return (
originChain !== targetChain &&
!!MULTI_CHAIN_TOKENS[sourceChain]?.[searchableAddress]
);
} else {
return false;
}
}, [originChain, targetChain, sourceChain, sourceParsedTokenAccount]);
const timeoutDuration = 5;
const [countdown, setCountdown] = useState(
deservesTimeout ? timeoutDuration : 0
);
useEffect(() => {
if (!deservesTimeout || countdown === 0) {
return;
}
let cancelled = false;
setInterval(() => {
if (!cancelled) {
setCountdown((state) => state - 1);
}
}, 1000);
return () => {
cancelled = true;
};
}, [deservesTimeout, countdown]);
useEffect(() => {
if (open && deservesTimeout) {
//Countdown starts on mount, but we actually want it to start on open
setCountdown(timeoutDuration);
}
}, [open, deservesTimeout]);
const sendConfirmationContent = (
<> <>
<DialogTitle>Are you sure?</DialogTitle>
<DialogContent>
{targetAsset ? ( {targetAsset ? (
<div style={{ textAlign: "center" }}> <div style={{ textAlign: "center", marginBottom: 16 }}>
<Typography variant="subtitle1" style={{ marginBottom: 8 }}>
You are about to perform this transfer:
</Typography>
<SmartAddress <SmartAddress
variant="h6" variant="h6"
chainId={sourceChain} chainId={sourceChain}
@ -55,14 +117,33 @@ function SendConfirmationContent() {
</div> </div>
</div> </div>
) : null} ) : null}
<Alert severity="warning" variant="outlined" style={{ marginTop: 8 }}> <TokenWarning
Once the transfer transaction is submitted, the transfer must be sourceAsset={sourceParsedTokenAccount?.mintKey}
completed by redeeming the tokens on the target chain. Please ensure sourceChain={sourceChain}
that the token listed above is the desired token and confirm that originChain={originChain}
markets exist on the target chain. targetAsset={targetAsset ?? undefined}
</Alert> targetChain={targetChain}
symbol={symbol}
/>
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={onClose}>
Cancel
</Button>
<Button
variant="contained"
color="primary"
onClick={onClick}
size={"medium"}
disabled={!!countdown}
>
{!!countdown ? countdown.toString() : "Confirm"}
</Button>
</DialogActions>
</> </>
); );
return sendConfirmationContent;
} }
export default function SendConfirmationDialog({ export default function SendConfirmationDialog({
@ -76,18 +157,11 @@ export default function SendConfirmationDialog({
}) { }) {
return ( return (
<Dialog open={open} onClose={onClose}> <Dialog open={open} onClose={onClose}>
<DialogTitle>Are you sure?</DialogTitle> <SendConfirmationContent
<DialogContent> open={open}
<SendConfirmationContent /> onClose={onClose}
</DialogContent> onClick={onClick}
<DialogActions> />
<Button variant="outlined" onClick={onClose}>
Cancel
</Button>
<Button variant="contained" color="primary" onClick={onClick}>
Confirm
</Button>
</DialogActions>
</Dialog> </Dialog>
); );
} }

View File

@ -36,7 +36,6 @@ import LowBalanceWarning from "../LowBalanceWarning";
import NumberTextField from "../NumberTextField"; import NumberTextField from "../NumberTextField";
import StepDescription from "../StepDescription"; import StepDescription from "../StepDescription";
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector"; import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
import TokenWarning from "./TokenWarning";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
transferField: { transferField: {
@ -135,11 +134,6 @@ function Source() {
</Button> </Button>
) : ( ) : (
<> <>
<TokenWarning
sourceChain={sourceChain}
tokenAddress={parsedTokenAccount?.mintKey}
symbol={parsedTokenAccount?.symbol}
/>
<LowBalanceWarning chainId={sourceChain} /> <LowBalanceWarning chainId={sourceChain} />
{hasParsedTokenAccount ? ( {hasParsedTokenAccount ? (
<NumberTextField <NumberTextField

View File

@ -8,7 +8,6 @@ import {
} from "../../store/selectors"; } from "../../store/selectors";
import { CHAINS_BY_ID } from "../../utils/consts"; import { CHAINS_BY_ID } from "../../utils/consts";
import SmartAddress from "../SmartAddress"; import SmartAddress from "../SmartAddress";
import TokenWarning from "./TokenWarning";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
description: { description: {
@ -54,11 +53,6 @@ export default function SourcePreview() {
> >
{explainerContent} {explainerContent}
</Typography> </Typography>
<TokenWarning
sourceChain={sourceChain}
tokenAddress={sourceParsedTokenAccount?.mintKey}
symbol={sourceParsedTokenAccount?.symbol}
/>
</> </>
); );
} }

View File

@ -1,19 +1,10 @@
import { import { ChainId, CHAIN_ID_ETH, isEVMChain } from "@certusone/wormhole-sdk";
ChainId, import { Box, Link, makeStyles, Typography } from "@material-ui/core";
CHAIN_ID_BSC,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
WSOL_ADDRESS,
} from "@certusone/wormhole-sdk";
import { getAddress } from "@ethersproject/address";
import { makeStyles } from "@material-ui/core";
import { Alert } from "@material-ui/lab"; import { Alert } from "@material-ui/lab";
import { useMemo } from "react";
import { import {
BSC_MARKET_WARNINGS, AVAILABLE_MARKETS_URL,
ETH_TOKENS_THAT_CAN_BE_SWAPPED_ON_SOLANA, CHAINS_BY_ID,
ETH_TOKENS_THAT_EXIST_ELSEWHERE, MULTI_CHAIN_TOKENS,
SOLANA_TOKENS_THAT_EXIST_ELSEWHERE,
} from "../../utils/consts"; } from "../../utils/consts";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
@ -21,81 +12,122 @@ const useStyles = makeStyles((theme) => ({
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
}, },
alert: {
textAlign: "center",
},
line: {
marginBottom: theme.spacing(2),
},
})); }));
export default function TokenWarning({ function WormholeWrappedWarning() {
sourceChain, const classes = useStyles();
tokenAddress, return (
<Alert severity="info" variant="outlined" className={classes.alert}>
<Typography component="div" className={classes.line}>
The tokens you will receive are{" "}
<Box fontWeight={900} display="inline">
Wormhole Wrapped Tokens
</Box>{" "}
and will need to be exchanged for native assets.
</Typography>
<Typography component="div">
<Link
href={AVAILABLE_MARKETS_URL}
target="_blank"
rel="noopener noreferrer"
>
Click here to see available markets for wrapped tokens.
</Link>
</Typography>
</Alert>
);
}
function MultichainWarning({
symbol, symbol,
targetChain,
}: { }: {
sourceChain: ChainId; symbol: string;
tokenAddress: string | undefined; targetChain: ChainId;
symbol: string | undefined;
}) { }) {
const classes = useStyles(); const classes = useStyles();
const tokenConflictingNativeWarning = useMemo( return (
() => <Alert severity="warning" variant="outlined" className={classes.alert}>
tokenAddress && <Typography
((sourceChain === CHAIN_ID_SOLANA && variant="h6"
SOLANA_TOKENS_THAT_EXIST_ELSEWHERE.includes(tokenAddress)) || className={classes.line}
(sourceChain === CHAIN_ID_ETH && >{`You will not receive native ${symbol} on ${CHAINS_BY_ID[targetChain].name}`}</Typography>
ETH_TOKENS_THAT_EXIST_ELSEWHERE.includes(getAddress(tokenAddress)))) <Typography
? `Bridging ${ className={classes.line}
symbol ? symbol : "the token" >{`To receive native ${symbol}, you will have to perform a swap with the wrapped tokens once you are done bridging.`}</Typography>
} via Wormhole will not produce native ${ <Typography>
symbol ? symbol : "assets" <Link
}. It will produce a wrapped version which might have no liquidity or utility on the target chain.` href={AVAILABLE_MARKETS_URL}
: undefined, target="_blank"
[sourceChain, tokenAddress, symbol] rel="noopener noreferrer"
>
Click here to see available markets for wrapped tokens.
</Link>
</Typography>
</Alert>
); );
const marketsWarning = useMemo(() => {
let show = false;
if (sourceChain === CHAIN_ID_SOLANA && tokenAddress === WSOL_ADDRESS) {
show = true;
} else if (
sourceChain === CHAIN_ID_BSC &&
tokenAddress &&
BSC_MARKET_WARNINGS.includes(getAddress(tokenAddress))
) {
show = true;
} }
if (show) {
return `As of 10/13/2021, markets have not been established for ${
symbol ? "Wormhole-wrapped " + symbol : "this token"
}. Please verify this token will be useful on the target chain.`;
} else {
return null;
}
}, [sourceChain, tokenAddress, symbol]);
const content = tokenConflictingNativeWarning ? ( function RewardsWarning() {
<Alert severity="warning" variant="outlined"> const classes = useStyles();
{tokenConflictingNativeWarning} return (
</Alert> <Alert severity="warning" variant="outlined" className={classes.alert}>
) : marketsWarning ? (
<Alert severity="warning" variant="outlined">
{marketsWarning}
</Alert>
) : sourceChain === CHAIN_ID_ETH &&
tokenAddress &&
getAddress(tokenAddress) ===
getAddress("0xae7ab96520de3a18e5e111b5eaab095312d7fe84") ? ( // stETH (Lido)
<Alert severity="warning" variant="outlined">
Lido stETH rewards can only be received on Ethereum. Use the value Lido stETH rewards can only be received on Ethereum. Use the value
accruing wrapper token wstETH instead. accruing wrapper token wstETH instead.
</Alert> </Alert>
) : sourceChain === CHAIN_ID_ETH && );
tokenAddress && }
ETH_TOKENS_THAT_CAN_BE_SWAPPED_ON_SOLANA.includes(
getAddress(tokenAddress) export default function TokenWarning({
) ? ( sourceChain,
//TODO: will this be accurate with Terra support? sourceAsset,
<Alert severity="info" variant="outlined"> originChain,
Bridging {symbol ? symbol : "the token"} via Wormhole will not produce targetChain,
native {symbol ? symbol : "assets"}. It will produce a wrapped version targetAsset,
which can be swapped using a stable swap protocol. symbol,
</Alert> }: {
) : null; sourceChain?: ChainId;
sourceAsset?: string;
return content ? <div className={classes.container}>{content}</div> : null; originChain?: ChainId;
targetChain?: ChainId;
targetAsset?: string;
symbol?: string;
}) {
if (
!(originChain && targetChain && targetAsset && sourceChain && sourceAsset)
) {
return null;
}
const searchableAddress = isEVMChain(sourceChain)
? sourceAsset.toLowerCase()
: sourceAsset;
const isWormholeWrapped = originChain !== targetChain;
const isMultiChain = !!MULTI_CHAIN_TOKENS[sourceChain]?.[searchableAddress];
const isRewardsToken =
searchableAddress === "0xae7ab96520de3a18e5e111b5eaab095312d7fe84" &&
sourceChain === CHAIN_ID_ETH;
const showMultiChainWarning = isMultiChain && isWormholeWrapped;
const showWrappedWarning = !isMultiChain && isWormholeWrapped; //Multichain warning is more important
const showRewardsWarning = isRewardsToken;
return (
<>
{showMultiChainWarning ? (
<MultichainWarning
symbol={symbol || "tokens"}
targetChain={targetChain}
/>
) : null}
{showWrappedWarning ? <WormholeWrappedWarning /> : null}
{showRewardsWarning ? <RewardsWarning /> : null}
</>
);
} }

View File

@ -629,3 +629,36 @@ export const VAA_EMITTER_ADDRESSES = [
]; ];
export const WORMHOLE_EXPLORER_BASE = "https://wormholenetwork.com/en/explorer"; export const WORMHOLE_EXPLORER_BASE = "https://wormholenetwork.com/en/explorer";
export const MULTI_CHAIN_TOKENS: {
[x: number]: { [address: string]: string };
} =
//EVM chains should format the addresses to all lowercase
CLUSTER === "mainnet"
? {
[CHAIN_ID_SOLANA]: {
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: "USDC",
Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: "USDT",
},
[CHAIN_ID_ETH]: {
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": "USDC",
"0xdac17f958d2ee523a2206206994597c13d831ec7": "USDT",
},
[CHAIN_ID_TERRA]: {},
[CHAIN_ID_BSC]: {
"0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d": "USDC",
"0x55d398326f99059ff775485246999027b3197955": "USDT",
},
[CHAIN_ID_POLYGON]: {
"0x2791bca1f2de4661ed88a30c99a7a9449aa84174": "USDC",
"0xc2132d05d31c914a87c6611c10748aeb04b58e8f": "USDT",
},
}
: {
[CHAIN_ID_SOLANA]: {
"2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ": "SOLT",
},
};
export const AVAILABLE_MARKETS_URL =
"https://docs.wormholenetwork.com/wormhole/overview-liquid-markets";