diff --git a/bridge_ui/src/components/Transfer/SendConfirmationDialog.tsx b/bridge_ui/src/components/Transfer/SendConfirmationDialog.tsx index a8870e0d..cedcf9c7 100644 --- a/bridge_ui/src/components/Transfer/SendConfirmationDialog.tsx +++ b/bridge_ui/src/components/Transfer/SendConfirmationDialog.tsx @@ -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 ? ( -
- -
- - {CHAINS_BY_ID[sourceChain].name} + Are you sure? + + {targetAsset ? ( +
+ + You are about to perform this transfer: + +
+ + {CHAINS_BY_ID[sourceChain].name} + +
+
+ +
+ +
+ + {CHAINS_BY_ID[targetChain].name} + +
-
- -
- -
- - {CHAINS_BY_ID[targetChain].name} - -
-
- ) : null} - - 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. - + ) : null} + + + + + + ); + + return sendConfirmationContent; } export default function SendConfirmationDialog({ @@ -76,18 +157,11 @@ export default function SendConfirmationDialog({ }) { return ( - Are you sure? - - - - - - - + ); } diff --git a/bridge_ui/src/components/Transfer/Source.tsx b/bridge_ui/src/components/Transfer/Source.tsx index 40be6913..7cccecb7 100644 --- a/bridge_ui/src/components/Transfer/Source.tsx +++ b/bridge_ui/src/components/Transfer/Source.tsx @@ -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() { ) : ( <> - {hasParsedTokenAccount ? ( ({ description: { @@ -54,11 +53,6 @@ export default function SourcePreview() { > {explainerContent} - ); } diff --git a/bridge_ui/src/components/Transfer/TokenWarning.tsx b/bridge_ui/src/components/Transfer/TokenWarning.tsx index 3f9f6082..5453512e 100644 --- a/bridge_ui/src/components/Transfer/TokenWarning.tsx +++ b/bridge_ui/src/components/Transfer/TokenWarning.tsx @@ -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 ( + + + The tokens you will receive are{" "} + + Wormhole Wrapped Tokens + {" "} + and will need to be exchanged for native assets. + + + + Click here to see available markets for wrapped tokens. + + + + ); +} + +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 ( + + {`You will not receive native ${symbol} on ${CHAINS_BY_ID[targetChain].name}`} + {`To receive native ${symbol}, you will have to perform a swap with the wrapped tokens once you are done bridging.`} + + + Click here to see available markets for wrapped tokens. + + + ); - 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 ? ( - - {tokenConflictingNativeWarning} - - ) : marketsWarning ? ( - - {marketsWarning} - - ) : sourceChain === CHAIN_ID_ETH && - tokenAddress && - getAddress(tokenAddress) === - getAddress("0xae7ab96520de3a18e5e111b5eaab095312d7fe84") ? ( // stETH (Lido) - +function RewardsWarning() { + const classes = useStyles(); + return ( + Lido stETH rewards can only be received on Ethereum. Use the value accruing wrapper token wstETH instead. - ) : sourceChain === CHAIN_ID_ETH && - tokenAddress && - ETH_TOKENS_THAT_CAN_BE_SWAPPED_ON_SOLANA.includes( - getAddress(tokenAddress) - ) ? ( - //TODO: will this be accurate with Terra support? - - 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. - - ) : null; - - return content ?
{content}
: 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 ? ( + + ) : null} + {showWrappedWarning ? : null} + {showRewardsWarning ? : null} + + ); } diff --git a/bridge_ui/src/utils/consts.ts b/bridge_ui/src/utils/consts.ts index 7d6ad57f..bbe4787b 100644 --- a/bridge_ui/src/utils/consts.ts +++ b/bridge_ui/src/utils/consts.ts @@ -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";