diff --git a/src/components/ChainWarningMessage.tsx b/src/components/ChainWarningMessage.tsx index 5700347e6..ff622c963 100644 --- a/src/components/ChainWarningMessage.tsx +++ b/src/components/ChainWarningMessage.tsx @@ -1,8 +1,6 @@ -import { ChainId } from "@certusone/wormhole-sdk"; import { Link, makeStyles, Typography } from "@material-ui/core"; import { Alert } from "@material-ui/lab"; -import { useMemo } from "react"; -import { CHAIN_CONFIG_MAP } from "../config"; +import { WarningMessage } from "../hooks/useWarningRulesEngine"; const useStyles = makeStyles((theme) => ({ alert: { @@ -11,27 +9,22 @@ const useStyles = makeStyles((theme) => ({ }, })); -export default function ChainWarningMessage({ chainId }: { chainId: ChainId }) { - const classes = useStyles(); - - const warningMessage = useMemo(() => { - return CHAIN_CONFIG_MAP[chainId]?.warningMessage; - }, [chainId]); - - if (warningMessage === undefined) { - return null; - } +export interface ChainWarningProps { + message: WarningMessage; +} +export default function ChainWarningMessage({ message }: ChainWarningProps) { + const classes = useStyles(); return ( - {warningMessage.text} - {warningMessage.link ? ( + {message.text} + {message.link && ( - - {warningMessage.link.text} + + {message.link.text} - ) : null} + )} ); } diff --git a/src/components/NFT/Source.tsx b/src/components/NFT/Source.tsx index 85eccb260..2259596f5 100644 --- a/src/components/NFT/Source.tsx +++ b/src/components/NFT/Source.tsx @@ -2,7 +2,7 @@ import { CHAIN_ID_SOLANA, isEVMChain } from "@certusone/wormhole-sdk"; import { Button, makeStyles } from "@material-ui/core"; import { VerifiedUser } from "@material-ui/icons"; import { Alert } from "@material-ui/lab"; -import { useCallback, useMemo } from "react"; +import { useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { Link } from "react-router-dom"; import useIsWalletReady from "../../hooks/useIsWalletReady"; @@ -13,12 +13,9 @@ import { selectNFTSourceBalanceString, selectNFTSourceChain, selectNFTSourceError, + selectNFTTargetChain, } from "../../store/selectors"; -import { - CHAINS_WITH_NFT_SUPPORT, - CLUSTER, - getIsTransferDisabled, -} from "../../utils/consts"; +import { CHAINS_WITH_NFT_SUPPORT, CLUSTER } from "../../utils/consts"; import ButtonWithLoader from "../ButtonWithLoader"; import ChainSelect from "../ChainSelect"; import KeyAndBalance from "../KeyAndBalance"; @@ -27,6 +24,8 @@ import SolanaTPSWarning from "../SolanaTPSWarning"; import StepDescription from "../StepDescription"; import { TokenSelector } from "../TokenSelectors/SourceTokenSelector"; import ChainWarningMessage from "../ChainWarningMessage"; +import transferRules from "../../config/transferRules"; +import useTransferControl from "../../hooks/useTransferControl"; const useStyles = makeStyles((theme) => ({ transferField: { @@ -38,6 +37,7 @@ function Source() { const classes = useStyles(); const dispatch = useDispatch(); const sourceChain = useSelector(selectNFTSourceChain); + const targetChain = useSelector(selectNFTTargetChain); const uiAmountString = useSelector(selectNFTSourceBalanceString); const error = useSelector(selectNFTSourceError); const isSourceComplete = useSelector(selectNFTIsSourceComplete); @@ -52,9 +52,11 @@ function Source() { const handleNextClick = useCallback(() => { dispatch(incrementStep()); }, [dispatch]); - const isTransferDisabled = useMemo(() => { - return getIsTransferDisabled(sourceChain, true); - }, [sourceChain]); + const { isTransferDisabled, warnings } = useTransferControl( + transferRules, + sourceChain, + targetChain + ); return ( <> @@ -103,7 +105,9 @@ function Source() { {sourceChain === CHAIN_ID_SOLANA && CLUSTER === "mainnet" && ( )} - + {warnings.map((message, key) => ( + + ))} ({ transferField: { @@ -92,9 +93,11 @@ function Target() { const handleNextClick = useCallback(() => { dispatch(incrementStep()); }, [dispatch]); - const isTransferDisabled = useMemo(() => { - return getIsTransferDisabled(targetChain, false); - }, [targetChain]); + const { isTransferDisabled, warnings } = useTransferControl( + transferRules, + sourceChain, + targetChain + ); const isValidTargetAssetAddress = targetAsset && targetAsset !== ethers.constants.AddressZero; return ( @@ -152,7 +155,9 @@ function Target() { {targetChain === CHAIN_ID_SOLANA && CLUSTER === "mainnet" && ( )} - + {warnings.map((message, key) => ( + + ))} { - console.log("handle go", selectedRelayer, parsedPayload); if (!(selectedRelayer && selectedRelayer.url)) { return; } @@ -462,7 +459,7 @@ function RelayerRecovery({ }); } ); - }, [selectedRelayer, enqueueSnackbar, onClick, signedVaa, parsedPayload]); + }, [selectedRelayer, enqueueSnackbar, onClick, signedVaa]); if (!isEligible) { return null; @@ -885,7 +882,7 @@ export default function Recovery() { const parsedVAA = parseVaa(hexToUint8Array(recoverySignedVAA)); setRecoveryParsedVAA(parsedVAA); } catch (e) { - console.log(e); + console.error(e); setRecoveryParsedVAA(null); } } diff --git a/src/components/Transfer/Redeem.tsx b/src/components/Transfer/Redeem.tsx index d324a3ab6..2087f79cb 100644 --- a/src/components/Transfer/Redeem.tsx +++ b/src/components/Transfer/Redeem.tsx @@ -66,6 +66,10 @@ import TerraFeeDenomPicker from "../TerraFeeDenomPicker"; import AddToMetamask from "./AddToMetamask"; import RedeemPreview from "./RedeemPreview"; import WaitingForWalletMessage from "./WaitingForWalletMessage"; +import ChainWarningMessage from "../ChainWarningMessage"; +import { useRedeemControl } from "../../hooks/useRedeemControl"; +import transferRules from "../../config/transferRules"; +import { RootState } from "../../store"; const useStyles = makeStyles((theme) => ({ alert: { @@ -179,7 +183,22 @@ function Redeem() { dispatch(reset()); }, [dispatch]); const howToAddTokensUrl = getHowToAddTokensToWalletUrl(targetChain); - + const originAsset = useSelector( + (state: RootState) => state.transfer.originAsset + ); + const originChain = useSelector( + (state: RootState) => state.transfer.originChain + ); + const sourceChain = useSelector( + (state: RootState) => state.transfer.sourceChain + ); + const { warnings, isRedeemDisabled } = useRedeemControl( + transferRules, + sourceChain, + targetChain, + originAsset, + originChain + ); const relayerContent = ( <> {isEVMChain(targetChain) && !isTransferCompleted && !targetIsAcala ? ( @@ -270,12 +289,15 @@ function Redeem() { {targetChain === CHAIN_ID_SOLANA ? ( ) : null} - + {warnings.map((message, key) => ( + + ))} <> {" "} Redeem diff --git a/src/components/Transfer/Source.tsx b/src/components/Transfer/Source.tsx index e0c8a4e3c..bc41e795f 100644 --- a/src/components/Transfer/Source.tsx +++ b/src/components/Transfer/Source.tsx @@ -7,7 +7,7 @@ import { import { getAddress } from "@ethersproject/address"; import { Button, makeStyles, Typography } from "@material-ui/core"; import { VerifiedUser } from "@material-ui/icons"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useHistory } from "react-router"; import { Link } from "react-router-dom"; @@ -34,7 +34,6 @@ import { CHAINS, CLUSTER, ETH_MIGRATION_ASSET_MAP, - getIsTransferDisabled, } from "../../utils/consts"; import ButtonWithLoader from "../ButtonWithLoader"; import ChainSelect from "../ChainSelect"; @@ -50,7 +49,9 @@ import ChainWarningMessage from "../ChainWarningMessage"; import useIsTransferLimited from "../../hooks/useIsTransferLimited"; import TransferLimitedWarning from "./TransferLimitedWarning"; import { RootState } from "../../store"; -import PandleWarning from "../PandleWarning"; +import useTransferControl from "../../hooks/useTransferControl"; +import transferRules from "../../config/transferRules"; +import useRoundTripTransfer from "../../hooks/useRoundTripTransfer"; const useStyles = makeStyles((theme) => ({ chainSelectWrapper: { @@ -86,12 +87,6 @@ function Source() { () => CHAINS.filter((c) => c.id !== sourceChain), [sourceChain] ); - const isSourceTransferDisabled = useMemo(() => { - return getIsTransferDisabled(sourceChain, true); - }, [sourceChain]); - const isTargetTransferDisabled = useMemo(() => { - return getIsTransferDisabled(targetChain, false); - }, [targetChain]); const parsedTokenAccount = useSelector( selectTransferSourceParsedTokenAccount ); @@ -153,37 +148,25 @@ function Source() { dispatch(incrementStep()); }, [dispatch]); - /* Only allow sending from ETH <-> BSC Pandle Token */ - const [isPandle, setIsPandle] = useState(false); const selectedTokenAddress = useSelector( (state: RootState) => state.transfer.sourceParsedTokenAccount?.mintKey ); - useEffect(() => { - const EthereumPandleAddress = - "0x808507121b80c02388fad14726482e061b8da827".toUpperCase(); - const BscPandleAddres = - "0xb3Ed0A426155B79B898849803E3B36552f7ED507".toUpperCase(); - const isFromEthereum = ( - sourceChain: number, - selectedTokenAddress: string | undefined - ) => - sourceChain === CHAIN_ID_ETH && - selectedTokenAddress === EthereumPandleAddress; - const isFromBsc = ( - sourceChain: number, - selectedTokenAddress: string | undefined - ) => - sourceChain === CHAIN_ID_BSC && selectedTokenAddress === BscPandleAddres; - if (isFromEthereum(sourceChain, selectedTokenAddress?.toUpperCase())) { - setIsPandle(true); - handleTargetChange({ target: { value: CHAIN_ID_BSC } }); - } else if (isFromBsc(sourceChain, selectedTokenAddress?.toUpperCase())) { - setIsPandle(true); - handleTargetChange({ target: { value: CHAIN_ID_ETH } }); - } else { - setIsPandle(false); - } - }, [sourceChain, selectedTokenAddress, handleTargetChange]); + const { isTransferDisabled, warnings, ids } = useTransferControl( + transferRules, + sourceChain, + targetChain, + selectedTokenAddress + ); + /* Only allow sending from ETH <-> BSC Pandle Token */ + const isPandle = (id: string) => id === "pandle"; + const isRoundTripTransfer = useRoundTripTransfer( + CHAIN_ID_ETH, + CHAIN_ID_BSC, + sourceChain, + (chainId: number) => handleTargetChange({ target: { value: chainId } }), + ids, + isPandle + ); /* End pandle token check */ return ( @@ -237,7 +220,7 @@ function Source() { fullWidth value={targetChain} onChange={handleTargetChange} - disabled={shouldLockFields || isPandle} + disabled={shouldLockFields || isRoundTripTransfer} chains={targetChainOptions} /> @@ -260,7 +243,6 @@ function Source() { ) : ( <> - {isPandle && } {sourceChain === CHAIN_ID_SOLANA && CLUSTER === "mainnet" && ( )} @@ -276,7 +258,7 @@ function Source() { className={classes.transferField} value={amount} onChange={handleAmountChange} - disabled={shouldLockFields} + disabled={isTransferDisabled || shouldLockFields} onMaxClick={ uiAmountString && !parsedTokenAccount.isNativeAsset ? handleMaxClick @@ -284,18 +266,15 @@ function Source() { } /> ) : null} - - + {warnings.map((message, key) => ( + + ))} Next diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index 6c8ce2d2e..000000000 --- a/src/config.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ChainId } from "@certusone/wormhole-sdk"; -import { CHAIN_ID_AURORA } from "@certusone/wormhole-sdk"; - -export type DisableTransfers = boolean | "to" | "from"; - -export interface WarningMessage { - text: string; - link?: { - url: string; - text: string; - }; -} - -export interface ChainConfig { - disableTransfers?: DisableTransfers; - warningMessage?: WarningMessage; -} - -export type ChainConfigMap = { - [key in ChainId]?: ChainConfig; -}; - -export const CHAIN_CONFIG_MAP: ChainConfigMap = { - [CHAIN_ID_AURORA]: { - disableTransfers: true, - warningMessage: { - text: "As a precautionary measure, Wormhole Network and Portal have paused Aurora support temporarily.", - }, - } as ChainConfig, -}; diff --git a/src/config/transferRules.ts b/src/config/transferRules.ts new file mode 100644 index 000000000..dc96f5121 --- /dev/null +++ b/src/config/transferRules.ts @@ -0,0 +1,58 @@ +import { + CHAIN_ID_BSC, + CHAIN_ID_ETH, + CHAIN_ID_TERRA, + CHAIN_ID_AURORA, +} from "@certusone/wormhole-sdk"; +import { terra } from "@certusone/wormhole-sdk"; +import { Rule, PredicateArgs } from "../hooks/useWarningRulesEngine"; + +const ETHEREUM_PANDLE_ADDRESS = "0X808507121B80C02388FAD14726482E061B8DA827"; + +const isPandleFromEthereum = ( + sourceChain: number, + selectedTokenAddress: string | undefined +) => + sourceChain === CHAIN_ID_ETH && + selectedTokenAddress === ETHEREUM_PANDLE_ADDRESS; + +const isPandleFromBsc = ( + sourceChain: number, + selectedTokenAddress: string | undefined +) => + sourceChain === CHAIN_ID_BSC && + selectedTokenAddress === ETHEREUM_PANDLE_ADDRESS; + +const PANDLE_MESSAGE = + "Pandle transfers are limited to Ethereum to BSC and BSC to Ethereum."; +const AuroraMessage = + "As a precautionary measure, Wormhole Network and Portal have paused Aurora support temporarily."; +const TERRA_CLASSIC_MESSAGE = + "Transfers of native tokens to/from Terra Classic have been temporarily paused."; + +const transferRules: Rule[] = [ + { + id: "pandle", + predicate: ({ source, token }: PredicateArgs) => + isPandleFromEthereum(source, token?.toUpperCase()) || + isPandleFromBsc(source, token?.toUpperCase()), + text: PANDLE_MESSAGE, + }, + { + id: "aurora", + predicate: ({ source, target }: PredicateArgs) => + source === CHAIN_ID_AURORA || target === CHAIN_ID_AURORA, + text: AuroraMessage, + disableTransfer: true, + }, + { + id: "terra-classic-native", + predicate: ({ source, target, token }: PredicateArgs) => + (source === CHAIN_ID_TERRA || target === CHAIN_ID_TERRA) && + terra.isNativeDenom(token), + text: TERRA_CLASSIC_MESSAGE, + disableTransfer: true, + }, +]; + +export default transferRules; diff --git a/src/hooks/useRedeemControl.ts b/src/hooks/useRedeemControl.ts new file mode 100644 index 000000000..425d2ab04 --- /dev/null +++ b/src/hooks/useRedeemControl.ts @@ -0,0 +1,49 @@ +import { ChainId, tryHexToNativeString } from "@certusone/wormhole-sdk"; +import { useMemo } from "react"; +import { Rule, useWarningRulesEngine } from "./useWarningRulesEngine"; + +function parseOriginAsset(originAddress?: string, originChain?: ChainId) { + try { + if (originAddress && originChain) { + return tryHexToNativeString(originAddress, originChain); + } + } catch (err) { + console.log(err); + } +} + +/** + * Will calculate if a redeem is allowed or not + * by using 4 dimension: + * - sourceChain + * - targetChain + * - originAddress + * - originChain + * + * @param sourceChain Which chain is the transfer coming from + * @param targetChain Which chain is the transfer going to + * @param originAddress Origin address of the asset + * @param originChain Origin chain of the asset used to calculate + * the asset address or symbol + * @returns a set of warnings, a set of ids and a boolean indicating + * if the transfer is disabled or not + */ +export function useRedeemControl( + rules: Rule[], + sourceChain: ChainId, + targetChain: ChainId, + originAddress?: string, + originChain?: ChainId +) { + const asset = useMemo( + () => parseOriginAsset(originAddress, originChain) || "", + [originAddress, originChain] + ); + const { warnings, ids, isDisabled } = useWarningRulesEngine( + rules, + sourceChain, + targetChain, + asset + ); + return { warnings, ids, isRedeemDisabled: isDisabled }; +} diff --git a/src/hooks/useRoundTripTransfer.ts b/src/hooks/useRoundTripTransfer.ts new file mode 100644 index 000000000..33e532bc6 --- /dev/null +++ b/src/hooks/useRoundTripTransfer.ts @@ -0,0 +1,40 @@ +import { ChainId } from "@certusone/wormhole-sdk"; +import { useEffect, useState } from "react"; + +const isSource = (sourceChain: ChainId, selectedSourceChain: ChainId) => + sourceChain === selectedSourceChain; + +function useRoundTripTransfer( + source: ChainId, + target: ChainId, + selectedSourceChain: ChainId, + onRoundTripTransfer: (chainId: ChainId) => void = () => {}, + ids: string[] = [], + predicate: (id: string) => boolean = () => true +) { + const [isRoundTripTransfer, setIsRoundTripTransfer] = useState(false); + useEffect(() => { + const apply = ids.some(predicate) || predicate(""); + if (apply) { + if (isSource(source, selectedSourceChain)) { + onRoundTripTransfer(target); + } else if (isSource(target, selectedSourceChain)) { + onRoundTripTransfer(source); + } + setIsRoundTripTransfer(true); + } else { + setIsRoundTripTransfer(false); + } + }, [ + source, + target, + ids, + predicate, + onRoundTripTransfer, + selectedSourceChain, + ]); + + return isRoundTripTransfer; +} + +export default useRoundTripTransfer; diff --git a/src/hooks/useTransferControl.ts b/src/hooks/useTransferControl.ts new file mode 100644 index 000000000..f68518b86 --- /dev/null +++ b/src/hooks/useTransferControl.ts @@ -0,0 +1,41 @@ +import { ChainId } from "@certusone/wormhole-sdk"; +import { Rule, useWarningRulesEngine } from "./useWarningRulesEngine"; +import { useEffect, useState } from "react"; +import useOriginalAsset from "./useOriginalAsset"; + +/** + * Will calculate if a transfer is allowed or not + * by using 3 dimension: + * - sourceChain + * - targetChain + * - asset + * + * @param sourceChain Which chain is the transfer coming from + * @param targetChain Which chain is the transfer going to + * @param assset Which asset is being transferred + * @returns a set of warnings, a set of ids and a boolean indicating + * if the transfer is disabled or not + */ +export function useTransferControl( + rules: Rule[], + sourceChain: ChainId, + targetChain: ChainId, + assetOrToken: string = "" +) { + const [asset, setAsset] = useState(assetOrToken); + const info = useOriginalAsset(sourceChain, assetOrToken, false); + useEffect(() => { + if (!info.isFetching) { + setAsset(info.data?.originAddress || assetOrToken); + } + }, [assetOrToken, info]); + const { warnings, ids, isDisabled } = useWarningRulesEngine( + rules, + sourceChain, + targetChain, + asset + ); + return { warnings, ids, isTransferDisabled: isDisabled }; +} + +export default useTransferControl; diff --git a/src/hooks/useWarningRulesEngine.ts b/src/hooks/useWarningRulesEngine.ts new file mode 100644 index 000000000..b3a8293d7 --- /dev/null +++ b/src/hooks/useWarningRulesEngine.ts @@ -0,0 +1,63 @@ +import { ChainId } from "@certusone/wormhole-sdk"; +import { useEffect, useState } from "react"; + +export type WarningMessage = { + text: string; + link?: { + url: string; + text: string; + }; +}; + +export type PredicateArgs = { + source: ChainId; + target: ChainId; + token?: string; +}; + +export type Rule = WarningMessage & { + id?: string; + disableTransfer?: boolean; + predicate: (args: PredicateArgs) => boolean; +}; + +/** + * Will calculate if a transfer is allowed or not + * by using 3 dimension: + * - sourceChain + * - targetChain + * - asset + * + * @param sourceChain Which chain is the transfer coming from + * @param targetChain Which chain is the transfer going to + * @param assset Which asset is being transferred + * @returns a set of warnings, a set of ids and a boolean indicating + * if the transfer is disabled or not + */ +export function useWarningRulesEngine( + rules: Rule[], + sourceChain: ChainId, + targetChain: ChainId, + token: string +) { + const [ids, setIds] = useState([]); + const [isDisabled, setIsDisabled] = useState(false); + const [warnings, setWarnings] = useState([]); + useEffect(() => { + const appliedRules = rules.filter((rule) => + rule.predicate({ source: sourceChain, target: targetChain, token }) + ); + if (appliedRules.length > 0) { + setWarnings(appliedRules); + setIsDisabled(appliedRules.some((rule) => rule.disableTransfer)); + setIds( + appliedRules.filter((rule) => !!rule.id).map((rule) => `${rule.id}`) + ); + } else { + setWarnings([]); + setIds([]); + setIsDisabled(false); + } + }, [rules, sourceChain, targetChain, token]); + return { warnings, ids, isDisabled }; +} diff --git a/src/utils/consts.ts b/src/utils/consts.ts index a36943a0e..960ea03e3 100644 --- a/src/utils/consts.ts +++ b/src/utils/consts.ts @@ -37,7 +37,6 @@ import { } from "@certusone/wormhole-sdk"; import { clusterApiUrl } from "@solana/web3.js"; import { getAddress } from "ethers/lib/utils"; -import { CHAIN_CONFIG_MAP } from "../config"; import aptosIcon from "../icons/aptos.svg"; import acalaIcon from "../icons/acala.svg"; import algorandIcon from "../icons/algorand.svg"; @@ -1763,18 +1762,6 @@ export const ETH_POLYGON_WRAPPED_TOKENS = [ export const JUPITER_SWAP_BASE_URL = "https://jup.ag/swap"; -export const getIsTransferDisabled = ( - chainId: ChainId, - isSourceChain: boolean -) => { - const disableTransfers = CHAIN_CONFIG_MAP[chainId]?.disableTransfers; - return disableTransfers === "from" - ? isSourceChain - : disableTransfers === "to" - ? !isSourceChain - : !!disableTransfers; -}; - export const LUNA_ADDRESS = "uluna"; export const UST_ADDRESS = "uusd";