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 {
Button,
Dialog,
@ -7,62 +8,142 @@ import {
Typography,
} from "@material-ui/core";
import { ArrowDownward } from "@material-ui/icons";
import { Alert } from "@material-ui/lab";
import { useEffect, useMemo, useState } from "react";
import { useSelector } from "react-redux";
import {
selectTransferOriginChain,
selectTransferSourceChain,
selectTransferSourceParsedTokenAccount,
} 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 { 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 sourceParsedTokenAccount = useSelector(
selectTransferSourceParsedTokenAccount
);
const { targetChain, targetAsset, symbol, tokenName, logo } = useTargetInfo();
return (
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 (
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 = (
<>
{targetAsset ? (
<div style={{ textAlign: "center" }}>
<SmartAddress
variant="h6"
chainId={sourceChain}
parsedTokenAccount={sourceParsedTokenAccount}
/>
<div>
<Typography variant="caption">
{CHAINS_BY_ID[sourceChain].name}
<DialogTitle>Are you sure?</DialogTitle>
<DialogContent>
{targetAsset ? (
<div style={{ textAlign: "center", marginBottom: 16 }}>
<Typography variant="subtitle1" style={{ marginBottom: 8 }}>
You are about to perform this transfer:
</Typography>
<SmartAddress
variant="h6"
chainId={sourceChain}
parsedTokenAccount={sourceParsedTokenAccount}
/>
<div>
<Typography variant="caption">
{CHAINS_BY_ID[sourceChain].name}
</Typography>
</div>
<div style={{ paddingTop: 4 }}>
<ArrowDownward fontSize="inherit" />
</div>
<SmartAddress
variant="h6"
chainId={targetChain}
address={targetAsset}
symbol={symbol}
tokenName={tokenName}
logo={logo}
/>
<div>
<Typography variant="caption">
{CHAINS_BY_ID[targetChain].name}
</Typography>
</div>
</div>
<div style={{ paddingTop: 4 }}>
<ArrowDownward fontSize="inherit" />
</div>
<SmartAddress
variant="h6"
chainId={targetChain}
address={targetAsset}
symbol={symbol}
tokenName={tokenName}
logo={logo}
/>
<div>
<Typography variant="caption">
{CHAINS_BY_ID[targetChain].name}
</Typography>
</div>
</div>
) : null}
<Alert severity="warning" variant="outlined" style={{ marginTop: 8 }}>
Once the transfer transaction is submitted, the transfer must be
completed by redeeming the tokens on the target chain. Please ensure
that the token listed above is the desired token and confirm that
markets exist on the target chain.
</Alert>
) : null}
<TokenWarning
sourceAsset={sourceParsedTokenAccount?.mintKey}
sourceChain={sourceChain}
originChain={originChain}
targetAsset={targetAsset ?? undefined}
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({
@ -76,18 +157,11 @@ export default function SendConfirmationDialog({
}) {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Are you sure?</DialogTitle>
<DialogContent>
<SendConfirmationContent />
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={onClose}>
Cancel
</Button>
<Button variant="contained" color="primary" onClick={onClick}>
Confirm
</Button>
</DialogActions>
<SendConfirmationContent
open={open}
onClose={onClose}
onClick={onClick}
/>
</Dialog>
);
}

View File

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

View File

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

View File

@ -1,19 +1,10 @@
import {
ChainId,
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 { ChainId, CHAIN_ID_ETH, isEVMChain } from "@certusone/wormhole-sdk";
import { Box, Link, makeStyles, Typography } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useMemo } from "react";
import {
BSC_MARKET_WARNINGS,
ETH_TOKENS_THAT_CAN_BE_SWAPPED_ON_SOLANA,
ETH_TOKENS_THAT_EXIST_ELSEWHERE,
SOLANA_TOKENS_THAT_EXIST_ELSEWHERE,
AVAILABLE_MARKETS_URL,
CHAINS_BY_ID,
MULTI_CHAIN_TOKENS,
} from "../../utils/consts";
const useStyles = makeStyles((theme) => ({
@ -21,81 +12,122 @@ const useStyles = makeStyles((theme) => ({
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
alert: {
textAlign: "center",
},
line: {
marginBottom: theme.spacing(2),
},
}));
export default function TokenWarning({
sourceChain,
tokenAddress,
function WormholeWrappedWarning() {
const classes = useStyles();
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,
targetChain,
}: {
sourceChain: ChainId;
tokenAddress: string | undefined;
symbol: string | undefined;
symbol: string;
targetChain: ChainId;
}) {
const classes = useStyles();
const tokenConflictingNativeWarning = useMemo(
() =>
tokenAddress &&
((sourceChain === CHAIN_ID_SOLANA &&
SOLANA_TOKENS_THAT_EXIST_ELSEWHERE.includes(tokenAddress)) ||
(sourceChain === CHAIN_ID_ETH &&
ETH_TOKENS_THAT_EXIST_ELSEWHERE.includes(getAddress(tokenAddress))))
? `Bridging ${
symbol ? symbol : "the token"
} via Wormhole will not produce native ${
symbol ? symbol : "assets"
}. It will produce a wrapped version which might have no liquidity or utility on the target chain.`
: undefined,
[sourceChain, tokenAddress, symbol]
return (
<Alert severity="warning" variant="outlined" className={classes.alert}>
<Typography
variant="h6"
className={classes.line}
>{`You will not receive native ${symbol} on ${CHAINS_BY_ID[targetChain].name}`}</Typography>
<Typography
className={classes.line}
>{`To receive native ${symbol}, you will have to perform a swap with the wrapped tokens once you are done bridging.`}</Typography>
<Typography>
<Link
href={AVAILABLE_MARKETS_URL}
target="_blank"
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 ? (
<Alert severity="warning" variant="outlined">
{tokenConflictingNativeWarning}
</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">
function RewardsWarning() {
const classes = useStyles();
return (
<Alert severity="warning" variant="outlined" className={classes.alert}>
Lido stETH rewards can only be received on Ethereum. Use the value
accruing wrapper token wstETH instead.
</Alert>
) : sourceChain === CHAIN_ID_ETH &&
tokenAddress &&
ETH_TOKENS_THAT_CAN_BE_SWAPPED_ON_SOLANA.includes(
getAddress(tokenAddress)
) ? (
//TODO: will this be accurate with Terra support?
<Alert severity="info" variant="outlined">
Bridging {symbol ? symbol : "the token"} via Wormhole will not produce
native {symbol ? symbol : "assets"}. It will produce a wrapped version
which can be swapped using a stable swap protocol.
</Alert>
) : null;
return content ? <div className={classes.container}>{content}</div> : null;
);
}
export default function TokenWarning({
sourceChain,
sourceAsset,
originChain,
targetChain,
targetAsset,
symbol,
}: {
sourceChain?: ChainId;
sourceAsset?: string;
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 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";