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";